From cf11d16b43d0c5301ad42af88c5bb854202ecf8c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 13 May 2026 08:40:27 +0100 Subject: [PATCH] fix(agents): make subagent task delivery visible Co-authored-by: stainlu --- CHANGELOG.md | 1 + .../.generated/plugin-sdk-api-baseline.sha256 | 4 +-- docs/concepts/session-tool.md | 4 ++- docs/tools/subagents.md | 1 + .../subagent-initial-user-message.test.ts | 17 +++++++---- src/agents/subagent-initial-user-message.ts | 16 ++++++---- src/agents/subagent-spawn.test.ts | 7 +++-- src/agents/subagent-spawn.ts | 1 + src/agents/subagent-system-prompt.ts | 30 +++++-------------- src/agents/system-prompt.test.ts | 12 ++++---- src/agents/tool-description-presets.ts | 1 + .../codex-dynamic-tools.discord-group.json | 2 +- .../codex-dynamic-tools.heartbeat-turn.json | 2 +- .../codex-dynamic-tools.telegram-direct.json | 2 +- .../discord-group-codex-message-tool.md | 8 ++--- .../telegram-direct-codex-message-tool.md | 8 ++--- .../telegram-heartbeat-codex-tool.md | 8 ++--- 17 files changed, 62 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c40ba3e9d0..cb5fc0077bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Agents/subagents: deliver native `sessions_spawn` tasks in the child session's first visible `[Subagent Task]` message instead of hiding the task in the sub-agent system prompt, keeping delegation auditable without duplicating tokens. Fixes #78592. Thanks @bradestes and @stainlu. - Voice Call/Telnyx: add realtime media-streaming call support for conversational voice calls. (#81024) Thanks @dynamite-bud. - Gateway/OpenAI HTTP: honor `max_completion_tokens` and `max_tokens` on inbound `/v1/chat/completions` requests so client-provided token caps reach the upstream provider via `streamParams.maxTokens`, with `max_completion_tokens` taking precedence when both are sent. Thanks @Lellansin. - Models/OpenAI CLI auth: make `openclaw models auth login --provider openai` start the ChatGPT/Codex account login by default, while `--method api-key` remains the explicit OpenAI API-key setup path. diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 039c68442f2..23dc538774e 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -981f125194293842b7a45b1de0ae2ec134f037f63a6cc672ee2a28648251b4c9 plugin-sdk-api-baseline.json -4c56ce2cb5bfae526557479a6cc19f8b0042d14f6c717996f8f86da5d5b159df plugin-sdk-api-baseline.jsonl +542dc30fe44a16119ee57f9fe48a5744beb7fc2cf425a5777b4c4b8b2ce883e1 plugin-sdk-api-baseline.json +9f4fde0de9773af635862ea15ce1a3391ef15e3165ad43b2050b1c4b3113acf4 plugin-sdk-api-baseline.jsonl diff --git a/docs/concepts/session-tool.md b/docs/concepts/session-tool.md index d9a90cd6401..7a57e2c4192 100644 --- a/docs/concepts/session-tool.md +++ b/docs/concepts/session-tool.md @@ -134,7 +134,9 @@ sub-agents. It supports: `sessions_spawn` creates an isolated session for a background task by default. It is always non-blocking -- it returns immediately with a `runId` and -`childSessionKey`. +`childSessionKey`. Native sub-agent runs receive the delegated task in the +child session's first visible `[Subagent Task]` message, while the system +prompt carries only sub-agent runtime rules and routing context. Key options: diff --git a/docs/tools/subagents.md b/docs/tools/subagents.md index d6ff0b3aa46..2ac6c297e33 100644 --- a/docs/tools/subagents.md +++ b/docs/tools/subagents.md @@ -144,6 +144,7 @@ session to confirm the effective tool list. - **Model:** inherits the caller unless you set `agents.defaults.subagents.model` (or per-agent `agents.list[].subagents.model`); an explicit `sessions_spawn.model` still wins. - **Thinking:** inherits the caller unless you set `agents.defaults.subagents.thinking` (or per-agent `agents.list[].subagents.thinking`); an explicit `sessions_spawn.thinking` still wins. - **Run timeout:** if `sessions_spawn.runTimeoutSeconds` is omitted, OpenClaw uses `agents.defaults.subagents.runTimeoutSeconds` when set; otherwise it falls back to `0` (no timeout). +- **Task delivery:** native sub-agents receive the delegated task in their first visible `[Subagent Task]` message. The sub-agent system prompt carries runtime rules and routing context, not a hidden duplicate of the task. ### Delegation prompt mode diff --git a/src/agents/subagent-initial-user-message.test.ts b/src/agents/subagent-initial-user-message.test.ts index 4481770ee35..5ce57980ca6 100644 --- a/src/agents/subagent-initial-user-message.test.ts +++ b/src/agents/subagent-initial-user-message.test.ts @@ -3,15 +3,18 @@ import { buildSubagentInitialUserMessage } from "./subagent-initial-user-message import { buildSubagentSystemPrompt } from "./subagent-system-prompt.js"; describe("buildSubagentInitialUserMessage", () => { - it("does not embed a task string already present in the system prompt (#72019)", () => { + it("embeds the delegated task in a visible task envelope", () => { const msg = buildSubagentInitialUserMessage({ childDepth: 1, maxSpawnDepth: 3, persistentSession: false, + task: "UNIQUE_VISIBLE_TASK\n preserve indentation", }); - expect(msg).not.toContain("[Subagent Task]:"); - expect(msg).toContain("**Your Role**"); + expect(msg).toContain("[Subagent Task]"); + expect(msg).toContain("UNIQUE_VISIBLE_TASK"); + expect(msg).toContain(" preserve indentation"); + expect(msg).not.toContain("**Your Role**"); expect(msg).toContain("depth 1/3"); }); @@ -20,12 +23,13 @@ describe("buildSubagentInitialUserMessage", () => { childDepth: 2, maxSpawnDepth: 4, persistentSession: true, + task: "continue the task", }); expect(msg).toContain("persistent and remains available"); }); - it("keeps the delegated task single-sourced across system and first user text", () => { + it("keeps the delegated task single-sourced in first user text", () => { const task = "UNIQUE_SUBAGENT_TASK_TOKEN\n preserve indentation"; const system = buildSubagentSystemPrompt({ childSessionKey: "agent:main:subagent:test", @@ -37,10 +41,11 @@ describe("buildSubagentInitialUserMessage", () => { childDepth: 1, maxSpawnDepth: 2, persistentSession: false, + task, }); - expect(system).toContain("UNIQUE_SUBAGENT_TASK_TOKEN"); - expect(user).not.toContain("UNIQUE_SUBAGENT_TASK_TOKEN"); + expect(system).not.toContain("UNIQUE_SUBAGENT_TASK_TOKEN"); + expect(user).toContain("UNIQUE_SUBAGENT_TASK_TOKEN"); expect(`${system}\n${user}`.match(/UNIQUE_SUBAGENT_TASK_TOKEN/g)).toHaveLength(1); }); }); diff --git a/src/agents/subagent-initial-user-message.ts b/src/agents/subagent-initial-user-message.ts index df5888bcb12..a73d01d7f16 100644 --- a/src/agents/subagent-initial-user-message.ts +++ b/src/agents/subagent-initial-user-message.ts @@ -1,15 +1,16 @@ /** * First user turn for a native `sessions_spawn` / subagent run. * - * Keep the full task out of this message: `buildSubagentSystemPrompt` already - * places it under **Your Role**, and repeating it here doubles first-request - * input tokens (#72019). + * Keep the delegated task transcript-visible and single-sourced here. The + * system prompt owns runtime/subagent rules; this user turn owns the actual + * task envelope so delivery is easy to audit without duplicating tokens. */ export function buildSubagentInitialUserMessage(params: { childDepth: number; maxSpawnDepth: number; /** When true, this subagent uses a persistent session for follow-up messages. */ persistentSession: boolean; + task?: string; }): string { const lines = [ `[Subagent Context] You are running as a subagent (depth ${params.childDepth}/${params.maxSpawnDepth}). Results auto-announce to your requester; do not busy-poll for status.`, @@ -19,8 +20,11 @@ export function buildSubagentInitialUserMessage(params: { "[Subagent Context] This subagent session is persistent and remains available for thread follow-up messages.", ); } - lines.push( - "Begin. Your assigned task is in the system prompt under **Your Role**; execute it to completion.", - ); + const taskBody = params.task?.trim(); + if (taskBody) { + lines.push("[Subagent Task]", taskBody, "Begin. Execute the assigned task to completion."); + } else { + lines.push("Begin. Execute the assigned task to completion."); + } return lines.join("\n\n"); } diff --git a/src/agents/subagent-spawn.test.ts b/src/agents/subagent-spawn.test.ts index 174889ea224..266141fa52d 100644 --- a/src/agents/subagent-spawn.test.ts +++ b/src/agents/subagent-spawn.test.ts @@ -402,9 +402,10 @@ describe("spawnSubagentDirect seam flow", () => { expect(result.status).toBe("accepted"); const agentCall = calls.find((call) => call.method === "agent"); const params = agentCall?.params as { message?: string; extraSystemPrompt?: string }; - expect(params.message).not.toContain("UNIQUE_LONG_SUBAGENT_TASK_TOKEN"); - expect(params.message).not.toContain("[Subagent Task]:"); - expect(params.message).toContain("**Your Role**"); + expect(params.message).toContain("[Subagent Task]"); + expect(params.message).toContain("UNIQUE_LONG_SUBAGENT_TASK_TOKEN"); + expect(params.message).toContain(" keep indentation"); + expect(params.message).not.toContain("**Your Role**"); expect(params.extraSystemPrompt).toBe("system-prompt"); }); diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 2cea2cd5fe2..9744fd86f23 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -1067,6 +1067,7 @@ export async function spawnSubagentDirect( childDepth, maxSpawnDepth, persistentSession: spawnMode === "session", + task, }); const toolSpawnMetadata = mapToolContextToSpawnedRunMetadata({ diff --git a/src/agents/subagent-system-prompt.ts b/src/agents/subagent-system-prompt.ts index 7a4b084f342..225d397210d 100644 --- a/src/agents/subagent-system-prompt.ts +++ b/src/agents/subagent-system-prompt.ts @@ -18,9 +18,6 @@ export function buildSubagentSystemPrompt(params: { /** Config value: max allowed spawn depth. */ maxSpawnDepth?: number; }) { - const taskRaw = typeof params.task === "string" ? params.task : ""; - const taskBody = taskRaw.trim(); - const hasTask = taskBody !== ""; const childDepth = typeof params.childDepth === "number" ? params.childDepth : 1; const maxSpawnDepth = typeof params.maxSpawnDepth === "number" @@ -32,26 +29,13 @@ export function buildSubagentSystemPrompt(params: { ); const canSpawn = childDepth < maxSpawnDepth; const parentLabel = childDepth >= 2 ? "parent orchestrator" : "main agent"; - const roleLines = - hasTask && taskBody.includes("\n") - ? [ - "## Your Role", - "- You were created to handle the following task (verbatim; line breaks preserved):", - "", - "```", - taskBody, - "```", - "- Complete this task. That's your entire purpose.", - `- You are NOT the ${parentLabel}. Don't try to be.`, - "", - ] - : [ - "## Your Role", - `- You were created to handle: ${hasTask ? taskBody : "{{TASK_DESCRIPTION}}"}`, - "- Complete this task. That's your entire purpose.", - `- You are NOT the ${parentLabel}. Don't try to be.`, - "", - ]; + const roleLines = [ + "## Your Role", + "- You were created to handle the task in the first user-visible `[Subagent Task]` message.", + "- Complete that task. That's your entire purpose.", + `- You are NOT the ${parentLabel}. Don't try to be.`, + "", + ]; const lines = [ "# Subagent Context", diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index fbee5bf4193..98c5ecb7404 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -1278,7 +1278,7 @@ describe("buildSubagentSystemPrompt", () => { expect(prompt).toContain("instead of full-file `cat`"); }); - it("keeps multiline and indented task text verbatim in the system prompt (#72019)", () => { + it("keeps delegated task text out of the system prompt", () => { const task = "line one\n line two\n line three"; const prompt = buildSubagentSystemPrompt({ childSessionKey: "agent:main:subagent:abc", @@ -1287,11 +1287,11 @@ describe("buildSubagentSystemPrompt", () => { maxSpawnDepth: 1, }); - expect(prompt).toContain("```"); - expect(prompt).toContain("line one"); - expect(prompt).toContain(" line two"); - expect(prompt).toContain(" line three"); - expect(prompt).not.toContain("line one line two"); + expect(prompt).toContain("## Your Role"); + expect(prompt).toContain("first user-visible `[Subagent Task]` message"); + expect(prompt).not.toContain("line one"); + expect(prompt).not.toContain(" line two"); + expect(prompt).not.toContain(" line three"); }); it("omits ACP spawning guidance when ACP is disabled", () => { diff --git a/src/agents/tool-description-presets.ts b/src/agents/tool-description-presets.ts index d2b1c012ded..511f575d41e 100644 --- a/src/agents/tool-description-presets.ts +++ b/src/agents/tool-description-presets.ts @@ -43,6 +43,7 @@ export function describeSessionsSpawnTool(options?: { ? '`mode="run"` is one-shot and `mode="session"` is persistent and thread-bound.' : '`mode="run"` is one-shot background work.', "Subagents inherit the parent workspace directory automatically.", + "Native subagents receive the delegated task in their first visible `[Subagent Task]` message.", 'For native subagents only, set `context="fork"` when the child needs the current transcript context; otherwise omit it or use `context="isolated"`.', "Use this when the work should happen in a fresh child session instead of the current one.", ]; 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 a12a8c35724..4d1ba846e70 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 @@ -921,7 +921,7 @@ }, { "deferLoading": true, - "description": "Spawn a clean isolated session by default with the native subagent runtime. `mode=\"run\"` is one-shot and `mode=\"session\"` is persistent and thread-bound. Subagents inherit the parent workspace directory automatically. For native subagents only, set `context=\"fork\"` when the child needs the current transcript context; otherwise omit it or use `context=\"isolated\"`. Use this when the work should happen in a fresh child session instead of the current one.", + "description": "Spawn a clean isolated session by default with the native subagent runtime. `mode=\"run\"` is one-shot and `mode=\"session\"` is persistent and thread-bound. Subagents inherit the parent workspace directory automatically. Native subagents receive the delegated task in their first visible `[Subagent Task]` message. For native subagents only, set `context=\"fork\"` when the child needs the current transcript context; otherwise omit it or use `context=\"isolated\"`. Use this when the work should happen in a fresh child session instead of the current one.", "inputSchema": { "properties": { "agentId": { 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 5e98adbed56..49a6cd8fa9d 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 @@ -957,7 +957,7 @@ }, { "deferLoading": true, - "description": "Spawn a clean isolated session by default with the native subagent runtime. `mode=\"run\"` is one-shot background work. Subagents inherit the parent workspace directory automatically. For native subagents only, set `context=\"fork\"` when the child needs the current transcript context; otherwise omit it or use `context=\"isolated\"`. Use this when the work should happen in a fresh child session instead of the current one.", + "description": "Spawn a clean isolated session by default with the native subagent runtime. `mode=\"run\"` is one-shot background work. Subagents inherit the parent workspace directory automatically. Native subagents receive the delegated task in their first visible `[Subagent Task]` message. For native subagents only, set `context=\"fork\"` when the child needs the current transcript context; otherwise omit it or use `context=\"isolated\"`. Use this when the work should happen in a fresh child session instead of the current one.", "inputSchema": { "properties": { "agentId": { 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 4310d0b21d2..74ba8fa8528 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 @@ -921,7 +921,7 @@ }, { "deferLoading": true, - "description": "Spawn a clean isolated session by default with the native subagent runtime. `mode=\"run\"` is one-shot background work. Subagents inherit the parent workspace directory automatically. For native subagents only, set `context=\"fork\"` when the child needs the current transcript context; otherwise omit it or use `context=\"isolated\"`. Use this when the work should happen in a fresh child session instead of the current one.", + "description": "Spawn a clean isolated session by default with the native subagent runtime. `mode=\"run\"` is one-shot background work. Subagents inherit the parent workspace directory automatically. Native subagents receive the delegated task in their first visible `[Subagent Task]` message. For native subagents only, set `context=\"fork\"` when the child needs the current transcript context; otherwise omit it or use `context=\"isolated\"`. Use this when the work should happen in a fresh child session instead of the current one.", "inputSchema": { "properties": { "agentId": { 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 8384a201977..ca077d67bf7 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 @@ -217,8 +217,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 140 }, "dynamicToolsJson": { - "chars": 43053, - "roughTokens": 10764 + "chars": 43147, + "roughTokens": 10787 }, "openClawDeveloperInstructions": { "chars": 5436, @@ -229,8 +229,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 7129 }, "totalWithDynamicToolsJson": { - "chars": 71571, - "roughTokens": 17893 + "chars": 71665, + "roughTokens": 17917 }, "userInputText": { "chars": 870, 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 ea82ffb4797..1b4f580b83d 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 @@ -217,8 +217,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 140 }, "dynamicToolsJson": { - "chars": 42744, - "roughTokens": 10686 + "chars": 42838, + "roughTokens": 10710 }, "openClawDeveloperInstructions": { "chars": 4412, @@ -229,8 +229,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6748 }, "totalWithDynamicToolsJson": { - "chars": 69738, - "roughTokens": 17435 + "chars": 69832, + "roughTokens": 17458 }, "userInputText": { "chars": 370, 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 99a23a977e1..cd606c8cf69 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 @@ -218,8 +218,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 140 }, "dynamicToolsJson": { - "chars": 43922, - "roughTokens": 10981 + "chars": 44016, + "roughTokens": 11004 }, "openClawDeveloperInstructions": { "chars": 4412, @@ -230,8 +230,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 7155 }, "totalWithDynamicToolsJson": { - "chars": 72543, - "roughTokens": 18136 + "chars": 72637, + "roughTokens": 18160 }, "userInputText": { "chars": 608,