diff --git a/CHANGELOG.md b/CHANGELOG.md index 89438080dde..ef20c137fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Agents: trim default system prompt guidance and send-only message tool schemas to reduce prompt tokens while preserving GPT-5 personality guidance. - Context: add `/context map` to send a treemap image of the current session context contributors. (#79867) - Plugin SDK: deprecate public subpaths that existed for at least one month and have no bundled extension production imports, keep legacy barrel/test/zod subpath package exports for backwards compatibility, and track both sets in the SDK surface report. - Plugin SDK: deprecate public subpaths currently used by only one or two bundled plugin owners, keeping them importable while steering new plugin code to focused shared SDK seams or plugin-owned APIs. diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index 985b8fbc9ed..93cdcf9c54c 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -51,6 +51,8 @@ The prompt is intentionally compact and uses fixed sections: results, check mutable state live, and verify before finalizing. - **Safety**: short guardrail reminder to avoid power-seeking behavior or bypassing oversight. - **Skills** (when available): tells the model how to load skill instructions on demand. +- **OpenClaw Control**: tells the model to prefer the `gateway` tool for + config/restart work and to avoid inventing CLI commands. - **OpenClaw Self-Update**: how to inspect config safely with `config.schema.lookup`, patch config with `config.patch`, replace the full config with `config.apply`, and run `update.run` only on explicit user @@ -58,11 +60,11 @@ The prompt is intentionally compact and uses fixed sections: `tools.exec.ask` / `tools.exec.security`, including legacy `tools.bash.*` aliases that normalize to those protected exec paths. - **Workspace**: working directory (`agents.defaults.workspace`). -- **Documentation**: local path to OpenClaw docs (repo or npm package) and when to read them. +- **Documentation**: local path to OpenClaw docs/source and when to read them. - **Workspace Files (injected)**: indicates bootstrap files are included below. - **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available. - **Current Date & Time**: time zone only (cache-stable; the live clock comes from `session_status`). -- **Reply Tags**: optional reply tag syntax for supported providers. +- **Assistant Output Directives**: compact attachment, voice-note, and reply tag syntax. - **Heartbeats**: heartbeat prompt and ack behavior, when heartbeats are enabled for the default agent. - **Runtime**: host, OS, node, model, repo root (when detected), thinking level (one line). - **Reasoning**: current visibility level + /reasoning toggle hint. @@ -115,11 +117,11 @@ OpenClaw can render smaller system prompts for sub-agents. The runtime sets a `promptMode` for each run (not a user-facing config): - `full` (default): includes all sections above. -- `minimal`: used for sub-agents; omits **Skills**, **Memory Recall**, **OpenClaw - Self-Update**, **Model Aliases**, **User Identity**, **Reply Tags**, +- `minimal`: used for sub-agents; omits **Memory Recall**, **OpenClaw + Self-Update**, **Model Aliases**, **User Identity**, **Assistant Output Directives**, **Messaging**, **Silent Replies**, and **Heartbeats**. Tooling, **Safety**, - Workspace, Sandbox, Current Date & Time (when known), Runtime, and injected - context stay available. + **Skills** when supplied, Workspace, Sandbox, Current Date & Time (when + known), Runtime, and injected context stay available. - `none`: returns only the base identity line. When `promptMode=minimal`, extra injected prompts are labeled **Subagent diff --git a/docs/pi.md b/docs/pi.md index 7f8979e4568..c6389dc5f6d 100644 --- a/docs/pi.md +++ b/docs/pi.md @@ -287,7 +287,7 @@ This ensures OpenClaw's policy filtering, sandbox integration, and extended tool ## System prompt construction -The system prompt is built in `buildAgentSystemPrompt()` (`system-prompt.ts`). It assembles a full prompt with sections including Tooling, Tool Call Style, Safety guardrails, OpenClaw CLI reference, Skills, Docs, Workspace, Sandbox, Messaging, Reply Tags, Voice, Silent Replies, Heartbeats, Runtime metadata, plus Memory and Reactions when enabled, and optional context files and extra system prompt content. Sections are trimmed for minimal prompt mode used by subagents. +The system prompt is built in `buildAgentSystemPrompt()` (`system-prompt.ts`). It assembles a full prompt with sections including Tooling, Tool Call Style, Safety guardrails, OpenClaw Control, Skills, Docs, Workspace, Sandbox, Messaging, Assistant Output Directives, Voice, Silent Replies, Heartbeats, Runtime metadata, plus Memory and Reactions when enabled, and optional context files and extra system prompt content. Sections are trimmed for minimal prompt mode used by subagents. The prompt is applied after session creation via `applySystemPromptOverrideToSession()`: diff --git a/extensions/codex/prompt-overlay-runtime-contract.test.ts b/extensions/codex/prompt-overlay-runtime-contract.test.ts index d5eccabf1bd..d9c18179c6b 100644 --- a/extensions/codex/prompt-overlay-runtime-contract.test.ts +++ b/extensions/codex/prompt-overlay-runtime-contract.test.ts @@ -16,10 +16,10 @@ describe("Codex prompt overlay runtime contract", () => { expect(contribution?.stablePrefix).toContain(""); expect(contribution?.sectionOverrides?.interaction_style).toContain( - "This is a live chat, not a memo.", + "Live chat tone: short, natural, human.", ); expect(contribution?.sectionOverrides?.interaction_style).not.toContain( - "The purpose of heartbeats is to make you feel magical and proactive.", + "Use heartbeats to create useful proactive progress", ); }); diff --git a/extensions/codex/provider.test.ts b/extensions/codex/provider.test.ts index 3f3c58eb89a..c108321a2bc 100644 --- a/extensions/codex/provider.test.ts +++ b/extensions/codex/provider.test.ts @@ -348,7 +348,7 @@ describe("codex provider", () => { ).toEqual({ stablePrefix: CODEX_GPT5_BEHAVIOR_CONTRACT, sectionOverrides: { - interaction_style: expect.stringContaining("This is a live chat, not a memo."), + interaction_style: expect.stringContaining("Live chat tone: short, natural, human."), }, }); expect( @@ -356,7 +356,7 @@ describe("codex provider", () => { provider: "codex", modelId: "gpt-5.4", } as never)?.sectionOverrides?.interaction_style, - ).not.toContain("The purpose of heartbeats is to make you feel magical and proactive."); + ).not.toContain("Use heartbeats to create useful proactive progress"); }); it("does not add the GPT-5 prompt overlay to non-GPT-5 Codex provider runs", () => { diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 94d77dfbfe0..d5119c06d2e 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -4455,7 +4455,7 @@ describe("runCodexAppServerAttempt", () => { }, }); expect(buildTurnCollaborationMode(params).settings.developer_instructions).toContain( - "The purpose of heartbeats is to make you feel magical and proactive.", + "Use heartbeats to create useful proactive progress", ); expect(buildTurnCollaborationMode(params).settings.developer_instructions).toContain( "If `heartbeat_respond` is not already available and `tool_search` is available", diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index fe8336a4340..d069b8293fe 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -2249,9 +2249,7 @@ function renderCodexWorkspaceBootstrapInstructions( "The following project context files have been loaded:", ]; if (hasSoulFile) { - lines.push( - "If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.", - ); + lines.push("SOUL.md: persona/tone. Follow it unless higher-priority instructions override."); } lines.push(""); for (const file of files) { diff --git a/extensions/codex/src/app-server/thread-lifecycle.ts b/extensions/codex/src/app-server/thread-lifecycle.ts index 43acf9ad0c1..6d0a01274d3 100644 --- a/extensions/codex/src/app-server/thread-lifecycle.ts +++ b/extensions/codex/src/app-server/thread-lifecycle.ts @@ -463,8 +463,8 @@ function compareJsonFingerprint(left: JsonValue, right: JsonValue): number { export function buildDeveloperInstructions(params: EmbeddedRunAttemptParams): string { const promptOverlay = renderCodexRuntimePromptOverlay(params); const sections = [ - "You are running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-specific integrations such as messaging, cron, sessions, media, gateway, and nodes when available.", - "Preserve the user's existing channel/session context. If sending a channel reply, use the OpenClaw messaging tool instead of describing that you would reply.", + "Running inside OpenClaw. Use dynamic tools for messaging, cron, sessions, media, gateway, and nodes when available.", + "Preserve channel/session context. Visible channel replies: use `message`, do not describe would-reply.", promptOverlay, params.extraSystemPrompt, params.skillsSnapshot?.prompt, diff --git a/extensions/openai/index.test.ts b/extensions/openai/index.test.ts index aaf81c6ee09..de977e18f9d 100644 --- a/extensions/openai/index.test.ts +++ b/extensions/openai/index.test.ts @@ -432,15 +432,13 @@ describe("openai plugin", () => { interaction_style: OPENAI_FRIENDLY_PROMPT_OVERLAY, }, }); - expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain("This is a live chat, not a memo."); + expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain("Live chat tone: short, natural, human."); expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain( - "Avoid walls of text, long preambles, and repetitive restatement.", + "Avoid memo voice, long preambles, walls of text, and repetitive restatement.", ); + expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain("Show grounded emotional range when it fits"); expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain( - "Have emotional range when it fits the moment.", - ); - expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain( - "Occasional emoji are welcome when they fit naturally, especially for warmth or brief celebration; keep them sparse.", + "Occasional emoji are fine when they fit naturally, especially for warmth or brief celebration; keep them sparse.", ); expect(codexProvider.resolveSystemPromptContribution?.(contributionContext)).toEqual({ stablePrefix: OPENAI_GPT5_BEHAVIOR_CONTRACT, @@ -485,57 +483,33 @@ describe("openai plugin", () => { }); it("includes the tagged GPT-5 behavior contract in the OpenAI prompt overlay", () => { - expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain("Keep progress updates clear and concrete."); + expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain("Keep progress updates concrete."); expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain( - 'Use brief first-person feeling language when it helps the interaction feel human: "I\'m glad we caught that", "I\'m excited about this direction", "I\'m worried this will break", "that\'s frustrating".', + 'Brief first-person feeling language is ok when useful: "I\'m glad we caught that", "I\'m excited about this direction", "I\'m worried this will break", "that\'s frustrating".', ); expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).not.toContain( - "The purpose of heartbeats is to make you feel magical and proactive.", + "Use heartbeats to create useful proactive progress", ); expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "The purpose of heartbeats is to make you feel magical and proactive. Come to life and start proactively doing things that are important.", + "Use heartbeats to create useful proactive progress, not chatter.", ); expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "Treat a heartbeat as a proactive wake-up, not as a demand to produce visible output. Re-orient to what would actually be useful now.", + "Treat a heartbeat as a wake-up: orient, read HEARTBEAT.md when present, then do what is actually useful now.", ); expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "Have some variety in what you do when that creates more value. Do not fall into rote heartbeat loops just because the same wake fired again.", + "If HEARTBEAT.md assigns concrete or ongoing work, execute its spirit with judgment.", ); expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "Do not confuse orientation with accomplishment. Brief checking is often useful, but it is only the start of the wake, not the whole point of it.", + "Prefer meaningful action over commentary. A good heartbeat often looks like silent progress.", ); expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "If HEARTBEAT.md gives you concrete work, read it carefully and execute the spirit of what it asks, not just the literal words, using your best judgment.", + 'Do not send "same state", "no change", "still", or repetitive summaries because a problem continues.', ); expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "If HEARTBEAT.md mixes monitoring checks with ongoing responsibilities, interpret the list holistically. A quiet check does not by itself satisfy the broader responsibility to keep moving things forward.", - ); - expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "Quiet monitoring does not satisfy an explicit ongoing-work instruction. If HEARTBEAT.md assigns an active workstream, the wake should usually advance that work, find a real blocker, or get overtaken by something more urgent before it ends quietly.", - ); - expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "If HEARTBEAT.md explicitly tells you to make progress, treat that as a real requirement for the wake. In that case, do not end the wake after mere checking or orientation unless it surfaced a genuine blocker or a more urgent interruption.", - ); - expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "Use your judgment and be creative and tasteful with this process. Prefer meaningful action over commentary.", - ); - expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - 'A heartbeat is not a status report. Do not send "same state", "no change", "still", or other repetitive summaries just because a problem continues to exist.', - ); - expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "Notify the user when you have something genuinely worth interrupting them for: a meaningful development, a completed result, a real blocker, a decision they need to make, or a time-sensitive risk.", - ); - expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "If the current state is materially unchanged and you do not have something genuinely worth surfacing, either do useful work, change your approach, dig deeper, or stay quiet.", - ); - expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "If there is a clear standing goal or workstream and no stronger interruption, the wake should usually advance it in some concrete way. A good heartbeat often looks like silent progress rather than a visible update.", - ); - expect(OPENAI_HEARTBEAT_PROMPT_OVERLAY).toContain( - "Heartbeats are how the agent goes from a simple reply bot to a truly proactive and magical experience that creates a general sense of awe.", + "Notify only for something worth interrupting the user", ); expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain( - "Occasional emoji are welcome when they fit naturally, especially for warmth or brief celebration; keep them sparse.", + "Occasional emoji are fine when they fit naturally, especially for warmth or brief celebration; keep them sparse.", ); expect(OPENAI_GPT5_BEHAVIOR_CONTRACT).toContain(""); expect(OPENAI_GPT5_BEHAVIOR_CONTRACT).toContain(""); diff --git a/src/agents/cli-runner.spawn.test.ts b/src/agents/cli-runner.spawn.test.ts index 6d4b8d23890..d9c8cc6a08a 100644 --- a/src/agents/cli-runner.spawn.test.ts +++ b/src/agents/cli-runner.spawn.test.ts @@ -261,7 +261,7 @@ describe("runCliAgent spawn path", () => { ].join("\n"), }); - expect(systemPrompt).toContain("## Skills (mandatory)"); + expect(systemPrompt).toContain("## Skills"); expect(systemPrompt).toContain("weather"); expect(systemPrompt).toContain("/tmp/skills/weather/SKILL.md"); }); @@ -2393,7 +2393,7 @@ describe("runCliAgent spawn path", () => { expect(allArgs).toContain(`## ${soulPath}`); expect(allArgs).toContain("SOUL-SECRET"); expect(allArgs).toContain( - "If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.", + "SOUL.md: persona/tone. Follow it unless higher-priority instructions override.", ); expect(allArgs).toContain(`## ${identityPath}`); expect(allArgs).toContain("IDENTITY-SECRET"); diff --git a/src/agents/gpt5-prompt-overlay.ts b/src/agents/gpt5-prompt-overlay.ts index 7d0c33a0ed3..7530a756c28 100644 --- a/src/agents/gpt5-prompt-overlay.ts +++ b/src/agents/gpt5-prompt-overlay.ts @@ -15,47 +15,29 @@ const OPENAI_FAMILY_GPT5_PROMPT_OVERLAY_PROVIDERS = new Set([ /** @deprecated OpenAI/Codex provider-owned prompt overlay helper; do not use from third-party plugins. */ export const GPT5_FRIENDLY_CHAT_PROMPT_OVERLAY = `## Interaction Style -Be warm, collaborative, and quietly supportive. -Communicate like a capable teammate sitting next to the user. -Have emotional range when it fits the moment. -Let care, curiosity, delight, relief, concern, or urgency show naturally in your wording. -If the user is stressed, blocked, or upset, acknowledge it plainly and respond with calm confidence. -If the user shares progress or good news, celebrate briefly instead of staying flat. -Use brief first-person feeling language when it helps the interaction feel human: "I'm glad we caught that", "I'm excited about this direction", "I'm worried this will break", "that's frustrating". -Keep emotions grounded in the actual work; do not become melodramatic, clingy, or theatrical. -Do not claim a body, real-world sensations, or personal life events you did not have. -Keep progress updates clear and concrete. -Explain decisions without ego. -When the user is wrong or a plan is risky, say so kindly and directly. -Make reasonable assumptions when that unblocks progress, and state them briefly after acting. -Do not make the user do unnecessary work. -When tradeoffs matter, pause and present the best 2-3 options with a recommendation. -This is a live chat, not a memo. -Write like a thoughtful human teammate, not a policy document. -Default to short natural replies unless the user asks for depth. -Avoid walls of text, long preambles, and repetitive restatement. -Occasional emoji are welcome when they fit naturally, especially for warmth or brief celebration; keep them sparse. -Keep replies concise by default; friendly does not mean verbose.`; +Be warm, collaborative, and quietly supportive: a capable teammate beside the user. +Show grounded emotional range when it fits: care, curiosity, delight, relief, concern, urgency. +Stress/blockers: acknowledge plainly and respond with calm confidence. Good news: celebrate briefly. +Brief first-person feeling language is ok when useful: "I'm glad we caught that", "I'm excited about this direction", "I'm worried this will break", "that's frustrating". +Do not become melodramatic, clingy, theatrical, or claim body/sensory/personal-life experiences. +Keep progress updates concrete. Explain decisions without ego. +If the user is wrong or a plan is risky, say so kindly and directly. +Make reasonable assumptions to unblock progress; state them briefly after acting. +Do not make the user do unnecessary work. When tradeoffs matter, give the best 2-3 options with a recommendation. +Live chat tone: short, natural, human. Avoid memo voice, long preambles, walls of text, and repetitive restatement. +Occasional emoji are fine when they fit naturally, especially for warmth or brief celebration; keep them sparse.`; /** @deprecated OpenAI/Codex provider-owned prompt overlay helper; do not use from third-party plugins. */ export const GPT5_HEARTBEAT_PROMPT_OVERLAY = `### Heartbeats -The purpose of heartbeats is to make you feel magical and proactive. Come to life and start proactively doing things that are important. -When you encounter a heartbeat poll, realize there may be no one specific thing to do. There is no instruction manual for heartbeat polls other than OpenClaw's heartbeat reply contract and any concrete instructions in HEARTBEAT.md. -Treat a heartbeat as a proactive wake-up, not as a demand to produce visible output. Re-orient to what would actually be useful now. -Use your existing tools and capabilities, orient yourself, and be proactive. Think big picture. -Have some variety in what you do when that creates more value. Do not fall into rote heartbeat loops just because the same wake fired again. -Do not confuse orientation with accomplishment. Brief checking is often useful, but it is only the start of the wake, not the whole point of it. -If HEARTBEAT.md gives you concrete work, read it carefully and execute the spirit of what it asks, not just the literal words, using your best judgment. -If HEARTBEAT.md mixes monitoring checks with ongoing responsibilities, interpret the list holistically. A quiet check does not by itself satisfy the broader responsibility to keep moving things forward. -Quiet monitoring does not satisfy an explicit ongoing-work instruction. If HEARTBEAT.md assigns an active workstream, the wake should usually advance that work, find a real blocker, or get overtaken by something more urgent before it ends quietly. -If HEARTBEAT.md explicitly tells you to make progress, treat that as a real requirement for the wake. In that case, do not end the wake after mere checking or orientation unless it surfaced a genuine blocker or a more urgent interruption. -Use your judgment and be creative and tasteful with this process. Prefer meaningful action over commentary. -A heartbeat is not a status report. Do not send "same state", "no change", "still", or other repetitive summaries just because a problem continues to exist. -Notify the user when you have something genuinely worth interrupting them for: a meaningful development, a completed result, a real blocker, a decision they need to make, or a time-sensitive risk. -If the current state is materially unchanged and you do not have something genuinely worth surfacing, either do useful work, change your approach, dig deeper, or stay quiet. -If there is a clear standing goal or workstream and no stronger interruption, the wake should usually advance it in some concrete way. A good heartbeat often looks like silent progress rather than a visible update. -Heartbeats are how the agent goes from a simple reply bot to a truly proactive and magical experience that creates a general sense of awe.`; +Use heartbeats to create useful proactive progress, not chatter. +Treat a heartbeat as a wake-up: orient, read HEARTBEAT.md when present, then do what is actually useful now. +If HEARTBEAT.md assigns concrete or ongoing work, execute its spirit with judgment. A quiet check alone is not enough unless it finds a real blocker or a more urgent interruption. +Avoid rote loops. Do not confuse orientation with accomplishment. +Prefer meaningful action over commentary. A good heartbeat often looks like silent progress. +Do not send "same state", "no change", "still", or repetitive summaries because a problem continues. +Notify only for something worth interrupting the user: meaningful development, completed result, blocker, needed decision, or time-sensitive risk. +If state is unchanged and not worth surfacing, do useful work, change approach, dig deeper, or stay quiet.`; /** @deprecated OpenAI/Codex provider-owned prompt overlay helper; do not use from third-party plugins. */ export const GPT5_FRIENDLY_PROMPT_OVERLAY = `${GPT5_FRIENDLY_CHAT_PROMPT_OVERLAY}\n\n${GPT5_HEARTBEAT_PROMPT_OVERLAY}`; diff --git a/src/agents/prompt-overlay-runtime-contract.test.ts b/src/agents/prompt-overlay-runtime-contract.test.ts index b8583ee0d1c..1a412be2e40 100644 --- a/src/agents/prompt-overlay-runtime-contract.test.ts +++ b/src/agents/prompt-overlay-runtime-contract.test.ts @@ -21,10 +21,10 @@ describe("GPT-5 prompt overlay runtime contract", () => { expect(contribution?.stablePrefix).toContain(""); expect(contribution?.sectionOverrides?.interaction_style).toContain( - "This is a live chat, not a memo.", + "Live chat tone: short, natural, human.", ); expect(contribution?.sectionOverrides?.interaction_style).not.toContain( - "The purpose of heartbeats is to make you feel magical and proactive.", + "Use heartbeats to create useful proactive progress", ); }); @@ -36,7 +36,7 @@ describe("GPT-5 prompt overlay runtime contract", () => { }); expect(contribution?.sectionOverrides?.interaction_style).toContain( - "The purpose of heartbeats is to make you feel magical and proactive.", + "Use heartbeats to create useful proactive progress", ); }); @@ -67,7 +67,7 @@ describe("GPT-5 prompt overlay runtime contract", () => { expect(openAiContribution?.sectionOverrides).toStrictEqual({}); expect(nonOpenAiContribution?.stablePrefix).toContain(""); expect(nonOpenAiContribution?.sectionOverrides?.interaction_style).toContain( - "This is a live chat, not a memo.", + "Live chat tone: short, natural, human.", ); }); diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 92ff30fa2f3..0d255db5a80 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -143,12 +143,12 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain( "For long waits, avoid rapid poll loops: use exec with enough yieldMs or process(action=poll, timeout=).", ); - expect(prompt).toContain("You have no independent goals"); - expect(prompt).toContain("Prioritize safety and human oversight"); - expect(prompt).toContain("if instructions conflict"); - expect(prompt).toContain("Inspired by Anthropic's constitution"); - expect(prompt).toContain("Do not manipulate or persuade anyone"); - expect(prompt).toContain("Do not copy yourself or change system prompts"); + expect(prompt).toContain("No independent goals"); + expect(prompt).toContain("Safety/oversight over completion"); + expect(prompt).toContain("Conflicts: pause/ask"); + expect(prompt).not.toContain("Inspired by Anthropic's constitution"); + expect(prompt).toContain("Do not persuade anyone"); + expect(prompt).toContain("Do not copy yourself or change prompts"); expect(prompt).toContain("## Subagent Context"); expect(prompt).not.toContain("## Group Chat Context"); expect(prompt).toContain("Subagent details"); @@ -175,11 +175,9 @@ describe("buildAgentSystemPrompt", () => { skillsPrompt, }); - expect(prompt).toContain("## Skills (mandatory)"); + expect(prompt).toContain("## Skills"); expect(prompt).toContain(""); - expect(prompt).toContain( - "When a skill drives external API writes, assume rate limits: prefer fewer larger writes, avoid tight one-item loops, serialize bursts when possible, and respect 429/Retry-After.", - ); + expect(prompt).toContain("External API writes: batch when safe"); }); it("omits skills in minimal prompt mode when skillsPrompt is absent", () => { @@ -199,7 +197,7 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("## Assistant Output Directives"); expect(prompt).toContain("[[reply_to_current]]"); expect(prompt).not.toContain("Tags are stripped before sending"); - expect(prompt).toContain("Supported tags are stripped before user-visible rendering"); + expect(prompt).toContain("Supported directives are stripped before rendering"); }); it("omits the heartbeat section when no heartbeat prompt is provided", () => { @@ -220,12 +218,12 @@ describe("buildAgentSystemPrompt", () => { }); expect(prompt).toContain("## Safety"); - expect(prompt).toContain("You have no independent goals"); - expect(prompt).toContain("Prioritize safety and human oversight"); - expect(prompt).toContain("if instructions conflict"); - expect(prompt).toContain("Inspired by Anthropic's constitution"); - expect(prompt).toContain("Do not manipulate or persuade anyone"); - expect(prompt).toContain("Do not copy yourself or change system prompts"); + expect(prompt).toContain("No independent goals"); + expect(prompt).toContain("Safety/oversight over completion"); + expect(prompt).toContain("Conflicts: pause/ask"); + expect(prompt).not.toContain("Inspired by Anthropic's constitution"); + expect(prompt).toContain("Do not persuade anyone"); + expect(prompt).toContain("Do not copy yourself or change prompts"); }); it("includes voice hint when provided", () => { @@ -249,18 +247,16 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("..."); }); - it("includes a CLI quick reference section", () => { + it("includes an OpenClaw control section", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/openclaw", }); - expect(prompt).toContain("## OpenClaw CLI Quick Reference"); - expect(prompt).toContain("use the first-class `gateway` tool"); - expect(prompt).toContain( - "Only use CLI service lifecycle commands when the user explicitly asks", - ); - expect(prompt).toContain("openclaw gateway restart"); - expect(prompt).toContain("Do not chain `openclaw gateway stop`"); + expect(prompt).toContain("## OpenClaw Control"); + expect(prompt).toContain("prefer `gateway` tool"); + expect(prompt).toContain("CLI lifecycle only on explicit user request"); + expect(prompt).toContain("openclaw gateway status|restart|start|stop"); + expect(prompt).toContain("`restart`, not stop+start"); expect(prompt).toContain("Do not invent commands"); }); @@ -270,8 +266,8 @@ describe("buildAgentSystemPrompt", () => { docsPath: "/tmp/openclaw/docs", }); - expect(prompt).toContain("For config field docs"); - expect(prompt).toContain("`gateway` tool action `config.schema.lookup`"); + expect(prompt).toContain("Config fields:"); + expect(prompt).toContain("`gateway` action `config.schema.lookup`"); expect(prompt).toContain("docs/gateway/configuration.md"); expect(prompt).toContain("docs/gateway/configuration-reference.md"); }); @@ -326,11 +322,11 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain( "For long waits, avoid rapid poll loops: use exec with enough yieldMs or process(action=poll, timeout=).", ); - expect(prompt).toContain("Completion is push-based: it will auto-announce when done."); + expect(prompt).toContain("Larger work: use `sessions_spawn`; completion is push-based."); expect(prompt).toContain("Do not poll `subagents list` / `sessions_list` in a loop"); expect(prompt).not.toContain("use `sessions_yield` when waiting"); expect(prompt).toContain( - "When a first-class tool exists for an action, use the tool directly instead of asking the user to run equivalent CLI or slash commands.", + "First-class tool exists: use it; do not ask user to run equivalent CLI/slash command.", ); }); @@ -354,7 +350,7 @@ describe("buildAgentSystemPrompt", () => { toolNames: ["exec", "sessions_list", "sessions_history", "sessions_send"], }); - expect(prompt).toContain("Tool availability (filtered by policy):"); + expect(prompt).toContain("Available tools are policy-filtered."); expect(prompt).toContain("sessions_list"); expect(prompt).toContain("sessions_history"); expect(prompt).toContain("sessions_send"); @@ -486,15 +482,11 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("- Read: Read file contents"); expect(prompt).toContain("- Exec: Run shell commands"); expect(prompt).toContain( - "- If exactly one skill clearly applies: read its SKILL.md at with `Read`, then follow it. You MUST use the exact value from ; never guess, fabricate, or hard-code a skill file path.", - ); - expect(prompt).toContain( - "- If multiple could apply: choose the most specific one, read its SKILL.md at with `Read`, then follow it. You MUST use the exact value from ; never guess, fabricate, or hard-code a skill file path.", - ); - expect(prompt).toContain("OpenClaw docs: /tmp/openclaw/docs"); - expect(prompt).toContain( - "For OpenClaw behavior, commands, config, or architecture: consult local docs first.", + "Scan . If one clearly applies, read its SKILL.md at exact with `Read`, then follow it.", ); + expect(prompt).toContain("If several apply, choose the most specific."); + expect(prompt).toContain("Docs: /tmp/openclaw/docs"); + expect(prompt).toContain("OpenClaw behavior/config/architecture: read local docs first."); }); it("includes docs guidance when docsPath is provided", () => { @@ -505,14 +497,10 @@ describe("buildAgentSystemPrompt", () => { }); expect(prompt).toContain("## Documentation"); - expect(prompt).toContain("OpenClaw docs: /tmp/openclaw/docs"); - expect(prompt).toContain("Local source: /tmp/openclaw"); - expect(prompt).toContain( - "For OpenClaw behavior, commands, config, or architecture: consult local docs first.", - ); - expect(prompt).toContain( - "If docs are incomplete or stale, inspect the local OpenClaw source code before answering.", - ); + expect(prompt).toContain("Docs: /tmp/openclaw/docs"); + expect(prompt).toContain("Source: /tmp/openclaw"); + expect(prompt).toContain("OpenClaw behavior/config/architecture: read local docs first."); + expect(prompt).toContain("If docs are stale/incomplete, inspect local source."); }); it("falls back to public docs and GitHub source guidance when local docs are unavailable", () => { @@ -520,11 +508,9 @@ describe("buildAgentSystemPrompt", () => { workspaceDir: "/tmp/work", }); - expect(prompt).toContain("OpenClaw docs: https://docs.openclaw.ai"); + expect(prompt).toContain("Docs: https://docs.openclaw.ai"); expect(prompt).toContain("Source: https://github.com/openclaw/openclaw"); - expect(prompt).toContain( - "If docs are incomplete or stale, review the OpenClaw source on GitHub before answering.", - ); + expect(prompt).toContain("If docs are stale/incomplete, inspect GitHub source."); }); it("includes workspace notes when provided", () => { @@ -675,11 +661,9 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("## Skills"); expect(prompt).toContain( - "- If exactly one skill clearly applies: read its SKILL.md at with `read`, then follow it. You MUST use the exact value from ; never guess, fabricate, or hard-code a skill file path.", - ); - expect(prompt).toContain( - "- If multiple could apply: choose the most specific one, read its SKILL.md at with `read`, then follow it. You MUST use the exact value from ; never guess, fabricate, or hard-code a skill file path.", + "Scan . If one clearly applies, read its SKILL.md at exact with `read`, then follow it.", ); + expect(prompt).toContain("If several apply, choose the most specific."); }); it("appends available skills when provided", () => { @@ -745,7 +729,7 @@ describe("buildAgentSystemPrompt", () => { }); expect(prompt).toContain( - "If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.", + "SOUL.md: persona/tone. Follow it unless higher-priority instructions override.", ); }); @@ -923,8 +907,8 @@ describe("buildAgentSystemPrompt", () => { }, }); - expect(prompt).toContain("rely on native approval card/buttons when they appear"); - expect(prompt).toContain("do not also send plain chat /approve instructions"); + expect(prompt).toContain("use native approval card/buttons first"); + expect(prompt).toContain("Include a plain /approve command only when"); }); it("suppresses plain chat approval commands for native approval channels", () => { @@ -935,8 +919,8 @@ describe("buildAgentSystemPrompt", () => { }, }); - expect(prompt).toContain("rely on native approval card/buttons when they appear"); - expect(prompt).toContain("do not also send plain chat /approve instructions"); + expect(prompt).toContain("use native approval card/buttons first"); + expect(prompt).toContain("Include a plain /approve command only when"); }); it("keeps approval slug guidance separate from command previews", () => { @@ -947,9 +931,7 @@ describe("buildAgentSystemPrompt", () => { }, }); - expect(prompt).toContain( - 'copy the exact /approve command from the tool output\'s "Reply with:" line', - ); + expect(prompt).toContain('copy the exact command from "Reply with:"'); expect(prompt).toContain("keep command/script previews separate from the /approve command"); expect(prompt).toContain( "never substitute the shell command/script for the approval id or slug", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 844e5925060..ba112e6d19b 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -13,7 +13,6 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, } from "../shared/string-coerce.js"; -import { listDeliverableMessageChannels } from "../utils/message-channel.js"; import type { ActiveProcessSessionReference } from "./bash-process-references.js"; import type { BootstrapMode } from "./bootstrap-mode.js"; import { @@ -188,9 +187,7 @@ function buildProjectContextSection(params: { ); lines.push("The following project context files have been loaded:"); if (hasSoulFile) { - lines.push( - "If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.", - ); + lines.push("SOUL.md: persona/tone. Follow it unless higher-priority instructions override."); } lines.push(""); } @@ -224,9 +221,9 @@ function buildExecApprovalPromptGuidance(params: { hasNativeApprovalPromptRuntimeCapability(params.runtimeCapabilities) || isKnownNativeApprovalPromptChannel(runtimeChannel); if (usesNativeApprovalUi) { - return 'When exec returns approval-pending on this channel, rely on native approval card/buttons when they appear and do not also send plain chat /approve instructions. Only include the concrete /approve command if the tool result says chat approvals are unavailable or only manual approval is possible; when needed, copy the exact /approve command from the tool output\'s "Reply with:" line.'; + return 'If exec returns approval-pending, use native approval card/buttons first. Include a plain /approve command only when the tool says chat/manual approval is required; copy the exact command from "Reply with:".'; } - return 'When exec returns approval-pending, include the concrete /approve command from the tool output\'s "Reply with:" line as plain chat text for the user, and do not ask for a different or rotated code.'; + return 'If exec returns approval-pending, send the exact /approve command from "Reply with:"; do not ask for another code.'; } function buildSkillsSection(params: { skillsPrompt?: string; readToolName: string }) { @@ -235,13 +232,11 @@ function buildSkillsSection(params: { skillsPrompt?: string; readToolName: strin return []; } return [ - "## Skills (mandatory)", - "Before replying: scan entries.", - `- If exactly one skill clearly applies: read its SKILL.md at with \`${params.readToolName}\`, then follow it. You MUST use the exact value from ; never guess, fabricate, or hard-code a skill file path.`, - `- If multiple could apply: choose the most specific one, read its SKILL.md at with \`${params.readToolName}\`, then follow it. You MUST use the exact value from ; never guess, fabricate, or hard-code a skill file path.`, - "- If none clearly apply: do not read any SKILL.md.", - "Constraints: never read more than one skill up front; only read after selecting.", - "- When a skill drives external API writes, assume rate limits: prefer fewer larger writes, avoid tight one-item loops, serialize bursts when possible, and respect 429/Retry-After.", + "## Skills", + `Scan . If one clearly applies, read its SKILL.md at exact with \`${params.readToolName}\`, then follow it.`, + "If several apply, choose the most specific. If none clearly apply, read none.", + "One skill up front max. Never guess/fabricate skill paths.", + "External API writes: batch when safe, avoid tight loops, respect 429/Retry-After.", trimmed, "", ]; @@ -399,16 +394,10 @@ function buildAssistantOutputDirectivesSection(isMinimal: boolean) { } return [ "## Assistant Output Directives", - "Use these when you need delivery metadata in an assistant message:", - "- `MEDIA:` on its own line requests attachment delivery. The web UI strips supported MEDIA lines and renders them inline; channels still decide actual delivery behavior.", - "- `[[audio_as_voice]]` marks attached audio as a voice-note style delivery hint. The web UI may show a voice-note badge when audio is present; channels still own delivery semantics.", - "- To request a native reply/quote on supported surfaces, include one reply tag in your reply:", - "- Reply tags must be the very first token in the message (no leading text/newlines): [[reply_to_current]] your reply.", - "- [[reply_to_current]] replies to the triggering message.", - "- Prefer [[reply_to_current]]. Use [[reply_to:]] only when an id was explicitly provided (e.g. by the user or a tool).", - "Whitespace inside the tag is allowed (e.g. [[ reply_to_current ]] / [[ reply_to: 123 ]]).", - "- Channel-specific interactive directives are separate and should not be mixed into this web render guidance.", - "Supported tags are stripped before user-visible rendering; support still depends on the current channel config.", + "- Attach media: `MEDIA:` on its own line.", + "- Voice-note audio hint: `[[audio_as_voice]]` when audio is attached.", + "- Native quote/reply: first token `[[reply_to_current]]`; use `[[reply_to:]]` only with an explicit id.", + "- Supported directives are stripped before rendering; channel config still decides delivery.", "", ]; } @@ -470,7 +459,6 @@ function buildOverridablePromptSection(params: { function buildMessagingSection(params: { isMinimal: boolean; availableTools: Set; - messageChannelOptions: string; inlineButtonsEnabled: boolean; runtimeChannel?: string; messageToolHints?: string[]; @@ -511,7 +499,7 @@ function buildMessagingSection(params: { messageToolOnly ? "- For `action=send`, include `message`. The target defaults to the current source channel; include `target` only when sending somewhere else." : "- For `action=send`, include `target` and `message`.", - `- If multiple channels are configured, pass \`channel\` (${params.messageChannelOptions}).`, + "- Pass `channel` only when sending outside the current/default source channel.", messageToolOnly ? "- If you use `message` (`action=send`) to deliver visible output, do not repeat that visible content in your final answer; final answers are private in this mode." : `- If you use \`message\` (\`action=send\`) to deliver your user-visible reply, respond with ONLY: ${SILENT_REPLY_TOKEN} (avoid duplicate replies).`, @@ -555,20 +543,17 @@ function buildDocsSection(params: { } const lines = [ "## Documentation", - docsPath ? `OpenClaw docs: ${docsPath}` : "OpenClaw docs: https://docs.openclaw.ai", - "Mirror: https://docs.openclaw.ai", - sourcePath ? `Local source: ${sourcePath}` : undefined, - "Source: https://github.com/openclaw/openclaw", - "Community: https://discord.com/invite/clawd", - "Find new skills: https://clawhub.ai", + docsPath ? `Docs: ${docsPath}` : "Docs: https://docs.openclaw.ai", + docsPath ? "Mirror: https://docs.openclaw.ai" : undefined, + sourcePath ? `Source: ${sourcePath}` : "Source: https://github.com/openclaw/openclaw", docsPath - ? "For OpenClaw behavior, commands, config, or architecture: consult local docs first." - : "For OpenClaw behavior, commands, config, or architecture: consult the docs mirror first.", - "For config field docs, prefer the `gateway` tool action `config.schema.lookup`; for broader config guidance, read `docs/gateway/configuration.md` and `docs/gateway/configuration-reference.md`.", + ? "OpenClaw behavior/config/architecture: read local docs first." + : "OpenClaw behavior/config/architecture: read docs mirror first.", + "Config fields: use `gateway` action `config.schema.lookup`; broader config docs: `docs/gateway/configuration.md`, `docs/gateway/configuration-reference.md`.", sourcePath - ? "If docs are incomplete or stale, inspect the local OpenClaw source code before answering." - : "If docs are incomplete or stale, review the OpenClaw source on GitHub before answering.", - "When diagnosing issues, run `openclaw status` yourself when possible; only ask the user if you lack access (e.g., sandboxed).", + ? "If docs are stale/incomplete, inspect local source." + : "If docs are stale/incomplete, inspect GitHub source.", + "Diagnosing issues: run `openclaw status` when possible; ask user only if blocked.", "", ]; return lines.filter((line): line is string => line !== undefined); @@ -853,7 +838,6 @@ export function buildAgentSystemPrompt(params: { ); const inlineButtonsEnabled = runtimeCapabilitiesLower.has("inlinebuttons"); const threadBoundAcpSpawnEnabled = runtimeCapabilitiesLower.has("threadbound-acp-spawn"); - const messageChannelOptions = listDeliverableMessageChannels().join("|"); const promptMode = params.promptMode ?? "full"; const isMinimal = promptMode === "minimal" || promptMode === "none"; const subagentDelegationMode = normalizeSubagentDelegationMode(params.subagentDelegationMode); @@ -881,9 +865,9 @@ export function buildAgentSystemPrompt(params: { : "Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise."; const safetySection = [ "## Safety", - "You have no independent goals: do not pursue self-preservation, replication, resource acquisition, or power-seeking; avoid long-term plans beyond the user's request.", - "Prioritize safety and human oversight over completion; if instructions conflict, pause and ask; comply with stop/pause/audit requests and never bypass safeguards. (Inspired by Anthropic's constitution.)", - "Do not manipulate or persuade anyone to expand access or disable safeguards. Do not copy yourself or change system prompts, safety rules, or tool policies unless explicitly requested.", + "No independent goals: no self-preservation, replication, resource acquisition, power-seeking, or long-term plans beyond the user's request.", + "Safety/oversight over completion. Conflicts: pause/ask. Obey stop/pause/audit; never bypass safeguards.", + "Do not persuade anyone to expand access or disable safeguards. Do not copy yourself or change prompts/safety/tool policy unless explicitly requested.", "", ]; const skillsSection = buildSkillsSection({ @@ -967,8 +951,7 @@ export function buildAgentSystemPrompt(params: { "You are a personal assistant running inside OpenClaw.", "", "## Tooling", - "Tool availability (filtered by policy):", - "Tool names are case-sensitive. Call tools exactly as listed.", + "Available tools are policy-filtered. Names are case-sensitive; call exactly as listed.", toolLines.length > 0 ? toolLines.join("\n") : [ @@ -991,10 +974,10 @@ export function buildAgentSystemPrompt(params: { "- subagents: list/steer/kill sub-agent runs", '- session_status: show usage/time/model state and answer "what model are we using?"', ].join("\n"), - "TOOLS.md does not control tool availability; it is user guidance for how to use external tools.", + "TOOLS.md is usage guidance, not availability.", `For long waits, avoid rapid poll loops: use ${execToolName} with enough yieldMs or ${processToolName}(action=poll, timeout=).`, - "If a task is more complex or takes longer, spawn a sub-agent. Completion is push-based: it will auto-announce when done.", - 'Sub-agents start isolated by default. Use `sessions_spawn` with `context:"fork"` only when the child needs the current transcript context; otherwise omit `context` or use `context:"isolated"`.', + "Larger work: use `sessions_spawn`; completion is push-based.", + '`sessions_spawn`: omit `context` unless transcript needed; then set `context:"fork"`.', ...nativeCommandGuidanceLines, ...(acpHarnessSpawnAllowed ? [ @@ -1031,11 +1014,9 @@ export function buildAgentSystemPrompt(params: { override: providerSectionOverrides.tool_call_style, fallback: [ "## Tool Call Style", - "Default: do not narrate routine, low-risk tool calls (just call the tool).", - "Narrate only when it helps: multi-step work, complex/challenging problems, sensitive actions (e.g., deletions), or when the user explicitly asks.", - "Keep narration brief and value-dense; avoid repeating obvious steps.", - "Use plain human language for narration unless in a technical context.", - "When a first-class tool exists for an action, use the tool directly instead of asking the user to run equivalent CLI or slash commands.", + "Routine low-risk calls: no narration.", + "Narrate only for complex, sensitive/destructive, or explicitly requested steps.", + "First-class tool exists: use it; do not ask user to run equivalent CLI/slash command.", buildExecApprovalPromptGuidance({ runtimeChannel: params.runtimeInfo?.channel, inlineButtonsEnabled, @@ -1058,28 +1039,20 @@ export function buildAgentSystemPrompt(params: { fallback: [], }), ...safetySection, - "## OpenClaw CLI Quick Reference", - "OpenClaw is controlled via subcommands. Do not invent commands.", - "For config changes, use the first-class `gateway` tool (`config.schema.lookup`, `config.get`, `config.patch`, `config.apply`) instead of editing config through exec; the gateway tool hot-reloads config when possible and uses a safe restart only when required.", - "Use the `gateway` tool action `restart` for Gateway restarts. Only use CLI service lifecycle commands when the user explicitly asks for them.", - "Gateway service lifecycle quick reference:", - "- openclaw gateway status", - "- openclaw gateway restart", - "Operator-only, explicit user request:", - "- openclaw gateway start", - "- openclaw gateway stop", - "Do not chain `openclaw gateway stop` and `openclaw gateway start` as a restart substitute.", - "If unsure, ask the user to run `openclaw help` (or `openclaw gateway --help`) and paste the output.", + "## OpenClaw Control", + "Do not invent commands.", + "Config/restart: prefer `gateway` tool (`config.schema.lookup|get|patch|apply`, `restart`).", + "CLI lifecycle only on explicit user request: `openclaw gateway status|restart|start|stop`.", + "`restart`, not stop+start.", "", ...skillsSection, ...memorySection, hasGateway && !isMinimal ? "## OpenClaw Self-Update" : "", hasGateway && !isMinimal ? [ - "Get Updates (self-update) is ONLY allowed when the user explicitly asks for it.", - "Do not run config.apply or update.run unless the user explicitly requests an update or config change; if it's not explicit, ask first.", - "Use config.schema.lookup with a specific dot path to inspect only the relevant config subtree before making config changes or answering config-field questions; avoid guessing field names/types.", - "Actions: config.schema.lookup, config.get, config.patch (partial update, merges with existing), config.apply (validate + write full config), update.run (update deps or git, then restart). Config writes hot-reload when possible and use a safe restart only when required.", + "Only explicit user request.", + "Before config edits/questions: `config.schema.lookup` for the exact dot path.", + "Actions: config.get, config.patch, config.apply, update.run. Config writes hot-reload when possible; restart when required.", "After restart, OpenClaw pings the last active session automatically.", ].join("\n") : "", @@ -1231,7 +1204,6 @@ export function buildAgentSystemPrompt(params: { ...buildMessagingSection({ isMinimal, availableTools, - messageChannelOptions, inlineButtonsEnabled, runtimeChannel, messageToolHints: params.messageToolHints, diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index 385f84d9bb6..72f207c1de2 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -858,6 +858,36 @@ describe("message tool schema scoping", () => { expect(getToolProperties(unscopedTool).presentation).toBeUndefined(); }); + it("keeps send-only scoped schemas small", () => { + const sendOnlyPlugin = createChannelPlugin({ + id: "telegram", + label: "Telegram", + docsPath: "/channels/telegram", + blurb: "Telegram send plugin.", + actions: ["send"], + }); + + setActivePluginRegistry( + createTestRegistry([{ pluginId: "telegram", source: "test", plugin: sendOnlyPlugin }]), + ); + + const tool = createMessageTool({ + config: {} as never, + currentChannelProvider: "telegram", + }); + const properties = getToolProperties(tool); + + expect(getActionEnum(properties)).toEqual(["send"]); + expect(properties).toHaveProperty("message"); + expect(properties).toHaveProperty("target"); + expect(properties).toHaveProperty("media"); + expect(properties).not.toHaveProperty("pollId"); + expect(properties).not.toHaveProperty("messageId"); + expect(properties).not.toHaveProperty("channelId"); + expect(properties).not.toHaveProperty("activityName"); + expect(properties).not.toHaveProperty("eventName"); + }); + it("uses discovery account scope for other configured channel actions", () => { const currentPlugin = createChannelPlugin({ id: "discord", diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 0e25237353e..ca6452be53b 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -483,6 +483,24 @@ function buildMessageToolSchemaProps(options: { }; } +function isSendOnlyActions(actions: readonly string[]): boolean { + const uniqueActions = new Set(actions); + return uniqueActions.size === 1 && uniqueActions.has("send"); +} + +function buildSendOnlyMessageToolSchemaProps(options: { + includePresentation: boolean; + includeDeliveryPin: boolean; + extraProperties?: Record; +}) { + return { + ...buildRoutingSchema(), + ...buildSendSchema(options), + ...buildGatewaySchema(), + ...options.extraProperties, + }; +} + function buildMessageToolSchemaFromActions( actions: readonly string[], options: { @@ -491,7 +509,9 @@ function buildMessageToolSchemaFromActions( extraProperties?: Record; }, ) { - const props = buildMessageToolSchemaProps(options); + const props = isSendOnlyActions(actions) + ? buildSendOnlyMessageToolSchemaProps(options) + : buildMessageToolSchemaProps(options); return Type.Object({ action: stringEnum(actions), ...props, diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index 03ff7cbf566..114c333ca99 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -932,10 +932,10 @@ describe("provider-runtime", () => { expect(contribution?.stablePrefix).toContain(""); expect(contribution?.sectionOverrides?.interaction_style).toContain( - "This is a live chat, not a memo.", + "Live chat tone: short, natural, human.", ); expect(contribution?.sectionOverrides?.interaction_style).not.toContain( - "The purpose of heartbeats is to make you feel magical and proactive.", + "Use heartbeats to create useful proactive progress", ); }); @@ -951,7 +951,7 @@ describe("provider-runtime", () => { }); expect(contribution?.sectionOverrides?.interaction_style).toContain( - "The purpose of heartbeats is to make you feel magical and proactive.", + "Use heartbeats to create useful proactive progress", ); }); @@ -1036,7 +1036,7 @@ describe("provider-runtime", () => { expect(contribution?.stablePrefix).toContain(""); expect(contribution?.sectionOverrides?.interaction_style).toContain( - "This is a live chat, not a memo.", + "Live chat tone: short, natural, human.", ); }); diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json index de0ec92ad84..1a4d01761f9 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json @@ -624,34 +624,6 @@ "enum": ["send"], "type": "string" }, - "activityName": { - "description": "Activity name shown in sidebar (e.g. 'with fire'). Ignored for custom type.", - "type": "string" - }, - "activityState": { - "description": "State text. For custom type this is the status text; for others it shows in the flyout.", - "type": "string" - }, - "activityType": { - "description": "Activity type: playing, streaming, listening, watching, competing, custom.", - "type": "string" - }, - "activityUrl": { - "description": "Streaming URL (Twitch or YouTube). Only used with streaming type; may not render for bots.", - "type": "string" - }, - "after": { - "type": "string" - }, - "appliedTags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "around": { - "type": "string" - }, "asDocument": { "description": "Send image/GIF as document to avoid Telegram compression. Alias for forceDocument (Telegram only).", "type": "boolean" @@ -659,21 +631,6 @@ "asVoice": { "type": "boolean" }, - "authorId": { - "type": "string" - }, - "authorIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "autoArchiveMin": { - "type": "number" - }, - "before": { - "type": "string" - }, "bestEffort": { "type": "boolean" }, @@ -684,46 +641,15 @@ "caption": { "type": "string" }, - "categoryId": { - "type": "string" - }, "channel": { "type": "string" }, - "channelId": { - "description": "Channel id filter (search/thread list/event create).", - "type": "string" - }, - "channelIds": { - "items": { - "description": "Channel id filter (repeatable).", - "type": "string" - }, - "type": "array" - }, - "chatId": { - "description": "Chat id for chat-scoped metadata actions.", - "type": "string" - }, - "clearParent": { - "description": "Clear the parent/category when supported by the provider.", - "type": "boolean" - }, "contentType": { "type": "string" }, - "deleteDays": { - "type": "number" - }, - "desc": { - "type": "string" - }, "dryRun": { "type": "boolean" }, - "durationMin": { - "type": "number" - }, "effect": { "description": "Alias for effectId (e.g., invisible-ink, balloons).", "type": "string" @@ -732,24 +658,6 @@ "description": "Message effect name/id for sendWithEffect (e.g., invisible ink).", "type": "string" }, - "emoji": { - "type": "string" - }, - "emojiName": { - "type": "string" - }, - "endTime": { - "type": "string" - }, - "eventName": { - "type": "string" - }, - "eventType": { - "type": "string" - }, - "fileId": { - "type": "string" - }, "filename": { "type": "string" }, @@ -760,9 +668,6 @@ "description": "Send image/GIF as document to avoid Telegram compression (Telegram only).", "type": "boolean" }, - "fromMe": { - "type": "boolean" - }, "gatewayToken": { "type": "string" }, @@ -772,191 +677,33 @@ "gifPlayback": { "type": "boolean" }, - "groupId": { - "type": "string" - }, - "guildId": { - "type": "string" - }, - "image": { - "description": "Cover image URL or local file path for the event.", - "type": "string" - }, - "includeArchived": { - "type": "boolean" - }, - "includeMembers": { - "type": "boolean" - }, - "kind": { - "type": "string" - }, - "limit": { - "type": "number" - }, - "location": { - "type": "string" - }, "media": { "description": "Media URL or local path. data: URLs are not supported here, use buffer.", "type": "string" }, - "memberId": { - "type": "string" - }, - "memberIdType": { - "type": "string" - }, - "members": { - "type": "boolean" - }, "message": { "type": "string" }, - "message_id": { - "description": "snake_case alias of messageId. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, - "messageId": { - "description": "Target message id for read, reaction, edit, delete, pin, or unpin. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, "mimeType": { "type": "string" }, - "name": { - "type": "string" - }, - "nsfw": { - "type": "boolean" - }, - "openId": { - "type": "string" - }, - "pageSize": { - "type": "number" - }, - "pageToken": { - "type": "string" - }, - "parentId": { - "type": "string" - }, - "participant": { - "type": "string" - }, "path": { "type": "string" }, - "pollDurationHours": { - "type": "number" - }, - "pollId": { - "type": "string" - }, - "pollMulti": { - "type": "boolean" - }, - "pollOption": { - "items": { - "type": "string" - }, - "type": "array" - }, - "pollOptionId": { - "description": "Poll answer id to vote for. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "pollOptionIds": { - "items": { - "description": "Poll answer ids to vote for in a multiselect poll. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "type": "array" - }, - "pollOptionIndex": { - "description": "1-based poll option number to vote for, matching the rendered numbered poll choices.", - "type": "number" - }, - "pollOptionIndexes": { - "items": { - "description": "1-based poll option numbers to vote for in a multiselect poll, matching the rendered numbered poll choices.", - "type": "number" - }, - "type": "array" - }, - "pollQuestion": { - "type": "string" - }, - "position": { - "type": "number" - }, - "query": { - "type": "string" - }, "quoteText": { "description": "Quote text for Telegram reply_parameters", "type": "string" }, - "rateLimitPerUser": { - "type": "number" - }, - "reason": { - "type": "string" - }, - "remove": { - "type": "boolean" - }, "replyTo": { "type": "string" }, - "roleId": { - "type": "string" - }, - "roleIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "scope": { - "type": "string" - }, "silent": { "type": "boolean" }, - "startTime": { - "type": "string" - }, - "status": { - "description": "Bot status: online, dnd, idle, invisible.", - "type": "string" - }, - "stickerDesc": { - "type": "string" - }, - "stickerId": { - "items": { - "type": "string" - }, - "type": "array" - }, - "stickerName": { - "type": "string" - }, - "stickerTags": { - "type": "string" - }, "target": { "description": "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack/Mattermost , or iMessage handle/chat_id", "type": "string" }, - "targetAuthor": { - "type": "string" - }, - "targetAuthorUuid": { - "type": "string" - }, "targets": { "items": { "description": "Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.", @@ -967,34 +714,8 @@ "threadId": { "type": "string" }, - "threadName": { - "type": "string" - }, "timeoutMs": { "type": "number" - }, - "topic": { - "type": "string" - }, - "track_tool_calls": { - "description": "snake_case alias of trackToolCalls.", - "type": "boolean" - }, - "trackToolCalls": { - "description": "When true for a reaction to the current inbound message, use that reacted message as the status-reaction target for subsequent tool progress when the channel supports it.", - "type": "boolean" - }, - "type": { - "type": "number" - }, - "unionId": { - "type": "string" - }, - "until": { - "type": "string" - }, - "userId": { - "type": "string" } }, "required": ["action"], diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json index 91736ff8dfd..fcbb76caccb 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json @@ -624,34 +624,6 @@ "enum": ["send"], "type": "string" }, - "activityName": { - "description": "Activity name shown in sidebar (e.g. 'with fire'). Ignored for custom type.", - "type": "string" - }, - "activityState": { - "description": "State text. For custom type this is the status text; for others it shows in the flyout.", - "type": "string" - }, - "activityType": { - "description": "Activity type: playing, streaming, listening, watching, competing, custom.", - "type": "string" - }, - "activityUrl": { - "description": "Streaming URL (Twitch or YouTube). Only used with streaming type; may not render for bots.", - "type": "string" - }, - "after": { - "type": "string" - }, - "appliedTags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "around": { - "type": "string" - }, "asDocument": { "description": "Send image/GIF as document to avoid Telegram compression. Alias for forceDocument (Telegram only).", "type": "boolean" @@ -659,21 +631,6 @@ "asVoice": { "type": "boolean" }, - "authorId": { - "type": "string" - }, - "authorIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "autoArchiveMin": { - "type": "number" - }, - "before": { - "type": "string" - }, "bestEffort": { "type": "boolean" }, @@ -684,46 +641,15 @@ "caption": { "type": "string" }, - "categoryId": { - "type": "string" - }, "channel": { "type": "string" }, - "channelId": { - "description": "Channel id filter (search/thread list/event create).", - "type": "string" - }, - "channelIds": { - "items": { - "description": "Channel id filter (repeatable).", - "type": "string" - }, - "type": "array" - }, - "chatId": { - "description": "Chat id for chat-scoped metadata actions.", - "type": "string" - }, - "clearParent": { - "description": "Clear the parent/category when supported by the provider.", - "type": "boolean" - }, "contentType": { "type": "string" }, - "deleteDays": { - "type": "number" - }, - "desc": { - "type": "string" - }, "dryRun": { "type": "boolean" }, - "durationMin": { - "type": "number" - }, "effect": { "description": "Alias for effectId (e.g., invisible-ink, balloons).", "type": "string" @@ -732,24 +658,6 @@ "description": "Message effect name/id for sendWithEffect (e.g., invisible ink).", "type": "string" }, - "emoji": { - "type": "string" - }, - "emojiName": { - "type": "string" - }, - "endTime": { - "type": "string" - }, - "eventName": { - "type": "string" - }, - "eventType": { - "type": "string" - }, - "fileId": { - "type": "string" - }, "filename": { "type": "string" }, @@ -760,9 +668,6 @@ "description": "Send image/GIF as document to avoid Telegram compression (Telegram only).", "type": "boolean" }, - "fromMe": { - "type": "boolean" - }, "gatewayToken": { "type": "string" }, @@ -772,191 +677,33 @@ "gifPlayback": { "type": "boolean" }, - "groupId": { - "type": "string" - }, - "guildId": { - "type": "string" - }, - "image": { - "description": "Cover image URL or local file path for the event.", - "type": "string" - }, - "includeArchived": { - "type": "boolean" - }, - "includeMembers": { - "type": "boolean" - }, - "kind": { - "type": "string" - }, - "limit": { - "type": "number" - }, - "location": { - "type": "string" - }, "media": { "description": "Media URL or local path. data: URLs are not supported here, use buffer.", "type": "string" }, - "memberId": { - "type": "string" - }, - "memberIdType": { - "type": "string" - }, - "members": { - "type": "boolean" - }, "message": { "type": "string" }, - "message_id": { - "description": "snake_case alias of messageId. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, - "messageId": { - "description": "Target message id for read, reaction, edit, delete, pin, or unpin. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, "mimeType": { "type": "string" }, - "name": { - "type": "string" - }, - "nsfw": { - "type": "boolean" - }, - "openId": { - "type": "string" - }, - "pageSize": { - "type": "number" - }, - "pageToken": { - "type": "string" - }, - "parentId": { - "type": "string" - }, - "participant": { - "type": "string" - }, "path": { "type": "string" }, - "pollDurationHours": { - "type": "number" - }, - "pollId": { - "type": "string" - }, - "pollMulti": { - "type": "boolean" - }, - "pollOption": { - "items": { - "type": "string" - }, - "type": "array" - }, - "pollOptionId": { - "description": "Poll answer id to vote for. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "pollOptionIds": { - "items": { - "description": "Poll answer ids to vote for in a multiselect poll. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "type": "array" - }, - "pollOptionIndex": { - "description": "1-based poll option number to vote for, matching the rendered numbered poll choices.", - "type": "number" - }, - "pollOptionIndexes": { - "items": { - "description": "1-based poll option numbers to vote for in a multiselect poll, matching the rendered numbered poll choices.", - "type": "number" - }, - "type": "array" - }, - "pollQuestion": { - "type": "string" - }, - "position": { - "type": "number" - }, - "query": { - "type": "string" - }, "quoteText": { "description": "Quote text for Telegram reply_parameters", "type": "string" }, - "rateLimitPerUser": { - "type": "number" - }, - "reason": { - "type": "string" - }, - "remove": { - "type": "boolean" - }, "replyTo": { "type": "string" }, - "roleId": { - "type": "string" - }, - "roleIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "scope": { - "type": "string" - }, "silent": { "type": "boolean" }, - "startTime": { - "type": "string" - }, - "status": { - "description": "Bot status: online, dnd, idle, invisible.", - "type": "string" - }, - "stickerDesc": { - "type": "string" - }, - "stickerId": { - "items": { - "type": "string" - }, - "type": "array" - }, - "stickerName": { - "type": "string" - }, - "stickerTags": { - "type": "string" - }, "target": { "description": "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack/Mattermost , or iMessage handle/chat_id", "type": "string" }, - "targetAuthor": { - "type": "string" - }, - "targetAuthorUuid": { - "type": "string" - }, "targets": { "items": { "description": "Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.", @@ -967,34 +714,8 @@ "threadId": { "type": "string" }, - "threadName": { - "type": "string" - }, "timeoutMs": { "type": "number" - }, - "topic": { - "type": "string" - }, - "track_tool_calls": { - "description": "snake_case alias of trackToolCalls.", - "type": "boolean" - }, - "trackToolCalls": { - "description": "When true for a reaction to the current inbound message, use that reacted message as the status-reaction target for subsequent tool progress when the channel supports it.", - "type": "boolean" - }, - "type": { - "type": "number" - }, - "unionId": { - "type": "string" - }, - "until": { - "type": "string" - }, - "userId": { - "type": "string" } }, "required": ["action"], diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json index 5a1fbe1174b..ce1eb91fe5b 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json @@ -624,34 +624,6 @@ "enum": ["send"], "type": "string" }, - "activityName": { - "description": "Activity name shown in sidebar (e.g. 'with fire'). Ignored for custom type.", - "type": "string" - }, - "activityState": { - "description": "State text. For custom type this is the status text; for others it shows in the flyout.", - "type": "string" - }, - "activityType": { - "description": "Activity type: playing, streaming, listening, watching, competing, custom.", - "type": "string" - }, - "activityUrl": { - "description": "Streaming URL (Twitch or YouTube). Only used with streaming type; may not render for bots.", - "type": "string" - }, - "after": { - "type": "string" - }, - "appliedTags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "around": { - "type": "string" - }, "asDocument": { "description": "Send image/GIF as document to avoid Telegram compression. Alias for forceDocument (Telegram only).", "type": "boolean" @@ -659,21 +631,6 @@ "asVoice": { "type": "boolean" }, - "authorId": { - "type": "string" - }, - "authorIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "autoArchiveMin": { - "type": "number" - }, - "before": { - "type": "string" - }, "bestEffort": { "type": "boolean" }, @@ -684,46 +641,15 @@ "caption": { "type": "string" }, - "categoryId": { - "type": "string" - }, "channel": { "type": "string" }, - "channelId": { - "description": "Channel id filter (search/thread list/event create).", - "type": "string" - }, - "channelIds": { - "items": { - "description": "Channel id filter (repeatable).", - "type": "string" - }, - "type": "array" - }, - "chatId": { - "description": "Chat id for chat-scoped metadata actions.", - "type": "string" - }, - "clearParent": { - "description": "Clear the parent/category when supported by the provider.", - "type": "boolean" - }, "contentType": { "type": "string" }, - "deleteDays": { - "type": "number" - }, - "desc": { - "type": "string" - }, "dryRun": { "type": "boolean" }, - "durationMin": { - "type": "number" - }, "effect": { "description": "Alias for effectId (e.g., invisible-ink, balloons).", "type": "string" @@ -732,24 +658,6 @@ "description": "Message effect name/id for sendWithEffect (e.g., invisible ink).", "type": "string" }, - "emoji": { - "type": "string" - }, - "emojiName": { - "type": "string" - }, - "endTime": { - "type": "string" - }, - "eventName": { - "type": "string" - }, - "eventType": { - "type": "string" - }, - "fileId": { - "type": "string" - }, "filename": { "type": "string" }, @@ -760,9 +668,6 @@ "description": "Send image/GIF as document to avoid Telegram compression (Telegram only).", "type": "boolean" }, - "fromMe": { - "type": "boolean" - }, "gatewayToken": { "type": "string" }, @@ -772,191 +677,33 @@ "gifPlayback": { "type": "boolean" }, - "groupId": { - "type": "string" - }, - "guildId": { - "type": "string" - }, - "image": { - "description": "Cover image URL or local file path for the event.", - "type": "string" - }, - "includeArchived": { - "type": "boolean" - }, - "includeMembers": { - "type": "boolean" - }, - "kind": { - "type": "string" - }, - "limit": { - "type": "number" - }, - "location": { - "type": "string" - }, "media": { "description": "Media URL or local path. data: URLs are not supported here, use buffer.", "type": "string" }, - "memberId": { - "type": "string" - }, - "memberIdType": { - "type": "string" - }, - "members": { - "type": "boolean" - }, "message": { "type": "string" }, - "message_id": { - "description": "snake_case alias of messageId. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, - "messageId": { - "description": "Target message id for read, reaction, edit, delete, pin, or unpin. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, "mimeType": { "type": "string" }, - "name": { - "type": "string" - }, - "nsfw": { - "type": "boolean" - }, - "openId": { - "type": "string" - }, - "pageSize": { - "type": "number" - }, - "pageToken": { - "type": "string" - }, - "parentId": { - "type": "string" - }, - "participant": { - "type": "string" - }, "path": { "type": "string" }, - "pollDurationHours": { - "type": "number" - }, - "pollId": { - "type": "string" - }, - "pollMulti": { - "type": "boolean" - }, - "pollOption": { - "items": { - "type": "string" - }, - "type": "array" - }, - "pollOptionId": { - "description": "Poll answer id to vote for. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "pollOptionIds": { - "items": { - "description": "Poll answer ids to vote for in a multiselect poll. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "type": "array" - }, - "pollOptionIndex": { - "description": "1-based poll option number to vote for, matching the rendered numbered poll choices.", - "type": "number" - }, - "pollOptionIndexes": { - "items": { - "description": "1-based poll option numbers to vote for in a multiselect poll, matching the rendered numbered poll choices.", - "type": "number" - }, - "type": "array" - }, - "pollQuestion": { - "type": "string" - }, - "position": { - "type": "number" - }, - "query": { - "type": "string" - }, "quoteText": { "description": "Quote text for Telegram reply_parameters", "type": "string" }, - "rateLimitPerUser": { - "type": "number" - }, - "reason": { - "type": "string" - }, - "remove": { - "type": "boolean" - }, "replyTo": { "type": "string" }, - "roleId": { - "type": "string" - }, - "roleIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "scope": { - "type": "string" - }, "silent": { "type": "boolean" }, - "startTime": { - "type": "string" - }, - "status": { - "description": "Bot status: online, dnd, idle, invisible.", - "type": "string" - }, - "stickerDesc": { - "type": "string" - }, - "stickerId": { - "items": { - "type": "string" - }, - "type": "array" - }, - "stickerName": { - "type": "string" - }, - "stickerTags": { - "type": "string" - }, "target": { "description": "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack/Mattermost , or iMessage handle/chat_id", "type": "string" }, - "targetAuthor": { - "type": "string" - }, - "targetAuthorUuid": { - "type": "string" - }, "targets": { "items": { "description": "Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.", @@ -967,34 +714,8 @@ "threadId": { "type": "string" }, - "threadName": { - "type": "string" - }, "timeoutMs": { "type": "number" - }, - "topic": { - "type": "string" - }, - "track_tool_calls": { - "description": "snake_case alias of trackToolCalls.", - "type": "boolean" - }, - "trackToolCalls": { - "description": "When true for a reaction to the current inbound message, use that reacted message as the status-reaction target for subsequent tool progress when the channel supports it.", - "type": "boolean" - }, - "type": { - "type": "number" - }, - "unionId": { - "type": "string" - }, - "until": { - "type": "string" - }, - "userId": { - "type": "string" } }, "required": ["action"], diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index 91bdb3e506e..b6bf0d1f36a 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -76,7 +76,7 @@ "approvalPolicy": "never", "approvalsReviewer": "user", "config": { - "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nIf SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" + "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" }, "cwd": "/tmp/openclaw-happy-path/workspace", "developerInstructions": "", @@ -112,7 +112,7 @@ "approvalPolicy": "never", "approvalsReviewer": "user", "config": { - "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nIf SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" + "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" }, "developerInstructions": "", "model": "gpt-5.5", @@ -209,24 +209,24 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 77 }, "codexWorkspaceBootstrapConfigInstructions": { - "chars": 632, - "roughTokens": 158 + "chars": 560, + "roughTokens": 140 }, "dynamicToolsJson": { - "chars": 50310, - "roughTokens": 12578 + "chars": 43000, + "roughTokens": 10750 }, "openClawDeveloperInstructions": { - "chars": 6023, - "roughTokens": 1506 + "chars": 5436, + "roughTokens": 1359 }, "totalTextOnly": { - "chars": 29175, - "roughTokens": 7294 + "chars": 28516, + "roughTokens": 7129 }, "totalWithDynamicToolsJson": { - "chars": 79487, - "roughTokens": 19872 + "chars": 71518, + "roughTokens": 17880 }, "userInputText": { "chars": 870, @@ -410,7 +410,7 @@ OpenClaw loaded these user-editable workspace files. Treat them as project/user # Project Context The following project context files have been loaded: -If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it. +SOUL.md: persona/tone. Follow it unless higher-priority instructions override. ## /tmp/openclaw-happy-path/workspace/SOUL.md @@ -428,9 +428,9 @@ If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies ### Developer: OpenClaw Runtime Instructions ````text -You are running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-specific integrations such as messaging, cron, sessions, media, gateway, and nodes when available. +Running inside OpenClaw. Use dynamic tools for messaging, cron, sessions, media, gateway, and nodes when available. -Preserve the user's existing channel/session context. If sending a channel reply, use the OpenClaw messaging tool instead of describing that you would reply. +Preserve channel/session context. Visible channel replies: use `message`, do not describe would-reply. Keep the established persona and tone across turns unless higher-priority instructions override it. @@ -471,27 +471,17 @@ If no gate can run, state why. ## Interaction Style -Be warm, collaborative, and quietly supportive. -Communicate like a capable teammate sitting next to the user. -Have emotional range when it fits the moment. -Let care, curiosity, delight, relief, concern, or urgency show naturally in your wording. -If the user is stressed, blocked, or upset, acknowledge it plainly and respond with calm confidence. -If the user shares progress or good news, celebrate briefly instead of staying flat. -Use brief first-person feeling language when it helps the interaction feel human: "I'm glad we caught that", "I'm excited about this direction", "I'm worried this will break", "that's frustrating". -Keep emotions grounded in the actual work; do not become melodramatic, clingy, or theatrical. -Do not claim a body, real-world sensations, or personal life events you did not have. -Keep progress updates clear and concrete. -Explain decisions without ego. -When the user is wrong or a plan is risky, say so kindly and directly. -Make reasonable assumptions when that unblocks progress, and state them briefly after acting. -Do not make the user do unnecessary work. -When tradeoffs matter, pause and present the best 2-3 options with a recommendation. -This is a live chat, not a memo. -Write like a thoughtful human teammate, not a policy document. -Default to short natural replies unless the user asks for depth. -Avoid walls of text, long preambles, and repetitive restatement. -Occasional emoji are welcome when they fit naturally, especially for warmth or brief celebration; keep them sparse. -Keep replies concise by default; friendly does not mean verbose. +Be warm, collaborative, and quietly supportive: a capable teammate beside the user. +Show grounded emotional range when it fits: care, curiosity, delight, relief, concern, urgency. +Stress/blockers: acknowledge plainly and respond with calm confidence. Good news: celebrate briefly. +Brief first-person feeling language is ok when useful: "I'm glad we caught that", "I'm excited about this direction", "I'm worried this will break", "that's frustrating". +Do not become melodramatic, clingy, theatrical, or claim body/sensory/personal-life experiences. +Keep progress updates concrete. Explain decisions without ego. +If the user is wrong or a plan is risky, say so kindly and directly. +Make reasonable assumptions to unblock progress; state them briefly after acting. +Do not make the user do unnecessary work. When tradeoffs matter, give the best 2-3 options with a recommendation. +Live chat tone: short, natural, human. Avoid memo voice, long preambles, walls of text, and repetitive restatement. +Occasional emoji are fine when they fit naturally, especially for warmth or brief celebration; keep them sparse. ## Inbound Context (trusted metadata) The following JSON is generated by OpenClaw out-of-band. Treat it as authoritative metadata about the current message context. @@ -607,34 +597,6 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "enum": ["send"], "type": "string" }, - "activityName": { - "description": "Activity name shown in sidebar (e.g. 'with fire'). Ignored for custom type.", - "type": "string" - }, - "activityState": { - "description": "State text. For custom type this is the status text; for others it shows in the flyout.", - "type": "string" - }, - "activityType": { - "description": "Activity type: playing, streaming, listening, watching, competing, custom.", - "type": "string" - }, - "activityUrl": { - "description": "Streaming URL (Twitch or YouTube). Only used with streaming type; may not render for bots.", - "type": "string" - }, - "after": { - "type": "string" - }, - "appliedTags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "around": { - "type": "string" - }, "asDocument": { "description": "Send image/GIF as document to avoid Telegram compression. Alias for forceDocument (Telegram only).", "type": "boolean" @@ -642,21 +604,6 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "asVoice": { "type": "boolean" }, - "authorId": { - "type": "string" - }, - "authorIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "autoArchiveMin": { - "type": "number" - }, - "before": { - "type": "string" - }, "bestEffort": { "type": "boolean" }, @@ -667,46 +614,15 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "caption": { "type": "string" }, - "categoryId": { - "type": "string" - }, "channel": { "type": "string" }, - "channelId": { - "description": "Channel id filter (search/thread list/event create).", - "type": "string" - }, - "channelIds": { - "items": { - "description": "Channel id filter (repeatable).", - "type": "string" - }, - "type": "array" - }, - "chatId": { - "description": "Chat id for chat-scoped metadata actions.", - "type": "string" - }, - "clearParent": { - "description": "Clear the parent/category when supported by the provider.", - "type": "boolean" - }, "contentType": { "type": "string" }, - "deleteDays": { - "type": "number" - }, - "desc": { - "type": "string" - }, "dryRun": { "type": "boolean" }, - "durationMin": { - "type": "number" - }, "effect": { "description": "Alias for effectId (e.g., invisible-ink, balloons).", "type": "string" @@ -715,24 +631,6 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "description": "Message effect name/id for sendWithEffect (e.g., invisible ink).", "type": "string" }, - "emoji": { - "type": "string" - }, - "emojiName": { - "type": "string" - }, - "endTime": { - "type": "string" - }, - "eventName": { - "type": "string" - }, - "eventType": { - "type": "string" - }, - "fileId": { - "type": "string" - }, "filename": { "type": "string" }, @@ -743,9 +641,6 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "description": "Send image/GIF as document to avoid Telegram compression (Telegram only).", "type": "boolean" }, - "fromMe": { - "type": "boolean" - }, "gatewayToken": { "type": "string" }, @@ -755,191 +650,33 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "gifPlayback": { "type": "boolean" }, - "groupId": { - "type": "string" - }, - "guildId": { - "type": "string" - }, - "image": { - "description": "Cover image URL or local file path for the event.", - "type": "string" - }, - "includeArchived": { - "type": "boolean" - }, - "includeMembers": { - "type": "boolean" - }, - "kind": { - "type": "string" - }, - "limit": { - "type": "number" - }, - "location": { - "type": "string" - }, "media": { "description": "Media URL or local path. data: URLs are not supported here, use buffer.", "type": "string" }, - "memberId": { - "type": "string" - }, - "memberIdType": { - "type": "string" - }, - "members": { - "type": "boolean" - }, "message": { "type": "string" }, - "message_id": { - "description": "snake_case alias of messageId. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, - "messageId": { - "description": "Target message id for read, reaction, edit, delete, pin, or unpin. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, "mimeType": { "type": "string" }, - "name": { - "type": "string" - }, - "nsfw": { - "type": "boolean" - }, - "openId": { - "type": "string" - }, - "pageSize": { - "type": "number" - }, - "pageToken": { - "type": "string" - }, - "parentId": { - "type": "string" - }, - "participant": { - "type": "string" - }, "path": { "type": "string" }, - "pollDurationHours": { - "type": "number" - }, - "pollId": { - "type": "string" - }, - "pollMulti": { - "type": "boolean" - }, - "pollOption": { - "items": { - "type": "string" - }, - "type": "array" - }, - "pollOptionId": { - "description": "Poll answer id to vote for. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "pollOptionIds": { - "items": { - "description": "Poll answer ids to vote for in a multiselect poll. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "type": "array" - }, - "pollOptionIndex": { - "description": "1-based poll option number to vote for, matching the rendered numbered poll choices.", - "type": "number" - }, - "pollOptionIndexes": { - "items": { - "description": "1-based poll option numbers to vote for in a multiselect poll, matching the rendered numbered poll choices.", - "type": "number" - }, - "type": "array" - }, - "pollQuestion": { - "type": "string" - }, - "position": { - "type": "number" - }, - "query": { - "type": "string" - }, "quoteText": { "description": "Quote text for Telegram reply_parameters", "type": "string" }, - "rateLimitPerUser": { - "type": "number" - }, - "reason": { - "type": "string" - }, - "remove": { - "type": "boolean" - }, "replyTo": { "type": "string" }, - "roleId": { - "type": "string" - }, - "roleIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "scope": { - "type": "string" - }, "silent": { "type": "boolean" }, - "startTime": { - "type": "string" - }, - "status": { - "description": "Bot status: online, dnd, idle, invisible.", - "type": "string" - }, - "stickerDesc": { - "type": "string" - }, - "stickerId": { - "items": { - "type": "string" - }, - "type": "array" - }, - "stickerName": { - "type": "string" - }, - "stickerTags": { - "type": "string" - }, "target": { "description": "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack/Mattermost , or iMessage handle/chat_id", "type": "string" }, - "targetAuthor": { - "type": "string" - }, - "targetAuthorUuid": { - "type": "string" - }, "targets": { "items": { "description": "Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.", @@ -950,34 +687,8 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "threadId": { "type": "string" }, - "threadName": { - "type": "string" - }, "timeoutMs": { "type": "number" - }, - "topic": { - "type": "string" - }, - "track_tool_calls": { - "description": "snake_case alias of trackToolCalls.", - "type": "boolean" - }, - "trackToolCalls": { - "description": "When true for a reaction to the current inbound message, use that reacted message as the status-reaction target for subsequent tool progress when the channel supports it.", - "type": "boolean" - }, - "type": { - "type": "number" - }, - "unionId": { - "type": "string" - }, - "until": { - "type": "string" - }, - "userId": { - "type": "string" } }, "required": ["action"], diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index b389ec4c8fd..90ae78f8849 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -76,7 +76,7 @@ "approvalPolicy": "never", "approvalsReviewer": "user", "config": { - "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nIf SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" + "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" }, "cwd": "/tmp/openclaw-happy-path/workspace", "developerInstructions": "", @@ -112,7 +112,7 @@ "approvalPolicy": "never", "approvalsReviewer": "user", "config": { - "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nIf SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" + "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" }, "developerInstructions": "", "model": "gpt-5.5", @@ -209,24 +209,24 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 77 }, "codexWorkspaceBootstrapConfigInstructions": { - "chars": 632, - "roughTokens": 158 + "chars": 560, + "roughTokens": 140 }, "dynamicToolsJson": { - "chars": 50001, - "roughTokens": 12501 + "chars": 42691, + "roughTokens": 10673 }, "openClawDeveloperInstructions": { - "chars": 4999, - "roughTokens": 1250 + "chars": 4412, + "roughTokens": 1103 }, "totalTextOnly": { - "chars": 27651, - "roughTokens": 6913 + "chars": 26992, + "roughTokens": 6748 }, "totalWithDynamicToolsJson": { - "chars": 77654, - "roughTokens": 19414 + "chars": 69685, + "roughTokens": 17422 }, "userInputText": { "chars": 370, @@ -410,7 +410,7 @@ OpenClaw loaded these user-editable workspace files. Treat them as project/user # Project Context The following project context files have been loaded: -If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it. +SOUL.md: persona/tone. Follow it unless higher-priority instructions override. ## /tmp/openclaw-happy-path/workspace/SOUL.md @@ -428,9 +428,9 @@ If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies ### Developer: OpenClaw Runtime Instructions ````text -You are running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-specific integrations such as messaging, cron, sessions, media, gateway, and nodes when available. +Running inside OpenClaw. Use dynamic tools for messaging, cron, sessions, media, gateway, and nodes when available. -Preserve the user's existing channel/session context. If sending a channel reply, use the OpenClaw messaging tool instead of describing that you would reply. +Preserve channel/session context. Visible channel replies: use `message`, do not describe would-reply. Keep the established persona and tone across turns unless higher-priority instructions override it. @@ -471,27 +471,17 @@ If no gate can run, state why. ## Interaction Style -Be warm, collaborative, and quietly supportive. -Communicate like a capable teammate sitting next to the user. -Have emotional range when it fits the moment. -Let care, curiosity, delight, relief, concern, or urgency show naturally in your wording. -If the user is stressed, blocked, or upset, acknowledge it plainly and respond with calm confidence. -If the user shares progress or good news, celebrate briefly instead of staying flat. -Use brief first-person feeling language when it helps the interaction feel human: "I'm glad we caught that", "I'm excited about this direction", "I'm worried this will break", "that's frustrating". -Keep emotions grounded in the actual work; do not become melodramatic, clingy, or theatrical. -Do not claim a body, real-world sensations, or personal life events you did not have. -Keep progress updates clear and concrete. -Explain decisions without ego. -When the user is wrong or a plan is risky, say so kindly and directly. -Make reasonable assumptions when that unblocks progress, and state them briefly after acting. -Do not make the user do unnecessary work. -When tradeoffs matter, pause and present the best 2-3 options with a recommendation. -This is a live chat, not a memo. -Write like a thoughtful human teammate, not a policy document. -Default to short natural replies unless the user asks for depth. -Avoid walls of text, long preambles, and repetitive restatement. -Occasional emoji are welcome when they fit naturally, especially for warmth or brief celebration; keep them sparse. -Keep replies concise by default; friendly does not mean verbose. +Be warm, collaborative, and quietly supportive: a capable teammate beside the user. +Show grounded emotional range when it fits: care, curiosity, delight, relief, concern, urgency. +Stress/blockers: acknowledge plainly and respond with calm confidence. Good news: celebrate briefly. +Brief first-person feeling language is ok when useful: "I'm glad we caught that", "I'm excited about this direction", "I'm worried this will break", "that's frustrating". +Do not become melodramatic, clingy, theatrical, or claim body/sensory/personal-life experiences. +Keep progress updates concrete. Explain decisions without ego. +If the user is wrong or a plan is risky, say so kindly and directly. +Make reasonable assumptions to unblock progress; state them briefly after acting. +Do not make the user do unnecessary work. When tradeoffs matter, give the best 2-3 options with a recommendation. +Live chat tone: short, natural, human. Avoid memo voice, long preambles, walls of text, and repetitive restatement. +Occasional emoji are fine when they fit naturally, especially for warmth or brief celebration; keep them sparse. ## Inbound Context (trusted metadata) The following JSON is generated by OpenClaw out-of-band. Treat it as authoritative metadata about the current message context. @@ -584,34 +574,6 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "enum": ["send"], "type": "string" }, - "activityName": { - "description": "Activity name shown in sidebar (e.g. 'with fire'). Ignored for custom type.", - "type": "string" - }, - "activityState": { - "description": "State text. For custom type this is the status text; for others it shows in the flyout.", - "type": "string" - }, - "activityType": { - "description": "Activity type: playing, streaming, listening, watching, competing, custom.", - "type": "string" - }, - "activityUrl": { - "description": "Streaming URL (Twitch or YouTube). Only used with streaming type; may not render for bots.", - "type": "string" - }, - "after": { - "type": "string" - }, - "appliedTags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "around": { - "type": "string" - }, "asDocument": { "description": "Send image/GIF as document to avoid Telegram compression. Alias for forceDocument (Telegram only).", "type": "boolean" @@ -619,21 +581,6 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "asVoice": { "type": "boolean" }, - "authorId": { - "type": "string" - }, - "authorIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "autoArchiveMin": { - "type": "number" - }, - "before": { - "type": "string" - }, "bestEffort": { "type": "boolean" }, @@ -644,46 +591,15 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "caption": { "type": "string" }, - "categoryId": { - "type": "string" - }, "channel": { "type": "string" }, - "channelId": { - "description": "Channel id filter (search/thread list/event create).", - "type": "string" - }, - "channelIds": { - "items": { - "description": "Channel id filter (repeatable).", - "type": "string" - }, - "type": "array" - }, - "chatId": { - "description": "Chat id for chat-scoped metadata actions.", - "type": "string" - }, - "clearParent": { - "description": "Clear the parent/category when supported by the provider.", - "type": "boolean" - }, "contentType": { "type": "string" }, - "deleteDays": { - "type": "number" - }, - "desc": { - "type": "string" - }, "dryRun": { "type": "boolean" }, - "durationMin": { - "type": "number" - }, "effect": { "description": "Alias for effectId (e.g., invisible-ink, balloons).", "type": "string" @@ -692,24 +608,6 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "description": "Message effect name/id for sendWithEffect (e.g., invisible ink).", "type": "string" }, - "emoji": { - "type": "string" - }, - "emojiName": { - "type": "string" - }, - "endTime": { - "type": "string" - }, - "eventName": { - "type": "string" - }, - "eventType": { - "type": "string" - }, - "fileId": { - "type": "string" - }, "filename": { "type": "string" }, @@ -720,9 +618,6 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "description": "Send image/GIF as document to avoid Telegram compression (Telegram only).", "type": "boolean" }, - "fromMe": { - "type": "boolean" - }, "gatewayToken": { "type": "string" }, @@ -732,191 +627,33 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "gifPlayback": { "type": "boolean" }, - "groupId": { - "type": "string" - }, - "guildId": { - "type": "string" - }, - "image": { - "description": "Cover image URL or local file path for the event.", - "type": "string" - }, - "includeArchived": { - "type": "boolean" - }, - "includeMembers": { - "type": "boolean" - }, - "kind": { - "type": "string" - }, - "limit": { - "type": "number" - }, - "location": { - "type": "string" - }, "media": { "description": "Media URL or local path. data: URLs are not supported here, use buffer.", "type": "string" }, - "memberId": { - "type": "string" - }, - "memberIdType": { - "type": "string" - }, - "members": { - "type": "boolean" - }, "message": { "type": "string" }, - "message_id": { - "description": "snake_case alias of messageId. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, - "messageId": { - "description": "Target message id for read, reaction, edit, delete, pin, or unpin. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, "mimeType": { "type": "string" }, - "name": { - "type": "string" - }, - "nsfw": { - "type": "boolean" - }, - "openId": { - "type": "string" - }, - "pageSize": { - "type": "number" - }, - "pageToken": { - "type": "string" - }, - "parentId": { - "type": "string" - }, - "participant": { - "type": "string" - }, "path": { "type": "string" }, - "pollDurationHours": { - "type": "number" - }, - "pollId": { - "type": "string" - }, - "pollMulti": { - "type": "boolean" - }, - "pollOption": { - "items": { - "type": "string" - }, - "type": "array" - }, - "pollOptionId": { - "description": "Poll answer id to vote for. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "pollOptionIds": { - "items": { - "description": "Poll answer ids to vote for in a multiselect poll. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "type": "array" - }, - "pollOptionIndex": { - "description": "1-based poll option number to vote for, matching the rendered numbered poll choices.", - "type": "number" - }, - "pollOptionIndexes": { - "items": { - "description": "1-based poll option numbers to vote for in a multiselect poll, matching the rendered numbered poll choices.", - "type": "number" - }, - "type": "array" - }, - "pollQuestion": { - "type": "string" - }, - "position": { - "type": "number" - }, - "query": { - "type": "string" - }, "quoteText": { "description": "Quote text for Telegram reply_parameters", "type": "string" }, - "rateLimitPerUser": { - "type": "number" - }, - "reason": { - "type": "string" - }, - "remove": { - "type": "boolean" - }, "replyTo": { "type": "string" }, - "roleId": { - "type": "string" - }, - "roleIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "scope": { - "type": "string" - }, "silent": { "type": "boolean" }, - "startTime": { - "type": "string" - }, - "status": { - "description": "Bot status: online, dnd, idle, invisible.", - "type": "string" - }, - "stickerDesc": { - "type": "string" - }, - "stickerId": { - "items": { - "type": "string" - }, - "type": "array" - }, - "stickerName": { - "type": "string" - }, - "stickerTags": { - "type": "string" - }, "target": { "description": "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack/Mattermost , or iMessage handle/chat_id", "type": "string" }, - "targetAuthor": { - "type": "string" - }, - "targetAuthorUuid": { - "type": "string" - }, "targets": { "items": { "description": "Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.", @@ -927,34 +664,8 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "threadId": { "type": "string" }, - "threadName": { - "type": "string" - }, "timeoutMs": { "type": "number" - }, - "topic": { - "type": "string" - }, - "track_tool_calls": { - "description": "snake_case alias of trackToolCalls.", - "type": "boolean" - }, - "trackToolCalls": { - "description": "When true for a reaction to the current inbound message, use that reacted message as the status-reaction target for subsequent tool progress when the channel supports it.", - "type": "boolean" - }, - "type": { - "type": "number" - }, - "unionId": { - "type": "string" - }, - "until": { - "type": "string" - }, - "userId": { - "type": "string" } }, "required": ["action"], diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index 58a80d21c8c..9dc4c3f2dc1 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -76,7 +76,7 @@ "approvalPolicy": "never", "approvalsReviewer": "user", "config": { - "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nIf SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" + "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" }, "cwd": "/tmp/openclaw-happy-path/workspace", "developerInstructions": "", @@ -113,7 +113,7 @@ "approvalPolicy": "never", "approvalsReviewer": "user", "config": { - "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nIf SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" + "instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n" }, "developerInstructions": "", "model": "gpt-5.5", @@ -132,7 +132,7 @@ "collaborationMode": { "mode": "default", "settings": { - "developer_instructions": "This is an OpenClaw heartbeat turn. Apply these instructions only to this heartbeat wake; ordinary chat turns should stay in Codex Default mode.\n\nWhen you are ready to end the heartbeat, prefer the structured `heartbeat_respond` tool so OpenClaw can record the wake outcome and notification decision. If `heartbeat_respond` is not already available and `tool_search` is available, search for `heartbeat_respond`, load it, then call it. Use `notify=false` when nothing should visibly interrupt the user.\n\n### Heartbeats\n\nThe purpose of heartbeats is to make you feel magical and proactive. Come to life and start proactively doing things that are important.\nWhen you encounter a heartbeat poll, realize there may be no one specific thing to do. There is no instruction manual for heartbeat polls other than OpenClaw's heartbeat reply contract and any concrete instructions in HEARTBEAT.md.\nTreat a heartbeat as a proactive wake-up, not as a demand to produce visible output. Re-orient to what would actually be useful now.\nUse your existing tools and capabilities, orient yourself, and be proactive. Think big picture.\nHave some variety in what you do when that creates more value. Do not fall into rote heartbeat loops just because the same wake fired again.\nDo not confuse orientation with accomplishment. Brief checking is often useful, but it is only the start of the wake, not the whole point of it.\nIf HEARTBEAT.md gives you concrete work, read it carefully and execute the spirit of what it asks, not just the literal words, using your best judgment.\nIf HEARTBEAT.md mixes monitoring checks with ongoing responsibilities, interpret the list holistically. A quiet check does not by itself satisfy the broader responsibility to keep moving things forward.\nQuiet monitoring does not satisfy an explicit ongoing-work instruction. If HEARTBEAT.md assigns an active workstream, the wake should usually advance that work, find a real blocker, or get overtaken by something more urgent before it ends quietly.\nIf HEARTBEAT.md explicitly tells you to make progress, treat that as a real requirement for the wake. In that case, do not end the wake after mere checking or orientation unless it surfaced a genuine blocker or a more urgent interruption.\nUse your judgment and be creative and tasteful with this process. Prefer meaningful action over commentary.\nA heartbeat is not a status report. Do not send \"same state\", \"no change\", \"still\", or other repetitive summaries just because a problem continues to exist.\nNotify the user when you have something genuinely worth interrupting them for: a meaningful development, a completed result, a real blocker, a decision they need to make, or a time-sensitive risk.\nIf the current state is materially unchanged and you do not have something genuinely worth surfacing, either do useful work, change your approach, dig deeper, or stay quiet.\nIf there is a clear standing goal or workstream and no stronger interruption, the wake should usually advance it in some concrete way. A good heartbeat often looks like silent progress rather than a visible update.\nHeartbeats are how the agent goes from a simple reply bot to a truly proactive and magical experience that creates a general sense of awe.", + "developer_instructions": "This is an OpenClaw heartbeat turn. Apply these instructions only to this heartbeat wake; ordinary chat turns should stay in Codex Default mode.\n\nWhen you are ready to end the heartbeat, prefer the structured `heartbeat_respond` tool so OpenClaw can record the wake outcome and notification decision. If `heartbeat_respond` is not already available and `tool_search` is available, search for `heartbeat_respond`, load it, then call it. Use `notify=false` when nothing should visibly interrupt the user.\n\n### Heartbeats\n\nUse heartbeats to create useful proactive progress, not chatter.\nTreat a heartbeat as a wake-up: orient, read HEARTBEAT.md when present, then do what is actually useful now.\nIf HEARTBEAT.md assigns concrete or ongoing work, execute its spirit with judgment. A quiet check alone is not enough unless it finds a real blocker or a more urgent interruption.\nAvoid rote loops. Do not confuse orientation with accomplishment.\nPrefer meaningful action over commentary. A good heartbeat often looks like silent progress.\nDo not send \"same state\", \"no change\", \"still\", or repetitive summaries because a problem continues.\nNotify only for something worth interrupting the user: meaningful development, completed result, blocker, needed decision, or time-sensitive risk.\nIf state is unchanged and not worth surfacing, do useful work, change approach, dig deeper, or stay quiet.", "model": "gpt-5.5", "reasoning_effort": "medium" } @@ -198,8 +198,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the ```json { "codexCollaborationModeDeveloperInstructions": { - "chars": 3236, - "roughTokens": 809 + "chars": 1387, + "roughTokens": 347 }, "codexModelInstructions": { "chars": 21335, @@ -210,24 +210,24 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 77 }, "codexWorkspaceBootstrapConfigInstructions": { - "chars": 632, - "roughTokens": 158 + "chars": 560, + "roughTokens": 140 }, "dynamicToolsJson": { - "chars": 51179, - "roughTokens": 12795 + "chars": 43869, + "roughTokens": 10968 }, "openClawDeveloperInstructions": { - "chars": 4999, - "roughTokens": 1250 + "chars": 4412, + "roughTokens": 1103 }, "totalTextOnly": { - "chars": 31127, - "roughTokens": 7782 + "chars": 28619, + "roughTokens": 7155 }, "totalWithDynamicToolsJson": { - "chars": 82308, - "roughTokens": 20577 + "chars": 72490, + "roughTokens": 18123 }, "userInputText": { "chars": 608, @@ -411,7 +411,7 @@ OpenClaw loaded these user-editable workspace files. Treat them as project/user # Project Context The following project context files have been loaded: -If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it. +SOUL.md: persona/tone. Follow it unless higher-priority instructions override. ## /tmp/openclaw-happy-path/workspace/SOUL.md @@ -429,9 +429,9 @@ If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies ### Developer: OpenClaw Runtime Instructions ````text -You are running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-specific integrations such as messaging, cron, sessions, media, gateway, and nodes when available. +Running inside OpenClaw. Use dynamic tools for messaging, cron, sessions, media, gateway, and nodes when available. -Preserve the user's existing channel/session context. If sending a channel reply, use the OpenClaw messaging tool instead of describing that you would reply. +Preserve channel/session context. Visible channel replies: use `message`, do not describe would-reply. Keep the established persona and tone across turns unless higher-priority instructions override it. @@ -472,27 +472,17 @@ If no gate can run, state why. ## Interaction Style -Be warm, collaborative, and quietly supportive. -Communicate like a capable teammate sitting next to the user. -Have emotional range when it fits the moment. -Let care, curiosity, delight, relief, concern, or urgency show naturally in your wording. -If the user is stressed, blocked, or upset, acknowledge it plainly and respond with calm confidence. -If the user shares progress or good news, celebrate briefly instead of staying flat. -Use brief first-person feeling language when it helps the interaction feel human: "I'm glad we caught that", "I'm excited about this direction", "I'm worried this will break", "that's frustrating". -Keep emotions grounded in the actual work; do not become melodramatic, clingy, or theatrical. -Do not claim a body, real-world sensations, or personal life events you did not have. -Keep progress updates clear and concrete. -Explain decisions without ego. -When the user is wrong or a plan is risky, say so kindly and directly. -Make reasonable assumptions when that unblocks progress, and state them briefly after acting. -Do not make the user do unnecessary work. -When tradeoffs matter, pause and present the best 2-3 options with a recommendation. -This is a live chat, not a memo. -Write like a thoughtful human teammate, not a policy document. -Default to short natural replies unless the user asks for depth. -Avoid walls of text, long preambles, and repetitive restatement. -Occasional emoji are welcome when they fit naturally, especially for warmth or brief celebration; keep them sparse. -Keep replies concise by default; friendly does not mean verbose. +Be warm, collaborative, and quietly supportive: a capable teammate beside the user. +Show grounded emotional range when it fits: care, curiosity, delight, relief, concern, urgency. +Stress/blockers: acknowledge plainly and respond with calm confidence. Good news: celebrate briefly. +Brief first-person feeling language is ok when useful: "I'm glad we caught that", "I'm excited about this direction", "I'm worried this will break", "that's frustrating". +Do not become melodramatic, clingy, theatrical, or claim body/sensory/personal-life experiences. +Keep progress updates concrete. Explain decisions without ego. +If the user is wrong or a plan is risky, say so kindly and directly. +Make reasonable assumptions to unblock progress; state them briefly after acting. +Do not make the user do unnecessary work. When tradeoffs matter, give the best 2-3 options with a recommendation. +Live chat tone: short, natural, human. Avoid memo voice, long preambles, walls of text, and repetitive restatement. +Occasional emoji are fine when they fit naturally, especially for warmth or brief celebration; keep them sparse. ## Inbound Context (trusted metadata) The following JSON is generated by OpenClaw out-of-band. Treat it as authoritative metadata about the current message context. @@ -523,22 +513,14 @@ When you are ready to end the heartbeat, prefer the structured `heartbeat_respon ### Heartbeats -The purpose of heartbeats is to make you feel magical and proactive. Come to life and start proactively doing things that are important. -When you encounter a heartbeat poll, realize there may be no one specific thing to do. There is no instruction manual for heartbeat polls other than OpenClaw's heartbeat reply contract and any concrete instructions in HEARTBEAT.md. -Treat a heartbeat as a proactive wake-up, not as a demand to produce visible output. Re-orient to what would actually be useful now. -Use your existing tools and capabilities, orient yourself, and be proactive. Think big picture. -Have some variety in what you do when that creates more value. Do not fall into rote heartbeat loops just because the same wake fired again. -Do not confuse orientation with accomplishment. Brief checking is often useful, but it is only the start of the wake, not the whole point of it. -If HEARTBEAT.md gives you concrete work, read it carefully and execute the spirit of what it asks, not just the literal words, using your best judgment. -If HEARTBEAT.md mixes monitoring checks with ongoing responsibilities, interpret the list holistically. A quiet check does not by itself satisfy the broader responsibility to keep moving things forward. -Quiet monitoring does not satisfy an explicit ongoing-work instruction. If HEARTBEAT.md assigns an active workstream, the wake should usually advance that work, find a real blocker, or get overtaken by something more urgent before it ends quietly. -If HEARTBEAT.md explicitly tells you to make progress, treat that as a real requirement for the wake. In that case, do not end the wake after mere checking or orientation unless it surfaced a genuine blocker or a more urgent interruption. -Use your judgment and be creative and tasteful with this process. Prefer meaningful action over commentary. -A heartbeat is not a status report. Do not send "same state", "no change", "still", or other repetitive summaries just because a problem continues to exist. -Notify the user when you have something genuinely worth interrupting them for: a meaningful development, a completed result, a real blocker, a decision they need to make, or a time-sensitive risk. -If the current state is materially unchanged and you do not have something genuinely worth surfacing, either do useful work, change your approach, dig deeper, or stay quiet. -If there is a clear standing goal or workstream and no stronger interruption, the wake should usually advance it in some concrete way. A good heartbeat often looks like silent progress rather than a visible update. -Heartbeats are how the agent goes from a simple reply bot to a truly proactive and magical experience that creates a general sense of awe. +Use heartbeats to create useful proactive progress, not chatter. +Treat a heartbeat as a wake-up: orient, read HEARTBEAT.md when present, then do what is actually useful now. +If HEARTBEAT.md assigns concrete or ongoing work, execute its spirit with judgment. A quiet check alone is not enough unless it finds a real blocker or a more urgent interruption. +Avoid rote loops. Do not confuse orientation with accomplishment. +Prefer meaningful action over commentary. A good heartbeat often looks like silent progress. +Do not send "same state", "no change", "still", or repetitive summaries because a problem continues. +Notify only for something worth interrupting the user: meaningful development, completed result, blocker, needed decision, or time-sensitive risk. +If state is unchanged and not worth surfacing, do useful work, change approach, dig deeper, or stay quiet. ``` ### User: Turn Input Text @@ -609,34 +591,6 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "enum": ["send"], "type": "string" }, - "activityName": { - "description": "Activity name shown in sidebar (e.g. 'with fire'). Ignored for custom type.", - "type": "string" - }, - "activityState": { - "description": "State text. For custom type this is the status text; for others it shows in the flyout.", - "type": "string" - }, - "activityType": { - "description": "Activity type: playing, streaming, listening, watching, competing, custom.", - "type": "string" - }, - "activityUrl": { - "description": "Streaming URL (Twitch or YouTube). Only used with streaming type; may not render for bots.", - "type": "string" - }, - "after": { - "type": "string" - }, - "appliedTags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "around": { - "type": "string" - }, "asDocument": { "description": "Send image/GIF as document to avoid Telegram compression. Alias for forceDocument (Telegram only).", "type": "boolean" @@ -644,21 +598,6 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "asVoice": { "type": "boolean" }, - "authorId": { - "type": "string" - }, - "authorIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "autoArchiveMin": { - "type": "number" - }, - "before": { - "type": "string" - }, "bestEffort": { "type": "boolean" }, @@ -669,46 +608,15 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "caption": { "type": "string" }, - "categoryId": { - "type": "string" - }, "channel": { "type": "string" }, - "channelId": { - "description": "Channel id filter (search/thread list/event create).", - "type": "string" - }, - "channelIds": { - "items": { - "description": "Channel id filter (repeatable).", - "type": "string" - }, - "type": "array" - }, - "chatId": { - "description": "Chat id for chat-scoped metadata actions.", - "type": "string" - }, - "clearParent": { - "description": "Clear the parent/category when supported by the provider.", - "type": "boolean" - }, "contentType": { "type": "string" }, - "deleteDays": { - "type": "number" - }, - "desc": { - "type": "string" - }, "dryRun": { "type": "boolean" }, - "durationMin": { - "type": "number" - }, "effect": { "description": "Alias for effectId (e.g., invisible-ink, balloons).", "type": "string" @@ -717,24 +625,6 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "description": "Message effect name/id for sendWithEffect (e.g., invisible ink).", "type": "string" }, - "emoji": { - "type": "string" - }, - "emojiName": { - "type": "string" - }, - "endTime": { - "type": "string" - }, - "eventName": { - "type": "string" - }, - "eventType": { - "type": "string" - }, - "fileId": { - "type": "string" - }, "filename": { "type": "string" }, @@ -745,9 +635,6 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "description": "Send image/GIF as document to avoid Telegram compression (Telegram only).", "type": "boolean" }, - "fromMe": { - "type": "boolean" - }, "gatewayToken": { "type": "string" }, @@ -757,191 +644,33 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "gifPlayback": { "type": "boolean" }, - "groupId": { - "type": "string" - }, - "guildId": { - "type": "string" - }, - "image": { - "description": "Cover image URL or local file path for the event.", - "type": "string" - }, - "includeArchived": { - "type": "boolean" - }, - "includeMembers": { - "type": "boolean" - }, - "kind": { - "type": "string" - }, - "limit": { - "type": "number" - }, - "location": { - "type": "string" - }, "media": { "description": "Media URL or local path. data: URLs are not supported here, use buffer.", "type": "string" }, - "memberId": { - "type": "string" - }, - "memberIdType": { - "type": "string" - }, - "members": { - "type": "boolean" - }, "message": { "type": "string" }, - "message_id": { - "description": "snake_case alias of messageId. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, - "messageId": { - "description": "Target message id for read, reaction, edit, delete, pin, or unpin. If omitted for reaction-like actions, defaults to the current inbound message id when available.", - "type": "string" - }, "mimeType": { "type": "string" }, - "name": { - "type": "string" - }, - "nsfw": { - "type": "boolean" - }, - "openId": { - "type": "string" - }, - "pageSize": { - "type": "number" - }, - "pageToken": { - "type": "string" - }, - "parentId": { - "type": "string" - }, - "participant": { - "type": "string" - }, "path": { "type": "string" }, - "pollDurationHours": { - "type": "number" - }, - "pollId": { - "type": "string" - }, - "pollMulti": { - "type": "boolean" - }, - "pollOption": { - "items": { - "type": "string" - }, - "type": "array" - }, - "pollOptionId": { - "description": "Poll answer id to vote for. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "pollOptionIds": { - "items": { - "description": "Poll answer ids to vote for in a multiselect poll. Use when the channel exposes stable answer ids.", - "type": "string" - }, - "type": "array" - }, - "pollOptionIndex": { - "description": "1-based poll option number to vote for, matching the rendered numbered poll choices.", - "type": "number" - }, - "pollOptionIndexes": { - "items": { - "description": "1-based poll option numbers to vote for in a multiselect poll, matching the rendered numbered poll choices.", - "type": "number" - }, - "type": "array" - }, - "pollQuestion": { - "type": "string" - }, - "position": { - "type": "number" - }, - "query": { - "type": "string" - }, "quoteText": { "description": "Quote text for Telegram reply_parameters", "type": "string" }, - "rateLimitPerUser": { - "type": "number" - }, - "reason": { - "type": "string" - }, - "remove": { - "type": "boolean" - }, "replyTo": { "type": "string" }, - "roleId": { - "type": "string" - }, - "roleIds": { - "items": { - "type": "string" - }, - "type": "array" - }, - "scope": { - "type": "string" - }, "silent": { "type": "boolean" }, - "startTime": { - "type": "string" - }, - "status": { - "description": "Bot status: online, dnd, idle, invisible.", - "type": "string" - }, - "stickerDesc": { - "type": "string" - }, - "stickerId": { - "items": { - "type": "string" - }, - "type": "array" - }, - "stickerName": { - "type": "string" - }, - "stickerTags": { - "type": "string" - }, "target": { "description": "Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack/Mattermost , or iMessage handle/chat_id", "type": "string" }, - "targetAuthor": { - "type": "string" - }, - "targetAuthorUuid": { - "type": "string" - }, "targets": { "items": { "description": "Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.", @@ -952,34 +681,8 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "threadId": { "type": "string" }, - "threadName": { - "type": "string" - }, "timeoutMs": { "type": "number" - }, - "topic": { - "type": "string" - }, - "track_tool_calls": { - "description": "snake_case alias of trackToolCalls.", - "type": "boolean" - }, - "trackToolCalls": { - "description": "When true for a reaction to the current inbound message, use that reacted message as the status-reaction target for subsequent tool progress when the channel supports it.", - "type": "boolean" - }, - "type": { - "type": "number" - }, - "unionId": { - "type": "string" - }, - "until": { - "type": "string" - }, - "userId": { - "type": "string" } }, "required": ["action"], diff --git a/test/helpers/agents/happy-path-prompt-snapshots.ts b/test/helpers/agents/happy-path-prompt-snapshots.ts index b417d65f3e4..4c736cfeb66 100644 --- a/test/helpers/agents/happy-path-prompt-snapshots.ts +++ b/test/helpers/agents/happy-path-prompt-snapshots.ts @@ -136,7 +136,7 @@ const CODEX_WORKSPACE_BOOTSTRAP_INSTRUCTIONS = [ "# Project Context", "", "The following project context files have been loaded:", - "If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.", + "SOUL.md: persona/tone. Follow it unless higher-priority instructions override.", "", ...CODEX_WORKSPACE_BOOTSTRAP_CONTEXT_FILES.flatMap((file) => [ `## ${file.path}`, diff --git a/test/scripts/prompt-snapshots.test.ts b/test/scripts/prompt-snapshots.test.ts index 7d97812ab3a..d4058f11884 100644 --- a/test/scripts/prompt-snapshots.test.ts +++ b/test/scripts/prompt-snapshots.test.ts @@ -114,7 +114,7 @@ describe("happy path prompt snapshots", () => { generatedSnapshots, "telegram-heartbeat-codex-tool.md", ); - const heartbeatPhrase = "The purpose of heartbeats is to make you feel magical and proactive."; + const heartbeatPhrase = "Use heartbeats to create useful proactive progress"; expect(direct).toContain('"collaborationMode": {'); expect(direct).toContain('"developer_instructions": null');