review: one fold implementation, uniform end-edge absence, partial text in tool errors, snapshot scenario

Address ds-review-bot on #2127:

- assistant-output: the rule has ONE implementation, the incremental
  AssistantOutputFold (push/pushText/collect); finalAssistantOutput folds a
  complete suffix, the SDK backend folds notification events, and the ACP
  backend folds raw chunk text into the same streamed fallback.
- subagent/end.lastAssistantMessage: 'no output' is encoded once — absent,
  never [], on both lifecycle shapes (observeRun now omits empty output).
- tool-subagent: a non-completed foreground result stays isError but appends
  the child's preserved partial text after the stop-reason headline.
- Authored keyless snapshot scenario subagent-max-tokens-partial pins the
  assembled transcript: the child's committed log carries the usage-only
  empty message and the parent's tool result carries the partial answer.
- Rule-boundary sentence (message wins over later streamed text) and the
  consumer half recorded in the Agent Note; comments trimmed to pointers.
This commit is contained in:
Hypatia May
2026-08-10 16:09:04 +08:00
parent 2418f44781
commit ea47c32805
24 changed files with 221 additions and 96 deletions

View File

@@ -134,6 +134,21 @@ function stopReasonError(result: SubagentResult): string | undefined {
}
}
/**
* Append the child's preserved partial answer to a stop-reason error so a
* truncated or cancelled child's real text still reaches the parent model.
* @param error - the stop-reason headline.
* @param output - the child's selected output (`SubagentResult.output`).
* @returns the headline, extended with the partial text when any exists.
*/
function withPartialText(error: string, output: ContentBlock[]): string {
const text = output
.filter((block): block is Extract<ContentBlock, { type: 'text' }> => block.type === 'text')
.map(block => block.text)
.join('')
return text.length === 0 ? error : `${error}\nPartial output before the run ended:\n${text}`
}
type ForegroundToolResult = {
readonly kind: 'foreground'
readonly runId: SubagentRun['id']
@@ -149,8 +164,9 @@ async function settleForegroundRun(run: SubagentRun): Promise<ForegroundToolResu
run.result.then((result): ForegroundToolResult => {
const error = stopReasonError(result)
if (error !== undefined) {
// The registry converts this throw to isError; partial output is not success.
throw new Error(error)
// The registry converts this throw to isError; partial output is not
// success, but the preserved partial answer still reaches the parent.
throw new Error(withPartialText(error, result.output))
}
return {
kind: 'foreground',