Merge branch 'codex/canonical-tool-output' into codex/code-mode-typed-results

# Conflicts:
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl
This commit is contained in:
Tianyi Cui
2026-07-22 00:29:00 +08:00
252 changed files with 11596 additions and 6037 deletions

View File

@@ -12,7 +12,7 @@ The driver follows this sequence:
2. Call `parent.ctx.agents.create` directly, passing the required request signal into the factory's creation transaction.
3. During that transaction's unpublished setup window, install the requested persona, tool restriction, and structured-output runtime.
4. Publish the child, retain the returned `AgentHandle`, and drive one task with `child.send(prompt)` followed by `child.whenIdle()`.
5. Read the child's own last assistant message and terminal turn reason, excluding any fork seed.
5. Read the child's own last assistant message and latest message-triggered turn reason, excluding any fork seed and later plugin-owned zero-step turns.
The child gets the parent's working-directory/session lineage and inherits the parent model unless `request.agentOptions` overrides it. It gets a fresh flat registration scope: parent ownership does not import parent tool restrictions or establish an authority subset.

View File

@@ -10,7 +10,7 @@
import { randomUUID } from 'node:crypto'
import type { Context } from 'cordis'
import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent'
import { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
import { findLastMessageTurnEnd, SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { assertSubagentMaxDepth, delegationDepthOf } from '@deepseek-ai/dsh-subagent'
import type { SubagentResult, SubagentRun, SubagentStartRequest, SubagentStopReason } from '@deepseek-ai/dsh-subagent'
@@ -175,7 +175,7 @@ function readResult(
): SubagentResult {
const own = child.session.events.slice(seedLength)
const lastMessage = own.findLast((event): event is SessionEvent<'assistant/message'> => event.type === 'assistant/message')
const lastEnd = own.findLast((event): event is SessionEvent<'turn/end'> => event.type === 'turn/end')
const lastEnd = findLastMessageTurnEnd(own)
const output: ContentBlock[] = lastMessage?.data.content ?? []
const recorded = toStopReason(lastEnd?.data.reason)
// Disposal can tear the owner down before the loop records its ordinary

View File

@@ -9,7 +9,7 @@ import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService from '@deepseek-ai/dsh-subagent'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { maxTokensResponse, MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { startInProcessRun } from '../src/index.ts'
type Script = ConstructorParameters<typeof MockAdapter>[0]
@@ -55,6 +55,36 @@ describe('startInProcessRun', () => {
expect(ctx.agents.get(run.id)).toBeUndefined()
})
it('reports the message-turn outcome when a later non-message turn completes during flush', async () => {
const { ctx, parent } = await setup([maxTokensResponse('partial answer')])
let injected = false
ctx.on('session/flush', (session) => {
if (injected || session.header.parentSession === undefined) return
const lastEnd = session.events.findLast(event => event.type === 'turn/end')
if (lastEnd?.type !== 'turn/end' || lastEnd.data.reason.kind !== 'max-tokens') return
injected = true
const turn = lastEnd.data.turn + 1
session.append('turn/start', {
turn,
trigger: { kind: 'injection', source: { kind: 'plugin', plugin: 'late-metadata' } },
})
session.append('context/message', {
content: [{ type: 'text', text: 'late metadata' }],
source: { kind: 'plugin', plugin: 'late-metadata' },
}, { surfaceOp: 'append' })
session.append('turn/end', { turn, reason: { kind: 'completed' } })
})
const run = await startInProcessRun(request(parent), {})
const result = await run.result
const child = ctx.agents.get(run.id)!
expect(child.session.events.findLast(event => event.type === 'turn/end'))
.toMatchObject({ data: { reason: { kind: 'completed' } } })
expect(result.stopReason).toBe('max-tokens')
await run.dispose()
})
it('seeds a forked child but reads only the child-owned output', async () => {
const { ctx, parent } = await setup([textResponse('parent answer'), textResponse('child answer')])
parent.send([{ type: 'text', text: 'parent question' }])