mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
feat(agents): add prompt override and heartbeat controls
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
6ad199bff1771839d1ab1129c2bb27ff583cf5a2e60a5603fa87b8a34c0856d0 config-baseline.json
|
||||
cd556f8c976e535c710b5273c895bc5763650d67090e30dedc82cf227b2034d6 config-baseline.core.json
|
||||
64ff922efc6146d867f3858141772094a8a72cba99a8fd61878551175dd8c822 config-baseline.json
|
||||
5d0ce975352ff2b03077f6d71e9fe99ab0f0b118da0f72d47dc989c83f13d668 config-baseline.core.json
|
||||
d22f4414b79ee03d896e58d875c80523bcc12303cbacb1700261e6ec73945187 config-baseline.channel.json
|
||||
1891bcb68d80ab8b7546a2946b5a9d82b18c3e92ffd2c834d15928e73fa11564 config-baseline.plugin.json
|
||||
|
||||
@@ -1133,6 +1133,7 @@
|
||||
"prepack": "node --import tsx scripts/openclaw-prepack.ts",
|
||||
"prepare": "command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git config core.hooksPath git-hooks || exit 0",
|
||||
"prepush:ci": "bash scripts/prepush-ci.sh",
|
||||
"probe:anthropic:prompt": "node --import tsx scripts/anthropic-prompt-probe.ts",
|
||||
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/OpenClawProtocol/GatewayModels.swift apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
|
||||
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
|
||||
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
|
||||
|
||||
662
scripts/anthropic-prompt-probe.ts
Normal file
662
scripts/anthropic-prompt-probe.ts
Normal file
@@ -0,0 +1,662 @@
|
||||
import { spawn } from "node:child_process";
|
||||
// Live prompt probe for Anthropic setup-token and Claude CLI prompt-path debugging.
|
||||
// Usage:
|
||||
// OPENCLAW_PROMPT_TRANSPORT=direct|gateway
|
||||
// OPENCLAW_PROMPT_MODE=extra|override
|
||||
// OPENCLAW_PROMPT_TEXT='...'
|
||||
// OPENCLAW_PROMPT_CAPTURE=1
|
||||
// pnpm probe:anthropic:prompt
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { resolveOpenClawAgentDir } from "../src/agents/agent-paths.js";
|
||||
import { ensureAuthProfileStore, type AuthProfileCredential } from "../src/agents/auth-profiles.js";
|
||||
import { normalizeProviderId } from "../src/agents/model-selection.js";
|
||||
import { validateAnthropicSetupToken } from "../src/commands/auth-token.js";
|
||||
import { callGateway } from "../src/gateway/call.js";
|
||||
import { extractPayloadText } from "../src/gateway/test-helpers.agent-results.js";
|
||||
import { getFreePortBlockWithPermissionFallback } from "../src/test-utils/ports.js";
|
||||
|
||||
const TRANSPORT = process.env.OPENCLAW_PROMPT_TRANSPORT?.trim() === "direct" ? "direct" : "gateway";
|
||||
const GATEWAY_PROMPT_MODE =
|
||||
process.env.OPENCLAW_PROMPT_MODE?.trim() === "override" ? "override" : "extra";
|
||||
const PROMPT_TEXT = process.env.OPENCLAW_PROMPT_TEXT?.trim() ?? "";
|
||||
const PROMPT_LIST_JSON = process.env.OPENCLAW_PROMPT_LIST_JSON?.trim() ?? "";
|
||||
const USER_PROMPT = process.env.OPENCLAW_USER_PROMPT?.trim() || "is clawd here?";
|
||||
const ENABLE_CAPTURE = process.env.OPENCLAW_PROMPT_CAPTURE === "1";
|
||||
const INCLUDE_RAW = process.env.OPENCLAW_PROMPT_INCLUDE_RAW === "1";
|
||||
const CLAUDE_BIN = process.env.CLAUDE_BIN?.trim() || "claude";
|
||||
const NODE_BIN = process.env.OPENCLAW_NODE_BIN?.trim() || process.execPath;
|
||||
const TIMEOUT_MS = Number(process.env.OPENCLAW_PROMPT_TIMEOUT_MS ?? "45000");
|
||||
const GATEWAY_TIMEOUT_MS = Number(process.env.OPENCLAW_PROMPT_GATEWAY_TIMEOUT_MS ?? "120000");
|
||||
const SETUP_TOKEN_RAW = process.env.OPENCLAW_LIVE_SETUP_TOKEN?.trim() ?? "";
|
||||
const SETUP_TOKEN_VALUE = process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE?.trim() ?? "";
|
||||
const SETUP_TOKEN_PROFILE = process.env.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? "";
|
||||
const DIRECT_CLAUDE_ARGS = ["-p", "--append-system-prompt"];
|
||||
|
||||
if (!PROMPT_TEXT && !PROMPT_LIST_JSON) {
|
||||
throw new Error("missing OPENCLAW_PROMPT_TEXT or OPENCLAW_PROMPT_LIST_JSON");
|
||||
}
|
||||
|
||||
type CaptureSummary = {
|
||||
url?: string;
|
||||
authScheme?: string;
|
||||
xApp?: string;
|
||||
anthropicBeta?: string;
|
||||
systemBlockCount: number;
|
||||
systemBlocks: Array<{ index: number; bytes: number; preview: string }>;
|
||||
containsPromptExact: boolean;
|
||||
bodyContainsPromptExact: boolean;
|
||||
userBytes?: number;
|
||||
userPreview?: string;
|
||||
rawBody?: string;
|
||||
};
|
||||
|
||||
type PromptResult = {
|
||||
prompt: string;
|
||||
ok: boolean;
|
||||
transport: "direct" | "gateway";
|
||||
promptMode?: "extra" | "override";
|
||||
exitCode?: number | null;
|
||||
signal?: NodeJS.Signals | null;
|
||||
status?: string;
|
||||
text?: string;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
error?: string;
|
||||
matchedExtraUsage400: boolean;
|
||||
capture?: CaptureSummary;
|
||||
tmpDir: string;
|
||||
};
|
||||
|
||||
type ProxyCapture = {
|
||||
url?: string;
|
||||
authHeader?: string;
|
||||
xApp?: string;
|
||||
anthropicBeta?: string;
|
||||
systemTexts: string[];
|
||||
userText?: string;
|
||||
rawBody?: string;
|
||||
};
|
||||
|
||||
type TokenSource = {
|
||||
profileId: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
function toHeaderValue(value: string | string[] | undefined): string | undefined {
|
||||
return Array.isArray(value) ? value.join(", ") : value;
|
||||
}
|
||||
|
||||
function summarizeText(text: string, max = 120): string {
|
||||
const normalized = text.replace(/\s+/g, " ").trim();
|
||||
if (normalized.length <= max) {
|
||||
return normalized;
|
||||
}
|
||||
return `${normalized.slice(0, max - 1)}…`;
|
||||
}
|
||||
|
||||
function summarizeCapture(
|
||||
capture: ProxyCapture | undefined,
|
||||
prompt: string,
|
||||
): CaptureSummary | undefined {
|
||||
if (!capture) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
url: capture.url,
|
||||
authScheme: capture.authHeader?.split(/\s+/, 1)[0],
|
||||
xApp: capture.xApp,
|
||||
anthropicBeta: capture.anthropicBeta,
|
||||
systemBlockCount: capture.systemTexts.length,
|
||||
systemBlocks: capture.systemTexts.map((entry, index) => ({
|
||||
index,
|
||||
bytes: Buffer.byteLength(entry, "utf8"),
|
||||
preview: summarizeText(entry),
|
||||
})),
|
||||
containsPromptExact: capture.systemTexts.includes(prompt),
|
||||
bodyContainsPromptExact: capture.rawBody?.includes(prompt) ?? false,
|
||||
userBytes: capture.userText ? Buffer.byteLength(capture.userText, "utf8") : undefined,
|
||||
userPreview: capture.userText ? summarizeText(capture.userText) : undefined,
|
||||
rawBody: INCLUDE_RAW ? capture.rawBody : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function matchesExtraUsage400(...parts: Array<string | undefined>): boolean {
|
||||
return parts
|
||||
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes("third-party apps now draw from your extra usage");
|
||||
}
|
||||
|
||||
function isSetupToken(value: string): boolean {
|
||||
return value.startsWith("sk-ant-oat01-");
|
||||
}
|
||||
|
||||
function listSetupTokenProfiles(store: {
|
||||
profiles: Record<string, AuthProfileCredential>;
|
||||
}): Array<{ id: string; token: string }> {
|
||||
return Object.entries(store.profiles)
|
||||
.filter(([, cred]) => {
|
||||
if (cred.type !== "token") {
|
||||
return false;
|
||||
}
|
||||
if (normalizeProviderId(cred.provider) !== "anthropic") {
|
||||
return false;
|
||||
}
|
||||
return isSetupToken(cred.token ?? "");
|
||||
})
|
||||
.map(([id, cred]) => ({ id, token: cred.token ?? "" }));
|
||||
}
|
||||
|
||||
function pickSetupTokenProfile(candidates: Array<{ id: string; token: string }>): {
|
||||
id: string;
|
||||
token: string;
|
||||
} | null {
|
||||
const preferred = ["anthropic:setup-token-test", "anthropic:setup-token", "anthropic:default"];
|
||||
for (const id of preferred) {
|
||||
const match = candidates.find((entry) => entry.id === id);
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return candidates[0] ?? null;
|
||||
}
|
||||
|
||||
function validateSetupToken(value: string): string {
|
||||
const error = validateAnthropicSetupToken(value);
|
||||
if (error) {
|
||||
throw new Error(`invalid setup-token: ${error}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function resolveSetupTokenSource(): TokenSource {
|
||||
const explicitToken =
|
||||
(SETUP_TOKEN_RAW && isSetupToken(SETUP_TOKEN_RAW) ? SETUP_TOKEN_RAW : "") || SETUP_TOKEN_VALUE;
|
||||
if (explicitToken) {
|
||||
return {
|
||||
profileId: "anthropic:default",
|
||||
token: validateSetupToken(explicitToken),
|
||||
};
|
||||
}
|
||||
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const candidates = listSetupTokenProfiles(store);
|
||||
if (SETUP_TOKEN_PROFILE) {
|
||||
const match = candidates.find((entry) => entry.id === SETUP_TOKEN_PROFILE);
|
||||
if (!match) {
|
||||
throw new Error(`setup-token profile not found: ${SETUP_TOKEN_PROFILE}`);
|
||||
}
|
||||
return { profileId: match.id, token: validateSetupToken(match.token) };
|
||||
}
|
||||
const match = pickSetupTokenProfile(candidates);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
"no Anthropics setup-token profile found; set OPENCLAW_LIVE_SETUP_TOKEN_VALUE or OPENCLAW_LIVE_SETUP_TOKEN_PROFILE",
|
||||
);
|
||||
}
|
||||
return { profileId: match.id, token: validateSetupToken(match.token) };
|
||||
}
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
fallback: () => T,
|
||||
): Promise<T> {
|
||||
return await Promise.race([promise, sleep(timeoutMs).then(() => fallback())]);
|
||||
}
|
||||
|
||||
async function readRequestBody(req: http.IncomingMessage): Promise<Buffer> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of req) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
function extractProxyCapture(rawBody: string, req: http.IncomingMessage): ProxyCapture {
|
||||
let parsed: {
|
||||
system?: Array<{ text?: string }>;
|
||||
messages?: Array<{ role?: string; content?: unknown }>;
|
||||
} | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(rawBody) as typeof parsed;
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
const systemTexts = Array.isArray(parsed?.system)
|
||||
? parsed.system
|
||||
.map((entry) => (typeof entry?.text === "string" ? entry.text : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const userText = Array.isArray(parsed?.messages)
|
||||
? parsed.messages
|
||||
.filter((entry) => entry?.role === "user")
|
||||
.flatMap((entry) => {
|
||||
const content = entry?.content;
|
||||
if (typeof content === "string") {
|
||||
return [content];
|
||||
}
|
||||
if (!Array.isArray(content)) {
|
||||
return [];
|
||||
}
|
||||
return content
|
||||
.map((item) =>
|
||||
item && typeof item === "object" && "text" in item && typeof item.text === "string"
|
||||
? item.text
|
||||
: "",
|
||||
)
|
||||
.filter(Boolean);
|
||||
})
|
||||
.join("\n")
|
||||
: undefined;
|
||||
return {
|
||||
url: req.url ?? undefined,
|
||||
authHeader: toHeaderValue(req.headers.authorization),
|
||||
xApp: toHeaderValue(req.headers["x-app"]),
|
||||
anthropicBeta: toHeaderValue(req.headers["anthropic-beta"]),
|
||||
systemTexts,
|
||||
userText,
|
||||
rawBody,
|
||||
};
|
||||
}
|
||||
|
||||
async function startAnthropicProxy(params: { port: number; upstreamBaseUrl: string }) {
|
||||
let lastCapture: ProxyCapture | undefined;
|
||||
const sockets = new Set<import("node:net").Socket>();
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const method = req.method ?? "GET";
|
||||
const requestBody = await readRequestBody(req);
|
||||
const rawBody = requestBody.toString("utf8");
|
||||
lastCapture = extractProxyCapture(rawBody, req);
|
||||
|
||||
const upstreamUrl = new URL(req.url ?? "/", params.upstreamBaseUrl).toString();
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
const lower = key.toLowerCase();
|
||||
if (lower === "host" || lower === "content-length") {
|
||||
continue;
|
||||
}
|
||||
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
||||
}
|
||||
const upstreamRes = await fetch(upstreamUrl, {
|
||||
method,
|
||||
headers,
|
||||
body:
|
||||
method === "GET" || method === "HEAD" || requestBody.byteLength === 0
|
||||
? undefined
|
||||
: requestBody,
|
||||
duplex: "half",
|
||||
});
|
||||
const responseHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of upstreamRes.headers.entries()) {
|
||||
const lower = key.toLowerCase();
|
||||
if (
|
||||
lower === "content-length" ||
|
||||
lower === "content-encoding" ||
|
||||
lower === "transfer-encoding" ||
|
||||
lower === "connection" ||
|
||||
lower === "keep-alive"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
responseHeaders[key] = value;
|
||||
}
|
||||
res.writeHead(upstreamRes.status, responseHeaders);
|
||||
if (upstreamRes.body) {
|
||||
for await (const chunk of upstreamRes.body) {
|
||||
res.write(Buffer.from(chunk));
|
||||
}
|
||||
}
|
||||
res.end();
|
||||
} catch (error) {
|
||||
res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
|
||||
res.end(`proxy error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
server.on("connection", (socket) => {
|
||||
sockets.add(socket);
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(params.port, "127.0.0.1", () => resolve());
|
||||
});
|
||||
return {
|
||||
getLastCapture() {
|
||||
return lastCapture;
|
||||
},
|
||||
async stop() {
|
||||
for (const socket of sockets) {
|
||||
socket.destroy();
|
||||
}
|
||||
await withTimeout(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
}),
|
||||
1_000,
|
||||
() => undefined,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
return await getFreePortBlockWithPermissionFallback({
|
||||
offsets: [0, 1, 2, 4],
|
||||
fallbackBase: 44_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function runDirectPrompt(prompt: string): Promise<PromptResult> {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-direct-prompt-probe-"));
|
||||
const proxyPort = ENABLE_CAPTURE ? await getFreePort() : undefined;
|
||||
const proxy =
|
||||
ENABLE_CAPTURE && proxyPort
|
||||
? await startAnthropicProxy({ port: proxyPort, upstreamBaseUrl: "https://api.anthropic.com" })
|
||||
: undefined;
|
||||
|
||||
const stdout: string[] = [];
|
||||
const stderr: string[] = [];
|
||||
const child = spawn(CLAUDE_BIN, [...DIRECT_CLAUDE_ARGS, prompt, USER_PROMPT], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
...(proxyPort ? { ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxyPort}` } : {}),
|
||||
ANTHROPIC_API_KEY: "",
|
||||
ANTHROPIC_API_KEY_OLD: "",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
child.stdout.on("data", (chunk) => stdout.push(String(chunk)));
|
||||
child.stderr.on("data", (chunk) => stderr.push(String(chunk)));
|
||||
const exit = await withTimeout(
|
||||
new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => {
|
||||
child.once("exit", (code, signal) => resolve({ code, signal }));
|
||||
}),
|
||||
TIMEOUT_MS,
|
||||
() => {
|
||||
child.kill("SIGKILL");
|
||||
return { code: null, signal: "SIGKILL" as NodeJS.Signals };
|
||||
},
|
||||
);
|
||||
await proxy?.stop().catch(() => {});
|
||||
const joinedStdout = stdout.join("");
|
||||
const joinedStderr = stderr.join("");
|
||||
return {
|
||||
prompt,
|
||||
ok: exit.code === 0 && !matchesExtraUsage400(joinedStdout, joinedStderr),
|
||||
transport: "direct",
|
||||
exitCode: exit.code,
|
||||
signal: exit.signal,
|
||||
stdout: joinedStdout.trim() || undefined,
|
||||
stderr: joinedStderr.trim() || undefined,
|
||||
matchedExtraUsage400: matchesExtraUsage400(joinedStdout, joinedStderr),
|
||||
capture: summarizeCapture(proxy?.getLastCapture(), prompt),
|
||||
tmpDir,
|
||||
};
|
||||
}
|
||||
|
||||
async function startGatewayProcess(params: {
|
||||
port: number;
|
||||
gatewayToken: string;
|
||||
configPath: string;
|
||||
stateDir: string;
|
||||
agentDir: string;
|
||||
bundledPluginsDir: string;
|
||||
logPath: string;
|
||||
}) {
|
||||
const logFile = await fs.open(params.logPath, "a");
|
||||
const child = spawn(
|
||||
NODE_BIN,
|
||||
["openclaw.mjs", "gateway", "--port", String(params.port), "--bind", "loopback", "--force"],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_CONFIG_PATH: params.configPath,
|
||||
OPENCLAW_STATE_DIR: params.stateDir,
|
||||
OPENCLAW_AGENT_DIR: params.agentDir,
|
||||
OPENCLAW_GATEWAY_TOKEN: params.gatewayToken,
|
||||
OPENCLAW_SKIP_CHANNELS: "1",
|
||||
OPENCLAW_SKIP_GMAIL_WATCHER: "1",
|
||||
OPENCLAW_SKIP_CANVAS_HOST: "1",
|
||||
OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: "1",
|
||||
OPENCLAW_DISABLE_BONJOUR: "1",
|
||||
OPENCLAW_SKIP_CRON: "1",
|
||||
OPENCLAW_TEST_MINIMAL_GATEWAY: "1",
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: params.bundledPluginsDir,
|
||||
ANTHROPIC_API_KEY: "",
|
||||
ANTHROPIC_API_KEY_OLD: "",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
);
|
||||
child.stdout.on("data", (chunk) => void logFile.appendFile(chunk));
|
||||
child.stderr.on("data", (chunk) => void logFile.appendFile(chunk));
|
||||
return {
|
||||
async stop() {
|
||||
if (!child.killed) {
|
||||
child.kill("SIGINT");
|
||||
}
|
||||
const exited = await withTimeout(
|
||||
new Promise<boolean>((resolve) => child.once("exit", () => resolve(true))),
|
||||
1_500,
|
||||
() => false,
|
||||
);
|
||||
if (!exited && !child.killed) {
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
await logFile.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForGatewayReady(url: string, token: string): Promise<void> {
|
||||
const deadline = Date.now() + 45_000;
|
||||
let lastError = "gateway start timeout";
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
await callGateway({ url, token, method: "health", timeoutMs: 5_000 });
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = String(error);
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
throw new Error(lastError);
|
||||
}
|
||||
|
||||
async function readLogTail(logPath: string): Promise<string> {
|
||||
const raw = await fs.readFile(logPath, "utf8").catch(() => "");
|
||||
return raw.split(/\r?\n/).slice(-40).join("\n").trim();
|
||||
}
|
||||
|
||||
async function runGatewayPrompt(prompt: string): Promise<PromptResult> {
|
||||
const tokenSource = resolveSetupTokenSource();
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-prompt-probe-"));
|
||||
const stateDir = path.join(tmpDir, "state");
|
||||
const agentDir = path.join(stateDir, "agents", "main", "agent");
|
||||
const bundledPluginsDir = path.join(tmpDir, "bundled-plugins-empty");
|
||||
const configPath = path.join(tmpDir, "openclaw.json");
|
||||
const logPath = path.join(tmpDir, "gateway.log");
|
||||
const gatewayToken = `gw-${randomUUID()}`;
|
||||
const port = await getFreePort();
|
||||
const proxyPort = ENABLE_CAPTURE ? await getFreePort() : undefined;
|
||||
const proxy =
|
||||
ENABLE_CAPTURE && proxyPort
|
||||
? await startAnthropicProxy({ port: proxyPort, upstreamBaseUrl: "https://api.anthropic.com" })
|
||||
: undefined;
|
||||
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.mkdir(bundledPluginsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
gateway: {
|
||||
mode: "local",
|
||||
controlUi: { enabled: false },
|
||||
tailscale: { mode: "off" },
|
||||
},
|
||||
discovery: {
|
||||
mdns: { mode: "off" },
|
||||
wideArea: { enabled: false },
|
||||
},
|
||||
...(proxyPort
|
||||
? {
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: {
|
||||
baseUrl: `http://127.0.0.1:${proxyPort}`,
|
||||
api: "anthropic-messages",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
auth: {
|
||||
profiles: { [tokenSource.profileId]: { provider: "anthropic", mode: "token" } },
|
||||
order: { anthropic: [tokenSource.profileId] },
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
heartbeat: {
|
||||
includeSystemPromptSection: false,
|
||||
},
|
||||
...(GATEWAY_PROMPT_MODE === "override" ? { systemPromptOverride: prompt } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(agentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[tokenSource.profileId]: {
|
||||
type: "token",
|
||||
provider: "anthropic",
|
||||
token: tokenSource.token,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
const gateway = await startGatewayProcess({
|
||||
port,
|
||||
gatewayToken,
|
||||
configPath,
|
||||
stateDir,
|
||||
agentDir,
|
||||
bundledPluginsDir,
|
||||
logPath,
|
||||
});
|
||||
try {
|
||||
const url = `ws://127.0.0.1:${port}`;
|
||||
await waitForGatewayReady(url, gatewayToken);
|
||||
const agentRes = await callGateway<{ runId?: string }>({
|
||||
url,
|
||||
token: gatewayToken,
|
||||
method: "agent",
|
||||
params: {
|
||||
sessionKey: `agent:main:prompt-probe-${randomUUID()}`,
|
||||
idempotencyKey: `idem-${randomUUID()}`,
|
||||
message: "Reply with exactly: PROMPT PROBE OK.",
|
||||
...(GATEWAY_PROMPT_MODE === "extra" ? { extraSystemPrompt: prompt } : {}),
|
||||
deliver: false,
|
||||
},
|
||||
timeoutMs: 15_000,
|
||||
clientName: "cli",
|
||||
mode: "cli",
|
||||
});
|
||||
if (typeof agentRes.runId !== "string" || agentRes.runId.trim().length === 0) {
|
||||
return {
|
||||
prompt,
|
||||
ok: false,
|
||||
transport: "gateway",
|
||||
promptMode: GATEWAY_PROMPT_MODE,
|
||||
error: `missing runId: ${JSON.stringify(agentRes)}`,
|
||||
matchedExtraUsage400: false,
|
||||
capture: summarizeCapture(proxy?.getLastCapture(), prompt),
|
||||
tmpDir,
|
||||
};
|
||||
}
|
||||
const waitRes = await callGateway<{ status?: string; error?: string; payloads?: unknown[] }>({
|
||||
url,
|
||||
token: gatewayToken,
|
||||
method: "agent.wait",
|
||||
params: { runId: agentRes.runId, timeoutMs: GATEWAY_TIMEOUT_MS },
|
||||
timeoutMs: GATEWAY_TIMEOUT_MS + 10_000,
|
||||
clientName: "cli",
|
||||
mode: "cli",
|
||||
});
|
||||
const text = extractPayloadText(waitRes);
|
||||
const logTail = await readLogTail(logPath);
|
||||
const matched400 = matchesExtraUsage400(waitRes.error, logTail, JSON.stringify(waitRes));
|
||||
return {
|
||||
prompt,
|
||||
ok: waitRes.status === "ok" && !matched400,
|
||||
transport: "gateway",
|
||||
promptMode: GATEWAY_PROMPT_MODE,
|
||||
status: waitRes.status,
|
||||
text: text || undefined,
|
||||
error: waitRes.status === "ok" ? undefined : waitRes.error || logTail || "agent.wait failed",
|
||||
matchedExtraUsage400: matched400,
|
||||
capture: summarizeCapture(proxy?.getLastCapture(), prompt),
|
||||
tmpDir,
|
||||
};
|
||||
} finally {
|
||||
await gateway.stop().catch(() => {});
|
||||
await proxy?.stop().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const prompts = PROMPT_LIST_JSON ? (JSON.parse(PROMPT_LIST_JSON) as string[]) : [PROMPT_TEXT];
|
||||
const results: PromptResult[] = [];
|
||||
for (const prompt of prompts) {
|
||||
results.push(
|
||||
TRANSPORT === "direct" ? await runDirectPrompt(prompt) : await runGatewayPrompt(prompt),
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
transport: TRANSPORT,
|
||||
...(TRANSPORT === "gateway" ? { promptMode: GATEWAY_PROMPT_MODE } : {}),
|
||||
capture: ENABLE_CAPTURE,
|
||||
results,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -37,6 +37,7 @@ type ResolvedAgentConfig = {
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
systemPromptOverride?: AgentEntry["systemPromptOverride"];
|
||||
model?: AgentEntry["model"];
|
||||
thinkingDefault?: AgentEntry["thinkingDefault"];
|
||||
verboseDefault?: AgentDefaultsConfig["verboseDefault"];
|
||||
@@ -141,6 +142,7 @@ export function resolveAgentConfig(
|
||||
name: readStringValue(entry.name),
|
||||
workspace: readStringValue(entry.workspace),
|
||||
agentDir: readStringValue(entry.agentDir),
|
||||
systemPromptOverride: readStringValue(entry.systemPromptOverride),
|
||||
model:
|
||||
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
|
||||
? entry.model
|
||||
|
||||
@@ -132,6 +132,51 @@ describe("resolveBootstrapContextForRun", () => {
|
||||
|
||||
expect(files).toEqual([]);
|
||||
});
|
||||
|
||||
it("drops HEARTBEAT.md for non-heartbeat runs when the heartbeat prompt section is disabled", async () => {
|
||||
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
||||
await fs.writeFile(path.join(workspaceDir, "HEARTBEAT.md"), "check inbox", "utf8");
|
||||
await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "repo rules", "utf8");
|
||||
|
||||
const files = await resolveBootstrapFilesForRun({
|
||||
workspaceDir,
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
includeSystemPromptSection: false,
|
||||
},
|
||||
},
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(files.some((file) => file.name === "HEARTBEAT.md")).toBe(false);
|
||||
expect(files.some((file) => file.name === "AGENTS.md")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps HEARTBEAT.md for actual heartbeat runs even when the prompt section is disabled", async () => {
|
||||
const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
|
||||
await fs.writeFile(path.join(workspaceDir, "HEARTBEAT.md"), "check inbox", "utf8");
|
||||
|
||||
const files = await resolveBootstrapFilesForRun({
|
||||
workspaceDir,
|
||||
runKind: "heartbeat",
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
includeSystemPromptSection: false,
|
||||
},
|
||||
},
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(files.some((file) => file.name === "HEARTBEAT.md")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasCompletedBootstrapTurn", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { AgentContextInjection } from "../config/types.agent-defaults.js";
|
||||
import { resolveAgentConfig, resolveSessionAgentIds } from "./agent-scope.js";
|
||||
import { getOrLoadBootstrapFiles } from "./bootstrap-cache.js";
|
||||
import { applyBootstrapHookOverrides } from "./bootstrap-hooks.js";
|
||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
resolveBootstrapTotalMaxChars,
|
||||
} from "./pi-embedded-helpers.js";
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_FILENAME,
|
||||
filterBootstrapFilesForSession,
|
||||
loadWorkspaceBootstrapFiles,
|
||||
type WorkspaceBootstrapFile,
|
||||
@@ -142,6 +144,40 @@ function applyContextModeFilter(params: {
|
||||
return [];
|
||||
}
|
||||
|
||||
function shouldExcludeHeartbeatBootstrapFile(params: {
|
||||
config?: OpenClawConfig;
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
agentId?: string;
|
||||
runKind?: BootstrapContextRunKind;
|
||||
}): boolean {
|
||||
if (!params.config || params.runKind === "heartbeat") {
|
||||
return false;
|
||||
}
|
||||
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
if (sessionAgentId !== defaultAgentId) {
|
||||
return false;
|
||||
}
|
||||
const defaults = params.config.agents?.defaults?.heartbeat;
|
||||
const overrides = resolveAgentConfig(params.config, sessionAgentId)?.heartbeat;
|
||||
const merged = !defaults && !overrides ? overrides : { ...defaults, ...overrides };
|
||||
return merged?.includeSystemPromptSection === false;
|
||||
}
|
||||
|
||||
function filterHeartbeatBootstrapFile(
|
||||
files: WorkspaceBootstrapFile[],
|
||||
excludeHeartbeatBootstrapFile: boolean,
|
||||
): WorkspaceBootstrapFile[] {
|
||||
if (!excludeHeartbeatBootstrapFile) {
|
||||
return files;
|
||||
}
|
||||
return files.filter((file) => file.name !== DEFAULT_HEARTBEAT_FILENAME);
|
||||
}
|
||||
|
||||
export async function resolveBootstrapFilesForRun(params: {
|
||||
workspaceDir: string;
|
||||
config?: OpenClawConfig;
|
||||
@@ -152,6 +188,7 @@ export async function resolveBootstrapFilesForRun(params: {
|
||||
contextMode?: BootstrapContextMode;
|
||||
runKind?: BootstrapContextRunKind;
|
||||
}): Promise<WorkspaceBootstrapFile[]> {
|
||||
const excludeHeartbeatBootstrapFile = shouldExcludeHeartbeatBootstrapFile(params);
|
||||
const sessionKey = params.sessionKey ?? params.sessionId;
|
||||
const rawFiles = params.sessionKey
|
||||
? await getOrLoadBootstrapFiles({
|
||||
@@ -173,7 +210,10 @@ export async function resolveBootstrapFilesForRun(params: {
|
||||
sessionId: params.sessionId,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
return sanitizeBootstrapFiles(updated, params.warn);
|
||||
return sanitizeBootstrapFiles(
|
||||
filterHeartbeatBootstrapFile(updated, excludeHeartbeatBootstrapFile),
|
||||
params.warn,
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolveBootstrapContextForRun(params: {
|
||||
|
||||
@@ -68,7 +68,6 @@ setCliRunnerExecuteTestDeps({
|
||||
setCliRunnerPrepareTestDeps({
|
||||
makeBootstrapWarn: () => () => {},
|
||||
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
|
||||
resolveHeartbeatPrompt: async () => "",
|
||||
resolveOpenClawDocsPath: async () => null,
|
||||
});
|
||||
|
||||
@@ -375,7 +374,6 @@ export function restoreCliRunnerPrepareTestDeps() {
|
||||
setCliRunnerPrepareTestDeps({
|
||||
makeBootstrapWarn: () => () => {},
|
||||
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
|
||||
resolveHeartbeatPrompt: async () => "",
|
||||
resolveOpenClawDocsPath: async () => null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,11 +16,13 @@ import {
|
||||
import { resolveCliAuthEpoch } from "../cli-auth-epoch.js";
|
||||
import { resolveCliBackendConfig } from "../cli-backends.js";
|
||||
import { hashCliSessionText, resolveCliSessionReuse } from "../cli-session.js";
|
||||
import { resolveHeartbeatPromptForSystemPrompt } from "../heartbeat-system-prompt.js";
|
||||
import {
|
||||
resolveBootstrapMaxChars,
|
||||
resolveBootstrapPromptTruncationWarningMode,
|
||||
resolveBootstrapTotalMaxChars,
|
||||
} from "../pi-embedded-helpers.js";
|
||||
import { resolveSystemPromptOverride } from "../system-prompt-override.js";
|
||||
import { buildSystemPromptReport } from "../system-prompt-report.js";
|
||||
import { redactRunIdentifier, resolveRunWorkspaceDir } from "../workspace-run.js";
|
||||
import { prepareCliBundleMcpConfig } from "./bundle-mcp.js";
|
||||
@@ -33,9 +35,6 @@ const prepareDeps = {
|
||||
resolveBootstrapContextForRun: resolveBootstrapContextForRunImpl,
|
||||
getActiveMcpLoopbackRuntime,
|
||||
createMcpLoopbackServerConfig,
|
||||
resolveHeartbeatPrompt: async (
|
||||
prompt: Parameters<typeof import("../../auto-reply/heartbeat.js").resolveHeartbeatPrompt>[0],
|
||||
) => (await import("../../auto-reply/heartbeat.js")).resolveHeartbeatPrompt(prompt),
|
||||
resolveOpenClawDocsPath: async (
|
||||
params: Parameters<typeof import("../docs-path.js").resolveOpenClawDocsPath>[0],
|
||||
) => (await import("../docs-path.js")).resolveOpenClawDocsPath(params),
|
||||
@@ -148,29 +147,35 @@ export async function prepareCliRunContext(
|
||||
`cli session reset: provider=${params.provider} reason=${reusableCliSession.invalidatedReason}`,
|
||||
);
|
||||
}
|
||||
const heartbeatPrompt =
|
||||
sessionAgentId === defaultAgentId
|
||||
? await prepareDeps.resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
: undefined;
|
||||
const heartbeatPrompt = resolveHeartbeatPromptForSystemPrompt({
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
defaultAgentId,
|
||||
});
|
||||
const docsPath = await prepareDeps.resolveOpenClawDocsPath({
|
||||
workspaceDir,
|
||||
argv1: process.argv[1],
|
||||
cwd: process.cwd(),
|
||||
moduleUrl: import.meta.url,
|
||||
});
|
||||
const systemPrompt = buildSystemPrompt({
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
defaultThinkLevel: params.thinkLevel,
|
||||
extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
heartbeatPrompt,
|
||||
docsPath: docsPath ?? undefined,
|
||||
tools: [],
|
||||
contextFiles,
|
||||
modelDisplay,
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
const systemPrompt =
|
||||
resolveSystemPromptOverride({
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
}) ??
|
||||
buildSystemPrompt({
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
defaultThinkLevel: params.thinkLevel,
|
||||
extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
heartbeatPrompt,
|
||||
docsPath: docsPath ?? undefined,
|
||||
tools: [],
|
||||
contextFiles,
|
||||
modelDisplay,
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
const systemPromptReport = buildSystemPromptReport({
|
||||
source: "run",
|
||||
generatedAt: Date.now(),
|
||||
|
||||
74
src/agents/heartbeat-system-prompt.test.ts
Normal file
74
src/agents/heartbeat-system-prompt.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveHeartbeatPromptForSystemPrompt } from "./heartbeat-system-prompt.js";
|
||||
|
||||
describe("resolveHeartbeatPromptForSystemPrompt", () => {
|
||||
it("omits the heartbeat section when disabled in defaults", () => {
|
||||
expect(
|
||||
resolveHeartbeatPromptForSystemPrompt({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
includeSystemPromptSection: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
agentId: "main",
|
||||
defaultAgentId: "main",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("honors default-agent overrides for the prompt text", () => {
|
||||
expect(
|
||||
resolveHeartbeatPromptForSystemPrompt({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
prompt: "Default prompt",
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
heartbeat: {
|
||||
prompt: " Ops check ",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
agentId: "main",
|
||||
defaultAgentId: "main",
|
||||
}),
|
||||
).toBe("Ops check");
|
||||
});
|
||||
|
||||
it("does not inject the heartbeat section for non-default agents", () => {
|
||||
expect(
|
||||
resolveHeartbeatPromptForSystemPrompt({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
prompt: "Default prompt",
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "ops",
|
||||
heartbeat: {
|
||||
prompt: "Ops prompt",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
agentId: "ops",
|
||||
defaultAgentId: "main",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
38
src/agents/heartbeat-system-prompt.ts
Normal file
38
src/agents/heartbeat-system-prompt.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { resolveHeartbeatPrompt as resolveHeartbeatPromptText } from "../auto-reply/heartbeat.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
|
||||
import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope.js";
|
||||
|
||||
type HeartbeatConfig = AgentDefaultsConfig["heartbeat"];
|
||||
|
||||
function resolveHeartbeatConfigForSystemPrompt(
|
||||
config?: OpenClawConfig,
|
||||
agentId?: string,
|
||||
): HeartbeatConfig | undefined {
|
||||
const defaults = config?.agents?.defaults?.heartbeat;
|
||||
if (!config || !agentId) {
|
||||
return defaults;
|
||||
}
|
||||
const overrides = resolveAgentConfig(config, agentId)?.heartbeat;
|
||||
if (!defaults && !overrides) {
|
||||
return overrides;
|
||||
}
|
||||
return { ...defaults, ...overrides };
|
||||
}
|
||||
|
||||
export function resolveHeartbeatPromptForSystemPrompt(params: {
|
||||
config?: OpenClawConfig;
|
||||
agentId?: string;
|
||||
defaultAgentId?: string;
|
||||
}): string | undefined {
|
||||
const defaultAgentId = params.defaultAgentId ?? resolveDefaultAgentId(params.config ?? {});
|
||||
const agentId = params.agentId ?? defaultAgentId;
|
||||
if (!agentId || agentId !== defaultAgentId) {
|
||||
return undefined;
|
||||
}
|
||||
const heartbeat = resolveHeartbeatConfigForSystemPrompt(params.config, agentId);
|
||||
if (heartbeat?.includeSystemPromptSection === false) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveHeartbeatPromptText(heartbeat?.prompt);
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
estimateTokens,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { resolveHeartbeatPrompt } from "../../auto-reply/heartbeat.js";
|
||||
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@@ -56,6 +55,7 @@ import { resolveContextWindowInfo } from "../context-window-guard.js";
|
||||
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
||||
import { resolveOpenClawDocsPath } from "../docs-path.js";
|
||||
import { resolveHeartbeatPromptForSystemPrompt } from "../heartbeat-system-prompt.js";
|
||||
import {
|
||||
applyAuthHeaderOverride,
|
||||
applyLocalNoAuthHeaderOverride,
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
resolveSkillsPromptForRun,
|
||||
type SkillSnapshot,
|
||||
} from "../skills.js";
|
||||
import { resolveSystemPromptOverride } from "../system-prompt-override.js";
|
||||
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
||||
import { classifyCompactionReason, resolveCompactionFailureReason } from "./compact-reasons.js";
|
||||
import {
|
||||
@@ -707,7 +708,6 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
|
||||
const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat);
|
||||
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
|
||||
const isDefaultAgent = sessionAgentId === defaultAgentId;
|
||||
const promptMode =
|
||||
isSubagentSessionKey(params.sessionKey) || isCronSessionKey(params.sessionKey)
|
||||
? "minimal"
|
||||
@@ -738,36 +738,42 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
});
|
||||
const buildSystemPromptOverride = (defaultThinkLevel: ThinkLevel) =>
|
||||
createSystemPromptOverride(
|
||||
buildEmbeddedSystemPrompt({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
defaultThinkLevel,
|
||||
reasoningLevel: params.reasoningLevel ?? "off",
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
ownerDisplay: ownerDisplay.ownerDisplay,
|
||||
ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt: isDefaultAgent
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
: undefined,
|
||||
skillsPrompt,
|
||||
docsPath: docsPath ?? undefined,
|
||||
ttsHint,
|
||||
promptMode,
|
||||
acpEnabled: params.config?.acp?.enabled !== false,
|
||||
runtimeInfo,
|
||||
reactionGuidance,
|
||||
messageToolHints,
|
||||
sandboxInfo,
|
||||
tools: effectiveTools,
|
||||
modelAliasLines: buildModelAliasLines(params.config),
|
||||
userTimezone,
|
||||
userTime,
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
memoryCitationsMode: params.config?.memory?.citations,
|
||||
promptContribution,
|
||||
}),
|
||||
resolveSystemPromptOverride({
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
}) ??
|
||||
buildEmbeddedSystemPrompt({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
defaultThinkLevel,
|
||||
reasoningLevel: params.reasoningLevel ?? "off",
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
ownerDisplay: ownerDisplay.ownerDisplay,
|
||||
ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt: resolveHeartbeatPromptForSystemPrompt({
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
defaultAgentId,
|
||||
}),
|
||||
skillsPrompt,
|
||||
docsPath: docsPath ?? undefined,
|
||||
ttsHint,
|
||||
promptMode,
|
||||
acpEnabled: params.config?.acp?.enabled !== false,
|
||||
runtimeInfo,
|
||||
reactionGuidance,
|
||||
messageToolHints,
|
||||
sandboxInfo,
|
||||
tools: effectiveTools,
|
||||
modelAliasLines: buildModelAliasLines(params.config),
|
||||
userTimezone,
|
||||
userTime,
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
memoryCitationsMode: params.config?.memory?.citations,
|
||||
promptContribution,
|
||||
}),
|
||||
);
|
||||
|
||||
const compactionTimeoutMs = resolveCompactionTimeoutMs(params.config);
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
} from "../../../plugins/types.js";
|
||||
import { isCronSessionKey, isSubagentSessionKey } from "../../../routing/session-key.js";
|
||||
import { joinPresentTextSegments } from "../../../shared/text/join-segments.js";
|
||||
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
|
||||
import { buildActiveMusicGenerationTaskPromptContextForSession } from "../../music-generation-task-status.js";
|
||||
import { prependSystemPromptAdditionAfterCacheBoundary } from "../../system-prompt-cache-boundary.js";
|
||||
import { resolveEffectiveToolFsWorkspaceOnly } from "../../tool-fs-policy.js";
|
||||
@@ -92,10 +93,23 @@ export function resolvePromptModeForSession(sessionKey?: string): "minimal" | "f
|
||||
}
|
||||
|
||||
export function shouldInjectHeartbeatPrompt(params: {
|
||||
config?: OpenClawConfig;
|
||||
agentId?: string;
|
||||
defaultAgentId?: string;
|
||||
isDefaultAgent: boolean;
|
||||
trigger?: EmbeddedRunAttemptParams["trigger"];
|
||||
}): boolean {
|
||||
return params.isDefaultAgent && shouldInjectHeartbeatPromptForTrigger(params.trigger);
|
||||
return (
|
||||
params.isDefaultAgent &&
|
||||
shouldInjectHeartbeatPromptForTrigger(params.trigger) &&
|
||||
Boolean(
|
||||
resolveHeartbeatPromptForSystemPrompt({
|
||||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
defaultAgentId: params.defaultAgentId,
|
||||
}),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldWarnOnOrphanedUserRepair(
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { filterHeartbeatPairs } from "../../../auto-reply/heartbeat-filter.js";
|
||||
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
|
||||
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
|
||||
import { formatErrorMessage } from "../../../infra/errors.js";
|
||||
import { resolveHeartbeatSummaryForAgent } from "../../../infra/heartbeat-summary.js";
|
||||
@@ -58,6 +57,7 @@ import {
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../defaults.js";
|
||||
import { resolveOpenClawDocsPath } from "../../docs-path.js";
|
||||
import { isTimeoutError } from "../../failover-error.js";
|
||||
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
|
||||
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
|
||||
import { buildModelAliasLines } from "../../model-alias-lines.js";
|
||||
import { resolveModelAuthMode } from "../../model-auth.js";
|
||||
@@ -98,6 +98,7 @@ import {
|
||||
applySkillEnvOverridesFromSnapshot,
|
||||
resolveSkillsPromptForRun,
|
||||
} from "../../skills.js";
|
||||
import { resolveSystemPromptOverride } from "../../system-prompt-override.js";
|
||||
import { buildSystemPromptParams } from "../../system-prompt-params.js";
|
||||
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
||||
import { sanitizeToolCallIdsForCloudCodeAssist } from "../../tool-call-id.js";
|
||||
@@ -695,10 +696,17 @@ export async function runEmbeddedAttempt(
|
||||
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
|
||||
const ownerDisplay = resolveOwnerDisplaySetting(params.config);
|
||||
const heartbeatPrompt = shouldInjectHeartbeatPrompt({
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
defaultAgentId,
|
||||
isDefaultAgent,
|
||||
trigger: params.trigger,
|
||||
})
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
? resolveHeartbeatPromptForSystemPrompt({
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
defaultAgentId,
|
||||
})
|
||||
: undefined;
|
||||
const promptContribution = resolveProviderSystemPromptContribution({
|
||||
provider: params.provider,
|
||||
@@ -717,35 +725,40 @@ export async function runEmbeddedAttempt(
|
||||
},
|
||||
});
|
||||
|
||||
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
defaultThinkLevel: params.thinkLevel,
|
||||
reasoningLevel: params.reasoningLevel ?? "off",
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
ownerDisplay: ownerDisplay.ownerDisplay,
|
||||
ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt,
|
||||
skillsPrompt: effectiveSkillsPrompt,
|
||||
docsPath: docsPath ?? undefined,
|
||||
ttsHint,
|
||||
workspaceNotes,
|
||||
reactionGuidance,
|
||||
promptMode: effectivePromptMode,
|
||||
acpEnabled: params.config?.acp?.enabled !== false,
|
||||
runtimeInfo,
|
||||
messageToolHints,
|
||||
sandboxInfo,
|
||||
tools: effectiveTools,
|
||||
modelAliasLines: buildModelAliasLines(params.config),
|
||||
userTimezone,
|
||||
userTime,
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
memoryCitationsMode: params.config?.memory?.citations,
|
||||
promptContribution,
|
||||
});
|
||||
const appendPrompt =
|
||||
resolveSystemPromptOverride({
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
}) ??
|
||||
buildEmbeddedSystemPrompt({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
defaultThinkLevel: params.thinkLevel,
|
||||
reasoningLevel: params.reasoningLevel ?? "off",
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
ownerDisplay: ownerDisplay.ownerDisplay,
|
||||
ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt,
|
||||
skillsPrompt: effectiveSkillsPrompt,
|
||||
docsPath: docsPath ?? undefined,
|
||||
ttsHint,
|
||||
workspaceNotes,
|
||||
reactionGuidance,
|
||||
promptMode: effectivePromptMode,
|
||||
acpEnabled: params.config?.acp?.enabled !== false,
|
||||
runtimeInfo,
|
||||
messageToolHints,
|
||||
sandboxInfo,
|
||||
tools: effectiveTools,
|
||||
modelAliasLines: buildModelAliasLines(params.config),
|
||||
userTimezone,
|
||||
userTime,
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
memoryCitationsMode: params.config?.memory?.citations,
|
||||
promptContribution,
|
||||
});
|
||||
const systemPromptReport = buildSystemPromptReport({
|
||||
source: "run",
|
||||
generatedAt: Date.now(),
|
||||
|
||||
46
src/agents/system-prompt-override.test.ts
Normal file
46
src/agents/system-prompt-override.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSystemPromptOverride } from "./system-prompt-override.js";
|
||||
|
||||
describe("resolveSystemPromptOverride", () => {
|
||||
it("uses defaults when no per-agent override exists", () => {
|
||||
expect(
|
||||
resolveSystemPromptOverride({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: { systemPromptOverride: " default system " },
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
},
|
||||
agentId: "main",
|
||||
}),
|
||||
).toBe("default system");
|
||||
});
|
||||
|
||||
it("prefers the per-agent override", () => {
|
||||
expect(
|
||||
resolveSystemPromptOverride({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: { systemPromptOverride: "default system" },
|
||||
list: [{ id: "main", systemPromptOverride: " agent system " }],
|
||||
},
|
||||
},
|
||||
agentId: "main",
|
||||
}),
|
||||
).toBe("agent system");
|
||||
});
|
||||
|
||||
it("ignores blank override values", () => {
|
||||
expect(
|
||||
resolveSystemPromptOverride({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: { systemPromptOverride: "default system" },
|
||||
list: [{ id: "main", systemPromptOverride: " " }],
|
||||
},
|
||||
},
|
||||
agentId: "main",
|
||||
}),
|
||||
).toBe("default system");
|
||||
});
|
||||
});
|
||||
27
src/agents/system-prompt-override.ts
Normal file
27
src/agents/system-prompt-override.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveAgentConfig } from "./agent-scope.js";
|
||||
|
||||
function trimNonEmpty(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export function resolveSystemPromptOverride(params: {
|
||||
config?: OpenClawConfig;
|
||||
agentId?: string;
|
||||
}): string | undefined {
|
||||
const config = params.config;
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
const agentOverride = trimNonEmpty(
|
||||
params.agentId ? resolveAgentConfig(config, params.agentId)?.systemPromptOverride : undefined,
|
||||
);
|
||||
if (agentOverride) {
|
||||
return agentOverride;
|
||||
}
|
||||
return trimNonEmpty(config.agents?.defaults?.systemPromptOverride);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const EXPECTED_HEARTBEAT_KEYS = [
|
||||
"every",
|
||||
"model",
|
||||
"prompt",
|
||||
"includeSystemPromptSection",
|
||||
"ackMaxChars",
|
||||
"suppressToolErrorWarnings",
|
||||
"lightContext",
|
||||
|
||||
@@ -4715,6 +4715,12 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
prompt: {
|
||||
type: "string",
|
||||
},
|
||||
includeSystemPromptSection: {
|
||||
type: "boolean",
|
||||
title: "Heartbeat Include System Prompt Section",
|
||||
description:
|
||||
"Includes the default agent's ## Heartbeats system prompt section when true. Turn this off to keep heartbeat runtime behavior while omitting the heartbeat prompt instructions from the agent system prompt.",
|
||||
},
|
||||
ackMaxChars: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
@@ -5929,6 +5935,12 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
prompt: {
|
||||
type: "string",
|
||||
},
|
||||
includeSystemPromptSection: {
|
||||
type: "boolean",
|
||||
title: "Heartbeat Include System Prompt Section",
|
||||
description:
|
||||
"Per-agent override for whether the default agent's ## Heartbeats system prompt section is injected. Use false to keep heartbeat runtime behavior but omit the heartbeat prompt instructions from that agent's system prompt.",
|
||||
},
|
||||
ackMaxChars: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
@@ -25181,6 +25193,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
help: 'How embedded Pi handles workspace-local `.pi/config/settings.json`: "sanitize" (default) strips shellPath/shellCommandPrefix, "ignore" disables project settings entirely, and "trusted" applies project settings as-is.',
|
||||
tags: ["access"],
|
||||
},
|
||||
"agents.defaults.heartbeat.includeSystemPromptSection": {
|
||||
label: "Heartbeat Include System Prompt Section",
|
||||
help: "Includes the default agent's ## Heartbeats system prompt section when true. Turn this off to keep heartbeat runtime behavior while omitting the heartbeat prompt instructions from the agent system prompt.",
|
||||
tags: ["automation"],
|
||||
},
|
||||
"agents.list.*.heartbeat.includeSystemPromptSection": {
|
||||
label: "Heartbeat Include System Prompt Section",
|
||||
help: "Per-agent override for whether the default agent's ## Heartbeats system prompt section is injected. Use false to keep heartbeat runtime behavior but omit the heartbeat prompt instructions from that agent's system prompt.",
|
||||
tags: ["automation"],
|
||||
},
|
||||
"agents.defaults.heartbeat.directPolicy": {
|
||||
label: "Heartbeat Direct Policy",
|
||||
help: 'Controls whether heartbeat delivery may target direct/DM chats: "allow" (default) permits DM delivery and "block" suppresses direct-target sends.',
|
||||
|
||||
@@ -1490,6 +1490,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Shows degraded/error heartbeat alerts when true so operator channels surface problems promptly. Keep enabled in production so broken channel states are visible.",
|
||||
"channels.defaults.heartbeat.useIndicator":
|
||||
"Enables concise indicator-style heartbeat rendering instead of verbose status text where supported. Use indicator mode for dense dashboards with many active channels.",
|
||||
"agents.defaults.heartbeat.includeSystemPromptSection":
|
||||
"Includes the default agent's ## Heartbeats system prompt section when true. Turn this off to keep heartbeat runtime behavior while omitting the heartbeat prompt instructions from the agent system prompt.",
|
||||
"agents.list.*.heartbeat.includeSystemPromptSection":
|
||||
"Per-agent override for whether the default agent's ## Heartbeats system prompt section is injected. Use false to keep heartbeat runtime behavior but omit the heartbeat prompt instructions from that agent's system prompt.",
|
||||
"agents.defaults.heartbeat.directPolicy":
|
||||
'Controls whether heartbeat delivery may target direct/DM chats: "allow" (default) permits DM delivery and "block" suppresses direct-target sends.',
|
||||
"agents.list.*.heartbeat.directPolicy":
|
||||
|
||||
@@ -538,6 +538,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.compaction.memoryFlush.systemPrompt": "Compaction Memory Flush System Prompt",
|
||||
"agents.defaults.embeddedPi": "Embedded Pi",
|
||||
"agents.defaults.embeddedPi.projectSettingsPolicy": "Embedded Pi Project Settings Policy",
|
||||
"agents.defaults.heartbeat.includeSystemPromptSection": "Heartbeat Include System Prompt Section",
|
||||
"agents.list.*.heartbeat.includeSystemPromptSection": "Heartbeat Include System Prompt Section",
|
||||
"agents.defaults.heartbeat.directPolicy": "Heartbeat Direct Policy",
|
||||
"agents.list.*.heartbeat.directPolicy": "Heartbeat Direct Policy",
|
||||
"agents.defaults.heartbeat.suppressToolErrorWarnings": "Heartbeat Suppress Tool Error Warnings",
|
||||
|
||||
@@ -153,6 +153,8 @@ export type AgentDefaultsConfig = {
|
||||
skills?: string[];
|
||||
/** Optional repository root for system prompt runtime line (overrides auto-detect). */
|
||||
repoRoot?: string;
|
||||
/** Optional full system prompt replacement. Primarily for prompt debugging and controlled experiments. */
|
||||
systemPromptOverride?: string;
|
||||
/** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */
|
||||
skipBootstrap?: boolean;
|
||||
/**
|
||||
@@ -273,6 +275,8 @@ export type AgentDefaultsConfig = {
|
||||
accountId?: string;
|
||||
/** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK."). */
|
||||
prompt?: string;
|
||||
/** Include the ## Heartbeats system prompt section for the default agent (default: true). */
|
||||
includeSystemPromptSection?: boolean;
|
||||
/** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */
|
||||
ackMaxChars?: number;
|
||||
/** Suppress tool error warning payloads during heartbeat runs. */
|
||||
|
||||
@@ -64,6 +64,8 @@ export type AgentConfig = {
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
/** Optional per-agent full system prompt replacement. */
|
||||
systemPromptOverride?: AgentDefaultsConfig["systemPromptOverride"];
|
||||
model?: AgentModelConfig;
|
||||
/** Optional per-agent default thinking level (overrides agents.defaults.thinkingDefault). */
|
||||
thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive";
|
||||
|
||||
@@ -44,6 +44,7 @@ export const AgentDefaultsSchema = z
|
||||
workspace: z.string().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
repoRoot: z.string().optional(),
|
||||
systemPromptOverride: z.string().optional(),
|
||||
skipBootstrap: z.boolean().optional(),
|
||||
contextInjection: z.union([z.literal("always"), z.literal("continuation-skip")]).optional(),
|
||||
bootstrapMaxChars: z.number().int().positive().optional(),
|
||||
|
||||
@@ -31,6 +31,7 @@ export const HeartbeatSchema = z
|
||||
to: z.string().optional(),
|
||||
accountId: z.string().optional(),
|
||||
prompt: z.string().optional(),
|
||||
includeSystemPromptSection: z.boolean().optional(),
|
||||
ackMaxChars: z.number().int().nonnegative().optional(),
|
||||
suppressToolErrorWarnings: z.boolean().optional(),
|
||||
lightContext: z.boolean().optional(),
|
||||
@@ -777,6 +778,7 @@ export const AgentEntrySchema = z
|
||||
name: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
agentDir: z.string().optional(),
|
||||
systemPromptOverride: z.string().optional(),
|
||||
model: AgentModelSchema.optional(),
|
||||
thinkingDefault: z
|
||||
.enum(["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"])
|
||||
|
||||
@@ -39,6 +39,21 @@ export const HEARTBEAT_CONFIG_HONOR_INVENTORY: ConfigHonorInventoryRow[] = [
|
||||
reloadPaths: ["src/gateway/config-reload-plan.ts"],
|
||||
testPaths: ["src/infra/heartbeat-runner.returns-default-unset.test.ts"],
|
||||
},
|
||||
{
|
||||
key: "includeSystemPromptSection",
|
||||
schemaPaths: [
|
||||
"agents.defaults.heartbeat.includeSystemPromptSection",
|
||||
"agents.list.*.heartbeat.includeSystemPromptSection",
|
||||
],
|
||||
typePaths: ["src/config/types.agent-defaults.ts", "src/config/zod-schema.agent-runtime.ts"],
|
||||
mergePaths: ["src/agents/heartbeat-system-prompt.ts"],
|
||||
consumerPaths: [
|
||||
"src/agents/heartbeat-system-prompt.ts",
|
||||
"src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts",
|
||||
],
|
||||
reloadPaths: ["src/gateway/config-reload-plan.ts"],
|
||||
testPaths: ["src/agents/heartbeat-system-prompt.test.ts"],
|
||||
},
|
||||
{
|
||||
key: "ackMaxChars",
|
||||
schemaPaths: ["agents.defaults.heartbeat.ackMaxChars", "agents.list.*.heartbeat.ackMaxChars"],
|
||||
|
||||
Reference in New Issue
Block a user