From d6942ec5d50f7bf8e9cce96475c3bc47395edfdb Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Sat, 30 May 2026 00:03:44 +0800 Subject: [PATCH] fix(agent-core): drop subagent summary-continuation re-prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The summary-continuation pass re-prompted any subagent whose first summary was under 200 chars to "expand" it, then read back the follow-up turn — replacing the original output rather than appending. For swarm's structured-output subagents this was harmful: a reviser's compact decision JSON (e.g. {"kind":"retry"}) is always under the threshold, so the expand turn always fired and could replace the JSON with prose, silently degrading the recovery loop into conservative drops. It also taxed every short-but-complete handoff with an extra turn. Remove the heuristic entirely so a subagent's first summary is returned as-is. The max-tokens truncation guard is unaffected. --- .../agent-core/src/session/subagent-host.ts | 22 +------------------ .../src/session/summary-continuation.md | 5 ----- .../test/session/subagent-host.test.ts | 18 +++++++-------- 3 files changed, 9 insertions(+), 36 deletions(-) delete mode 100644 packages/agent-core/src/session/summary-continuation.md diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index 58ccc5c1e..b797ad185 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -12,15 +12,7 @@ import { import { linkAbortSignal } from '../utils/abort'; import { collectGitContext } from './git-context'; import type { Session } from './index'; -import SUMMARY_CONTINUATION_PROMPT from './summary-continuation.md'; -/** - * A subagent summary shorter than this many characters triggers one - * follow-up turn that asks the subagent to expand it, so the parent - * agent receives a technically complete handoff. - */ -const SUMMARY_MIN_LENGTH = 200; -const SUMMARY_CONTINUATION_ATTEMPTS = 1; const HOOK_TEXT_PREVIEW_LENGTH = 500; const SUBAGENT_MAX_TOKENS_ERROR = 'Subagent turn failed before completing its final summary: reason=max_tokens'; @@ -258,19 +250,7 @@ export class SessionSubagentHost { child.turn.prompt([{ type: 'text', text: childPrompt }], origin); await runChildTurnToCompletion(child, options.signal); - // A subagent that returns an overly terse summary leaves the parent - // agent under-informed. Give it a bounded number of chances to expand - // the handoff; if it is still short after that, accept it as-is rather - // than retrying indefinitely. - let result = lastAssistantText(child); - let remainingContinuations = SUMMARY_CONTINUATION_ATTEMPTS; - while (remainingContinuations > 0 && result.length < SUMMARY_MIN_LENGTH) { - remainingContinuations -= 1; - options.signal.throwIfAborted(); - child.turn.prompt([{ type: 'text', text: SUMMARY_CONTINUATION_PROMPT }], origin); - await runChildTurnToCompletion(child, options.signal); - result = lastAssistantText(child); - } + const result = lastAssistantText(child); const usage = child.usage.data().total; parent.emitEvent({ type: 'subagent.completed', diff --git a/packages/agent-core/src/session/summary-continuation.md b/packages/agent-core/src/session/summary-continuation.md deleted file mode 100644 index 8efb589a5..000000000 --- a/packages/agent-core/src/session/summary-continuation.md +++ /dev/null @@ -1,5 +0,0 @@ -Your previous response was too brief. Please provide a more comprehensive summary that includes: - -1. Specific technical details and implementations -2. Detailed findings and analysis -3. All important information that the parent agent should know \ No newline at end of file diff --git a/packages/agent-core/test/session/subagent-host.test.ts b/packages/agent-core/test/session/subagent-host.test.ts index dfb92d70a..fde45abcc 100644 --- a/packages/agent-core/test/session/subagent-host.test.ts +++ b/packages/agent-core/test/session/subagent-host.test.ts @@ -435,15 +435,17 @@ describe('SessionSubagentHost', () => { ); }); - it('re-prompts the child when the first summary is too short', async () => { + it('returns a short summary as-is without re-prompting the child', async () => { const parent = testAgent(); parent.configure(); parent.newEvents(); - const longSummary = 'Detailed findings: '.repeat(20); + const shortSummary = 'done'; const child = testAgent(); - child.mockNextResponse({ type: 'text', text: 'done' }); - child.mockNextResponse({ type: 'text', text: longSummary }); + child.mockNextResponse({ type: 'text', text: shortSummary }); + // A second response is queued to prove it is never consumed: a short + // summary must NOT trigger a follow-up "expand" turn. + child.mockNextResponse({ type: 'text', text: 'Detailed findings: '.repeat(20) }); const session = fakeSession(parent.agent, child.agent); const host = new SessionSubagentHost(session, 'main'); @@ -455,12 +457,8 @@ describe('SessionSubagentHost', () => { signal, }); - await expect(handle.completion).resolves.toMatchObject({ result: longSummary.trim() }); - expect(child.llmCalls).toHaveLength(2); - expect(child.llmCalls[1]?.history.at(-1)).toMatchObject({ - role: 'user', - content: [{ type: 'text', text: expect.stringContaining('too brief') }], - }); + await expect(handle.completion).resolves.toMatchObject({ result: shortSummary }); + expect(child.llmCalls).toHaveLength(1); }); it('fails the child instead of re-prompting when the response is truncated', async () => {