fix(subagent): preserve follow-up provenance
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
The globally named `send_message` tool: a thin adapter over `ctx.subagentControl.sendMessage()`. Provider-bound `@deepseek-ai/dsh-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers the one shared control tool, so multiple delegation tools never register duplicate global controls.
|
||||
|
||||
The tool performs no lifecycle routing. The control service decides between live delivery to the running activation's existing Task and a fresh Task that cold-resumes the durable child; the tool renders which route was taken and the relevant Task id. A control-service throw becomes an errored tool result stating the message was not delivered.
|
||||
The tool performs no lifecycle routing. It attributes every follow-up as `{ kind: 'coordinator', senderSessionId: parent.id }`; the control service preserves that source while deciding between live delivery to the running activation's existing Task and a fresh Task that cold-resumes the durable child. The tool renders which route was taken and the relevant Task id. A control-service throw becomes an errored tool result stating the message was not delivered.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -67,7 +67,12 @@ export function apply(ctx: Context): void {
|
||||
throw new Error('send_message requires a calling agent (exec.agent was undefined)')
|
||||
}
|
||||
const message: ContentBlock[] = [{ type: 'text', text: args.message }]
|
||||
const result = ctx.subagentControl.sendMessage(parent, SessionId(args.subagent_id), message)
|
||||
const result = ctx.subagentControl.sendMessage(
|
||||
parent,
|
||||
SessionId(args.subagent_id),
|
||||
message,
|
||||
{ kind: 'coordinator', senderSessionId: parent.id },
|
||||
)
|
||||
return Promise.resolve(result)
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -83,17 +83,27 @@ describe('dsh-tool-subagent-control', () => {
|
||||
expect(text(result)).toBe(`message started task subagent-2 continuing subagent ${started.childId}`)
|
||||
const collected = await callTool(ctx, 'task_output', { task_id: 'subagent-2', wait: true }, parent)
|
||||
expect(text(collected)).toBe('second answer\n[status: completed]')
|
||||
const loaded = await ctx.sessionPersistence.load(started.childId)
|
||||
const followUp = loaded.events.findLast(event =>
|
||||
event.type === 'user/message',
|
||||
)
|
||||
expect(followUp?.type === 'user/message' && followUp.data.source).toEqual({
|
||||
kind: 'coordinator',
|
||||
senderSessionId: parent.id,
|
||||
})
|
||||
})
|
||||
|
||||
it('renders the steered route when the child is still running', async () => {
|
||||
// Script the child's single turn as two steps: the steer joins mid-turn.
|
||||
const { ctx, parent } = await setup([])
|
||||
let steered: string | undefined
|
||||
let source: unknown
|
||||
// Reach past the tool into the control service to fake a running route
|
||||
// deterministically: the tool is a thin adapter, so its steered wording is
|
||||
// what this test pins.
|
||||
ctx.subagentControl.sendMessage = (agent, _childId, message) => {
|
||||
ctx.subagentControl.sendMessage = (agent, _childId, message, messageSource) => {
|
||||
steered = (message[0] as { text: string }).text
|
||||
source = messageSource
|
||||
return { route: 'steered', taskId: ctx.tasks.list(agent)[0]?.id ?? ('subagent-9' as never) }
|
||||
}
|
||||
const result = await callTool(ctx, 'send_message', {
|
||||
@@ -102,6 +112,7 @@ describe('dsh-tool-subagent-control', () => {
|
||||
}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(steered).toBe('also consider Y')
|
||||
expect(source).toEqual({ kind: 'coordinator', senderSessionId: parent.id })
|
||||
expect(text(result)).toBe('message delivered to running task subagent-9')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user