Merge master into codex/session-title

This commit is contained in:
Tianyi Cui
2026-07-21 22:59:41 +08:00
197 changed files with 3343 additions and 1227 deletions

View File

@@ -135,7 +135,7 @@ export async function startInProcessRun(
const onAbort = (): void => {
flags.cancelled = true
child.cancel('subagent request aborted')
child.cancel({ kind: 'parent' })
}
request.signal.addEventListener('abort', onAbort, { once: true })

View File

@@ -96,7 +96,7 @@ export function attachStructuredRuntime(childCtx: Context, schema: StructuredOut
// Stop the child's turn once its output is captured. This monotonic serial
// checkpoint runs after the ordinary continuation waterfall, its reason,
// and late-steering folding, so no ordering trick can resume a finished run.
childCtx.on('agent/turn-stop', function (this: unknown): ContinuationStop | undefined {
childCtx.on('agent/turn-stop', function (this: unknown, _agent, _turn, _signal): ContinuationStop | undefined {
return captured === undefined ? undefined : { action: 'stop' }
})

View File

@@ -19,6 +19,8 @@ import {
STRUCTURED_OUTPUT_TOOL,
} from '../src/structured.ts'
const testToolSignal = new AbortController().signal
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
@@ -225,7 +227,7 @@ describe('in-process structured output', () => {
ctx.on('agent/session-start', (child) => {
if (child === parent) return
wrapperInstalled = true
child.ctx.on('agent/turn-continuation', async (_subject, _turn, _decision, next): Promise<ContinuationDecision> => {
child.ctx.on('agent/turn-continuation', async (_subject, _turn, _decision, _signal, next): Promise<ContinuationDecision> => {
const downstream = await next()
expect(downstream).toEqual({ action: 'stop' })
return { action: 'continue' }
@@ -251,7 +253,7 @@ describe('in-process structured output', () => {
const run = await ctx.subagents.start('spawn', structuredRequest(parent))
ctx.on('agent/session-start', (child) => {
if (child.id !== run.id) return
child.ctx.on('agent/turn-continuation', async (subject, _turn, _decision, next): Promise<ContinuationDecision> => {
child.ctx.on('agent/turn-continuation', async (subject, _turn, _decision, _signal, next): Promise<ContinuationDecision> => {
const downstream = await next()
expect(downstream).toEqual({ action: 'stop' })
subject.steer([{ type: 'text', text: 'late steering after downstream stop' }])
@@ -650,6 +652,7 @@ describe('in-process structured output', () => {
it('a structured_output call from an agent WITHOUT a structured run is UNKNOWN_TOOL (the tool does not exist for it)', async () => {
const { ctx, parent } = await setup([])
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: 'x' as never,
name: STRUCTURED_OUTPUT_TOOL,
arguments: { answer: 1 },
@@ -662,6 +665,7 @@ describe('in-process structured output', () => {
it('a structured_output call with NO calling agent at all is UNKNOWN_TOOL', async () => {
const { ctx } = await setup([])
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: 'x' as never,
name: STRUCTURED_OUTPUT_TOOL,
arguments: { answer: 1 },
@@ -694,6 +698,7 @@ describe('in-process structured output', () => {
// …and a LATER invalid call (its own body staged nothing) must not
// resurrect c1's discarded value: drive the pipeline directly.
const invalid = await ctx.tools.execute({
signal: testToolSignal,
callId: 'c2' as never,
name: STRUCTURED_OUTPUT_TOOL,
arguments: { answer: 'not-a-number' },
@@ -702,6 +707,7 @@ describe('in-process structured output', () => {
expect(invalid.isError).toBe(true)
// A fresh valid call still captures ITS OWN value.
const valid = await ctx.tools.execute({
signal: testToolSignal,
callId: 'c3' as never,
name: STRUCTURED_OUTPUT_TOOL,
arguments: { answer: 9 },
@@ -732,6 +738,7 @@ describe('in-process structured output', () => {
// (invalid args throw before the stage): the discarded value must not ride
// its acceptance.
const reused = await ctx.tools.execute({
signal: testToolSignal,
callId: 'c1' as never,
name: STRUCTURED_OUTPUT_TOOL,
arguments: { answer: 'not-a-number' },
@@ -740,6 +747,7 @@ describe('in-process structured output', () => {
expect(reused.isError).toBe(true)
// Nothing was ever committed: a fresh valid call is still required.
const valid = await ctx.tools.execute({
signal: testToolSignal,
callId: 'c1' as never,
name: STRUCTURED_OUTPUT_TOOL,
arguments: { answer: 5 },
@@ -774,6 +782,7 @@ describe('in-process structured output', () => {
return undefined as never
}, { prepend: true })
const denied = await ctx.tools.execute({
signal: testToolSignal,
callId: 'c1' as never,
name: STRUCTURED_OUTPUT_TOOL,
arguments: { answer: 2 },
@@ -784,6 +793,7 @@ describe('in-process structured output', () => {
// The discarded value was never promoted: a fresh valid call is required
// (and succeeds, proving the runtime is not wedged).
const valid = await ctx.tools.execute({
signal: testToolSignal,
callId: 'c1' as never,
name: STRUCTURED_OUTPUT_TOOL,
arguments: { answer: 5 },

View File

@@ -27,9 +27,10 @@ async function setup(script: Script) {
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
const adapter = new MockAdapter(script)
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
return { ctx, parent }
return { ctx, parent, adapter }
}
function request(parent: Agent, signal = new AbortController().signal) {
@@ -163,12 +164,16 @@ describe('startInProcessRun', () => {
})
it('uses the request signal after publication and dispose as cancellation paths', async () => {
const { parent } = await setup(['hang', 'hang'])
const { parent, adapter } = await setup(['hang', 'hang'])
const controller = new AbortController()
const signalled = await startInProcessRun(request(parent, controller.signal), {})
await new Promise(resolve => setTimeout(resolve, 30))
controller.abort('stop child')
await expect(signalled.result).resolves.toMatchObject({ stopReason: 'aborted' })
expect(adapter.requests[0]?.signal?.reason).toEqual({ kind: 'parent' })
const child = parent.ctx.agents.get(signalled.id)
const turnEnd = child?.session.events.findLast(event => event.type === 'turn/end')
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason).toEqual({ kind: 'aborted' })
await signalled.dispose()
const disposed = await startInProcessRun(request(parent), {})

View File

@@ -285,9 +285,6 @@ export function apply(ctx: Context, config: Config): void {
if (tasks === undefined) {
throw new Error('background tasks unavailable: load @deepseek-ai/dsh-tasks and @deepseek-ai/dsh-tool-tasks')
}
// Reject cancellation before spawning; after return, the task-owned
// signal covers both pending startup and the ready child.
if (exec.signal?.aborted) throw new Error('subagent delegation aborted')
// Task preflight finishes before the starter can spawn a child.
const id = tasks.start({
kind: 'subagent',
@@ -315,7 +312,7 @@ export function apply(ctx: Context, config: Config): void {
config,
args.prompt,
parent,
exec.signal ?? new AbortController().signal,
exec.signal,
)
const run: SubagentRun = await ctx.subagents.start(config.provider, request)

View File

@@ -3,7 +3,7 @@ import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import { type Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
@@ -15,6 +15,8 @@ import * as tool from '../src/index.ts'
import { runOutcome, settleRun } from '../src/index.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
const testToolSignal = new AbortController().signal
/**
* Drives the REAL plugin body: mounts `dsh-tool-subagent` on a real
* `ToolRegistry` + `SubagentService`, with a package-local scripted child
@@ -45,6 +47,7 @@ function callSubagent(ctx: Context, args: unknown, over: { agent?: Agent | undef
// exactOptionalPropertyTypes the key is omitted rather than set to undefined.
const agent = 'agent' in over ? over.agent : fakeAgent()
return ctx.tools.execute({
signal: testToolSignal,
callId: CallId(`call-${++callCounter}`),
name: 'subagent',
arguments: args,
@@ -100,11 +103,13 @@ describe('dsh-tool-subagent', () => {
it('keeps foreground and background calls exclusive', async () => {
const ctx = await setup({ provider: 'mock' })
expect(ctx.tools.executionMode({
signal: testToolSignal,
callId: CallId('subagent-foreground'),
name: 'subagent',
arguments: { description: 'do work', prompt: 'Reply OK' },
})).toEqual({ kind: 'exclusive' })
expect(ctx.tools.executionMode({
signal: testToolSignal,
callId: CallId('subagent-background'),
name: 'subagent',
arguments: { description: 'do work', prompt: 'Reply OK', run_in_background: true },
@@ -139,8 +144,8 @@ describe('dsh-tool-subagent', () => {
const names = ctx.tools.schemas().map(s => s.name).filter(n => n.startsWith('subagent')).sort()
expect(names).toEqual(['subagent', 'subagent_acp'])
const viaSpawn = await ctx.tools.execute({ callId: CallId('c-spawn'), name: 'subagent', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() })
const viaAcp = await ctx.tools.execute({ callId: CallId('c-acp'), name: 'subagent_acp', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() })
const viaSpawn = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c-spawn'), name: 'subagent', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() })
const viaAcp = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c-acp'), name: 'subagent_acp', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() })
expect(text(viaSpawn)).toBe('from spawn')
expect(text(viaAcp)).toBe('from acp')
})
@@ -418,7 +423,7 @@ describe('dsh-tool-subagent', () => {
expect(result.isError).toBe(true)
})
it('passes an already-aborted signal so provider startup rejects', async () => {
it('skips provider startup for an already-aborted signal', async () => {
const sawAborted = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
@@ -438,8 +443,9 @@ describe('dsh-tool-subagent', () => {
const controller = new AbortController()
controller.abort() // already aborted BEFORE the tool runs
const result = await callSubagent(ctx, { description: 'd', prompt: 'p' }, { signal: controller.signal })
expect(sawAborted).toHaveBeenCalledTimes(1)
expect(sawAborted).not.toHaveBeenCalled()
expect(result.isError).toBe(true)
expect(result.error).toEqual({ name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH })
})
it('tools depend on the service: no `subagent` tool without ctx.subagents', async () => {
@@ -640,6 +646,7 @@ describe('dsh-tool-subagent background mode', () => {
expect(text(start)).toBe('started background subagent task subagent-1')
const collected = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('collect-1'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
@@ -649,6 +656,7 @@ describe('dsh-tool-subagent background mode', () => {
// Final-output reads are idempotent (not consumed).
const again = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('collect-2'),
name: 'task_output',
arguments: { task_id: 'subagent-1' },
@@ -664,14 +672,15 @@ describe('dsh-tool-subagent background mode', () => {
expect(text(result)).toContain('background tasks unavailable: load @deepseek-ai/dsh-tasks')
})
it('refuses to start when the tool signal is already aborted', async () => {
it('skips background startup when the tool signal is already aborted', async () => {
const ctx = await backgroundSetup({ provider: 'mock' })
const parent = ownerAgent(ctx, 'sess-parent')
const controller = new AbortController()
controller.abort()
const result = await callSubagent(ctx, { description: 'd', prompt: 'p', run_in_background: true }, { agent: parent, signal: controller.signal })
expect(result.isError).toBe(true)
expect(text(result)).toContain('subagent delegation aborted')
expect(result.error).toEqual({ name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH })
expect(text(result)).toBe('Error: tool call aborted before dispatch')
})
it('settles an asynchronous provider-start failure as a failed task', async () => {
@@ -686,6 +695,7 @@ describe('dsh-tool-subagent background mode', () => {
tool.apply(ctx, { provider: 'broken-start', toolName: 'subagent_broken' })
const started = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-start'),
name: 'subagent_broken',
arguments: { description: 'broken', prompt: 'p', run_in_background: true },
@@ -693,6 +703,7 @@ describe('dsh-tool-subagent background mode', () => {
})
expect(text(started)).toBe('started background subagent task subagent-1')
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-output'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
@@ -715,18 +726,21 @@ describe('dsh-tool-subagent background mode', () => {
tool.apply(ctx, { provider: 'pending-start', toolName: 'subagent_pending' })
await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('pending-start'),
name: 'subagent_pending',
arguments: { description: 'pending', prompt: 'p', run_in_background: true },
agent: parent,
})
await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('pending-kill'),
name: 'task_kill',
arguments: { task_id: 'subagent-1', reason: 'no longer needed' },
agent: parent,
})
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('pending-output'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
@@ -764,19 +778,19 @@ describe('dsh-tool-subagent background mode', () => {
// Direct apply preserves omitted agentOptions instead of applying schema defaults.
tool.apply(ctx, { provider: 'hanging', toolName: 'subagent_hang' })
const startOne = await ctx.tools.execute({ callId: CallId('h1'), name: 'subagent_hang', arguments: { description: 'one', prompt: 'p', run_in_background: true }, agent: parent })
const startTwo = await ctx.tools.execute({ callId: CallId('h2'), name: 'subagent_hang', arguments: { description: 'two', prompt: 'p', run_in_background: true }, agent: parent })
const startOne = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h1'), name: 'subagent_hang', arguments: { description: 'one', prompt: 'p', run_in_background: true }, agent: parent })
const startTwo = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h2'), name: 'subagent_hang', arguments: { description: 'two', prompt: 'p', run_in_background: true }, agent: parent })
expect(text(startOne)).toBe('started background subagent task subagent-1')
expect(text(startTwo)).toBe('started background subagent task subagent-2')
const withReason = await ctx.tools.execute({ callId: CallId('k1'), name: 'task_kill', arguments: { task_id: 'subagent-1', reason: 'superseded' }, agent: parent })
const withoutReason = await ctx.tools.execute({ callId: CallId('k2'), name: 'task_kill', arguments: { task_id: 'subagent-2' }, agent: parent })
const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k1'), name: 'task_kill', arguments: { task_id: 'subagent-1', reason: 'superseded' }, agent: parent })
const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k2'), name: 'task_kill', arguments: { task_id: 'subagent-2' }, agent: parent })
expect(text(withReason)).toBe('requested cancellation of task subagent-1')
expect(text(withoutReason)).toBe('requested cancellation of task subagent-2')
expect(cancels).toEqual(['superseded', 'background subagent task killed'])
// The aborted children settle as killed tasks.
const killed = await ctx.tools.execute({ callId: CallId('w1'), name: 'task_output', arguments: { task_id: 'subagent-1', wait: true }, agent: parent })
const killed = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('w1'), name: 'task_output', arguments: { task_id: 'subagent-1', wait: true }, agent: parent })
expect(text(killed)).toBe('(no new output)\n[status: killed]')
})
@@ -870,6 +884,7 @@ describe('background preflight failure (no orphaned child, by construction)', ()
tool.apply(ctx, { provider: 'probe', toolName: 'subagent_probe' })
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('probe-1'),
name: 'subagent_probe',
arguments: { description: 'd', prompt: 'p', run_in_background: true },