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

@@ -16,7 +16,7 @@ import { DeepSeekHarness, type HarnessNotification } from '@deepseek-ai/dsh-sdk-
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
import type { SubagentResult, SubagentRun, SubagentStartRequest, SubagentStopReason } from '@deepseek-ai/dsh-subagent'
import { assistantMessageOutput, settleRunResult, subprocessRunHandle } from '@deepseek-ai/dsh-subagent'
import { AssistantOutputFold, settleRunResult, subprocessRunHandle } from '@deepseek-ai/dsh-subagent'
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
/** Resolved spawn spec for an SDK runtime child process (no defaults — see Config). */
@@ -163,28 +163,14 @@ export async function startSdkRun(request: SubagentStartRequest, spec: SdkRunSpe
}
const childSessionId = `session-${randomUUID().replaceAll('-', '')}`
// The child's final answer, folded incrementally under the seam's canonical
// rule (`finalAssistantOutput`): the last NON-EMPTY complete assistant
// message when one exists, else the text streamed so far (a partial answer
// surviving cancel). An empty-content message hosts only usage (a max-tokens
// step that assembled no text blocks), so it never erases streamed text.
let lastMessage: ContentBlock[] | undefined
const partial: string[] = []
// The child's final answer under the seam's canonical selection rule
// (`AssistantOutputFold`); a partial answer survives cancel and error paths.
const fold = new AssistantOutputFold()
const observe = (notification: HarnessNotification): void => {
if (notification.method !== 'session.event' || notification.params.sessionId !== childSessionId) return
const event = notification.params.event as SessionEvent
const content = assistantMessageOutput(event)
if (content !== undefined) {
lastMessage = content
} else if (event.type === 'assistant/chunk' && event.data.chunk.type === 'text-delta') {
partial.push(event.data.chunk.text)
}
}
const collectOutput = (): ContentBlock[] => {
if (lastMessage !== undefined) return lastMessage
const text = partial.join('')
return text.length > 0 ? [{ type: 'text', text }] : []
fold.push(notification.params.event as SessionEvent)
}
const collectOutput = (): ContentBlock[] => fold.collect() ?? []
// Race the child turn against local cancellation; the shared settlement
// flattens failures under the seam's never-reject contract.