From 8040f28bc5e70c22cdf057f365af94f6bed9985d Mon Sep 17 00:00:00 2001 From: OpenClaw Contributor <100menotu001@users.noreply.github.com> Date: Sat, 16 May 2026 15:57:45 -0400 Subject: [PATCH] fix(subagents): make completion handoff review-first --- CHANGELOG.md | 1 + docs/tools/subagents.md | 4 +++- src/agents/subagent-announce.format.e2e.test.ts | 14 +++++++++++--- src/agents/subagent-announce.ts | 4 ++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8fca4aff934..eb7627a77d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai - CLI/docs: call the canonical lowercase docs MCP search tool and surface MCP errors instead of returning empty search results. Fixes #82702. (#82704) Thanks @hclsys. - QA-Lab: ignore heartbeat-only operational transcripts when capturing runtime parity cells so background checks cannot replace the scenario reply. (#80323) Thanks @100yenadmin. - Gateway/exec approvals: wait for accepted async approval follow-up runs instead of direct-fallback sending duplicate completions when retries use different nonce keys. Fixes #82711. (#82717) Thanks @udaymanish6. +- Agents/subagents: mark completed subagent handoffs as ready for parent review so requester agents verify results and continue required follow-up work before reporting done. - CLI/config: add `--dry-run` support to `openclaw config unset`, with `--json` output and allow-exec validation parity with `config set`/`config patch` dry-run handling. (#81895) Thanks @giodl73-repo. - Memory-core: retry disabled dreaming cron cleanup until cron is available after startup, so persisted managed dreaming jobs are removed after restart. Fixes #82383. (#82389) Thanks @neeravmakwana. - Providers/xAI: keep retired Grok 3, Grok 4 Fast, Grok 4.1 Fast, and Grok Code slugs out of model pickers while preserving compatibility resolution for existing configs. diff --git a/docs/tools/subagents.md b/docs/tools/subagents.md index 2ac6c297e337..45d5f8cc451e 100644 --- a/docs/tools/subagents.md +++ b/docs/tools/subagents.md @@ -99,7 +99,9 @@ requester chat when the run finishes. - `Result` — latest visible `assistant` reply text, otherwise sanitized latest tool/toolResult text. Terminal failed runs do not reuse captured reply text. - `Status` — `completed successfully` / `failed` / `timed out` / `unknown`. - Compact runtime/token stats. - - A delivery instruction telling the requester agent to rewrite in normal assistant voice (not forward raw internal metadata). + - A review instruction telling the requester agent to verify the result before deciding whether the original task is done. + - Follow-up guidance telling the requester agent to continue the task or record a follow-up when the child result leaves more action. + - A final-update instruction for the no-more-action path, written in normal assistant voice without forwarding raw internal metadata. diff --git a/src/agents/subagent-announce.format.e2e.test.ts b/src/agents/subagent-announce.format.e2e.test.ts index 2c7a4652234f..fab26925378c 100644 --- a/src/agents/subagent-announce.format.e2e.test.ts +++ b/src/agents/subagent-announce.format.e2e.test.ts @@ -511,8 +511,13 @@ describe("subagent announce formatting", () => { expect(msg).toContain(""); expect(msg).toContain("raw subagent reply"); expect(msg).toContain("Stats:"); - expect(msg).toContain("A completed subagent task is ready for user delivery."); - expect(msg).toContain("Convert the result above into your normal assistant voice"); + expect(msg).toContain("A completed subagent task is ready for parent review."); + expect(msg).toContain( + "Review/verify the result above before deciding whether the original task is done.", + ); + expect(msg).toContain( + "If additional action is required, continue the task or record a follow-up; otherwise send a truthful user-facing update.", + ); expect(msg).toContain("Keep this internal context private"); expect(call?.params?.internalEvents?.[0]?.type).toBe("task_completion"); expect(call?.params?.internalEvents?.[0]?.taskLabel).toBe("do thing"); @@ -697,7 +702,10 @@ describe("subagent announce formatting", () => { expect(msg).toContain("tokens 1.0k (in 12 / out 1.0k)"); expect(msg).toContain("prompt/cache 197.0k"); expect(msg).toContain("session_id: child-session-usage"); - expect(msg).toContain("A completed subagent task is ready for user delivery."); + expect(msg).toContain("A completed subagent task is ready for parent review."); + expect(msg).toContain( + "If additional action is required, continue the task or record a follow-up; otherwise send a truthful user-facing update.", + ); expect(msg).toContain( `Reply ONLY: ${SILENT_REPLY_TOKEN} if this exact result was already delivered to the user in this same turn.`, ); diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index f843cbb84963..83e5f721871b 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -88,9 +88,9 @@ function buildAnnounceReplyInstruction(params: { return `Convert this completion into a concise internal orchestration update for your parent agent in your own words. Keep this internal context private (don't mention system/log/stats/session details or announce type). If this result is duplicate or no update is needed, reply ONLY: ${SILENT_REPLY_TOKEN}.`; } if (params.expectsCompletionMessage) { - return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (don't mention system/log/stats/session details or announce type).`; + return `A completed ${params.announceType} is ready for parent review. Review/verify the result above before deciding whether the original task is done. If additional action is required, continue the task or record a follow-up; otherwise send a truthful user-facing update. Keep this internal context private (don't mention system/log/stats/session details or announce type).`; } - return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (don't mention system/log/stats/session details or announce type), and do not copy the internal event text verbatim. Reply ONLY: ${SILENT_REPLY_TOKEN} if this exact result was already delivered to the user in this same turn.`; + return `A completed ${params.announceType} is ready for parent review. Review/verify the result above before deciding whether the original task is done. If additional action is required, continue the task or record a follow-up; otherwise send a truthful user-facing update. Keep this internal context private (don't mention system/log/stats/session details or announce type), and do not copy the internal event text verbatim. Reply ONLY: ${SILENT_REPLY_TOKEN} if this exact result was already delivered to the user in this same turn.`; } function buildAnnounceSteerMessage(events: AgentInternalEvent[]): string {