fix: address codex review round 2
- Wire the control service and send_message tool into every shipped composition with a resumable provider and background enabled (headless-agent, tui-agent, and the SDK helper's subagent feature base resources); jsonrpc-agent disables background and is unchanged. - Resolve the send_message availability check in the CALLER's tool scope so a restriction that removes the follow-up tool from one agent also blocks that agent's continuable start. - Control-service disposal now cancels live activations and awaits producer settlement instead of stranding them: TaskService keeps producer Tasks across a reload, so the disposing service aborts each activation-owned controller, resolves its terminal gate (the effect-scoped onTaskDone listener is already gone), and awaits done. A new test kills a mid-start activation through HMR disposal.
This commit is contained in:
12 files changed
+136
-9
No files matched your search
@@ -482,6 +482,51 @@ describe('SubagentControlService.sendMessage', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('service disposal with live activations', () => {
|
||||
it('cancels and settles a starting activation on service disposal instead of stranding it', async () => {
|
||||
const ctx = new Context()
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-subagent-control-hmr-'))
|
||||
roots.push(root)
|
||||
await ctx.plugin(JsonlSessionPersistence, { root })
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(TaskService)
|
||||
await ctx.plugin(ToolTasks, {})
|
||||
// A provider that stays pending until its signal aborts, so the activation
|
||||
// is observably mid-start when the control service is disposed.
|
||||
let sawAbort = false
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'pending',
|
||||
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
|
||||
inheritsParentContext: false,
|
||||
start: request => new Promise((_resolve, reject) => {
|
||||
request.signal.addEventListener('abort', () => {
|
||||
sawAbort = true
|
||||
reject(new Error('startup aborted'))
|
||||
}, { once: true })
|
||||
}),
|
||||
resume: () => Promise.reject(new Error('unreachable')),
|
||||
})
|
||||
const controlFiber = await ctx.plugin(SubagentControlService)
|
||||
ctx.llm.registerAdapter(['mock'], new MockAdapter([]))
|
||||
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
const control = ctx.get('subagentControl')!
|
||||
const started = control.startContinuable({
|
||||
provider: 'pending',
|
||||
label: 'will be interrupted',
|
||||
request: { prompt: message('go'), parent },
|
||||
})
|
||||
// TaskService keeps the producer Task; the disposing control service must
|
||||
// cancel its activation and await settlement rather than strand it.
|
||||
await controlFiber.dispose()
|
||||
expect(sawAbort).toBe(true)
|
||||
const snapshot = await waitTerminal(ctx, started.taskId, parent)
|
||||
expect(snapshot.status).toBe('killed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('outcome mapping helpers', () => {
|
||||
it('runOutcome maps the stop-reason vocabulary onto task outcomes', () => {
|
||||
const output = [{ type: 'text' as const, text: 'partial' }]
|
||||
|
||||
Reference in New Issue
Block a user