fix(agent-core): drop subagent summary-continuation re-prompt

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.
This commit is contained in:
Kaiyi 2026-05-30 00:03:44 +08:00
parent df04b8d2fb
commit d6942ec5d5
3 changed files with 9 additions and 36 deletions

View file

@ -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',

View file

@ -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

View file

@ -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 () => {