Merge branch 'worktree-subagent-seam-pr2.5' into worktree-subagent-seam-pr3
This commit is contained in:
@@ -42,8 +42,10 @@
|
|||||||
a fresh child agent (it works in its own context and returns only its
|
a fresh child agent (it works in its own context and returns only its
|
||||||
final result) — give it a complete, standalone instruction.
|
final result) — give it a complete, standalone instruction.
|
||||||
|
|
||||||
# The subagent seam + both in-process backends + the model-facing `subagent`
|
# The subagent seam + both in-process backends + two model-facing tools —
|
||||||
# tool — identical to cordis.yml's wiring (only the LLM backend differs above).
|
# identical to cordis.yml's wiring (only the LLM backend differs above): spawn
|
||||||
|
# and fork are each reachable via a dsh-tool-subagent bound to it with a distinct
|
||||||
|
# toolName (subagent → spawn, subagent_fork → fork).
|
||||||
- id: subagent
|
- id: subagent
|
||||||
name: '@deepseek-ai/dsh-subagent'
|
name: '@deepseek-ai/dsh-subagent'
|
||||||
|
|
||||||
@@ -61,3 +63,10 @@
|
|||||||
name: '@deepseek-ai/dsh-tool-subagent'
|
name: '@deepseek-ai/dsh-tool-subagent'
|
||||||
config:
|
config:
|
||||||
provider: spawn
|
provider: spawn
|
||||||
|
toolName: subagent
|
||||||
|
|
||||||
|
- id: tool-subagent-fork
|
||||||
|
name: '@deepseek-ai/dsh-tool-subagent'
|
||||||
|
config:
|
||||||
|
provider: fork
|
||||||
|
toolName: subagent_fork
|
||||||
|
|||||||
@@ -51,10 +51,12 @@
|
|||||||
a fresh child agent (it works in its own context and returns only its
|
a fresh child agent (it works in its own context and returns only its
|
||||||
final result) — give it a complete, standalone instruction.
|
final result) — give it a complete, standalone instruction.
|
||||||
|
|
||||||
# The subagent seam + both in-process backends + the model-facing `subagent`
|
# The subagent seam + both in-process backends + two model-facing tools, as leaf
|
||||||
# tool, as leaf entries after the app (which provides ctx.agents/ctx.tools). The
|
# entries after the app (which provides ctx.agents/ctx.tools). spawn (a fresh
|
||||||
# tool is bound to the `spawn` backend (a fresh child); the `fork` backend is
|
# child) and fork (a child seeded with the parent's completed-turn prefix) are
|
||||||
# loaded too so a multi-child scenario can exercise both transports.
|
# both reachable by the model: dsh-tool-subagent is loaded once per backend with
|
||||||
|
# a distinct toolName (subagent → spawn, subagent_fork → fork), so a multi-child
|
||||||
|
# scenario can exercise both transports.
|
||||||
- id: subagent
|
- id: subagent
|
||||||
name: '@deepseek-ai/dsh-subagent'
|
name: '@deepseek-ai/dsh-subagent'
|
||||||
|
|
||||||
@@ -72,3 +74,10 @@
|
|||||||
name: '@deepseek-ai/dsh-tool-subagent'
|
name: '@deepseek-ai/dsh-tool-subagent'
|
||||||
config:
|
config:
|
||||||
provider: spawn
|
provider: spawn
|
||||||
|
toolName: subagent
|
||||||
|
|
||||||
|
- id: tool-subagent-fork
|
||||||
|
name: '@deepseek-ai/dsh-tool-subagent'
|
||||||
|
config:
|
||||||
|
provider: fork
|
||||||
|
toolName: subagent_fork
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ export function apply(ctx: Context, config: Config): void {
|
|||||||
// aborted while the child is in flight, cancel the child too.
|
// aborted while the child is in flight, cancel the child too.
|
||||||
const onAbort = (): void => { run.cancel('parent step aborted') }
|
const onAbort = (): void => { run.cancel('parent step aborted') }
|
||||||
exec.signal?.addEventListener('abort', onAbort, { once: true })
|
exec.signal?.addEventListener('abort', onAbort, { once: true })
|
||||||
|
// `addEventListener` does NOT fire for a signal already aborted before this
|
||||||
|
// line, so a step cancelled before the tool ran would never reach the
|
||||||
|
// child. Cancel explicitly in that case — the bridge must honor an
|
||||||
|
// already-aborted signal, not lean on each provider re-checking it.
|
||||||
|
if (exec.signal?.aborted) run.cancel('parent step aborted')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await run.result
|
const result = await run.result
|
||||||
|
|||||||
@@ -282,6 +282,43 @@ describe('dsh-tool-subagent', () => {
|
|||||||
expect(result.isError).toBe(true)
|
expect(result.isError).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('cancels the run when the tool signal is ALREADY aborted before execute (no missed abort)', async () => {
|
||||||
|
// `addEventListener('abort')` does not fire for a signal already aborted
|
||||||
|
// before the listener is added, so a step cancelled before the tool ran
|
||||||
|
// would never reach the child unless the bridge re-checks `signal.aborted`.
|
||||||
|
// A provider that leans only on the abort EVENT (this spy never inspects
|
||||||
|
// request.signal) proves the bridge itself must cancel.
|
||||||
|
const cancelled = vi.fn()
|
||||||
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(SystemPrompt)
|
||||||
|
await ctx.plugin(ToolRegistry)
|
||||||
|
await ctx.plugin(SubagentService)
|
||||||
|
ctx.subagents.registerProvider({
|
||||||
|
name: 'spy',
|
||||||
|
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false },
|
||||||
|
start: () => {
|
||||||
|
let resolveResult: (r: { output: never[]; stopReason: 'aborted' }) => void
|
||||||
|
const result = new Promise<{ output: never[]; stopReason: 'aborted' }>((res) => { resolveResult = res })
|
||||||
|
return {
|
||||||
|
id: AgentId('spy-child'),
|
||||||
|
result,
|
||||||
|
cancel: () => {
|
||||||
|
cancelled()
|
||||||
|
resolveResult({ output: [], stopReason: 'aborted' })
|
||||||
|
},
|
||||||
|
dispose: async () => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await ctx.plugin(tool, { provider: 'spy' })
|
||||||
|
|
||||||
|
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(cancelled).toHaveBeenCalledTimes(1)
|
||||||
|
expect(result.isError).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('tools depend on the service: no `subagent` tool without ctx.subagents', async () => {
|
it('tools depend on the service: no `subagent` tool without ctx.subagents', async () => {
|
||||||
const ctx = new Context()
|
const ctx = new Context()
|
||||||
await ctx.plugin(SystemPrompt)
|
await ctx.plugin(SystemPrompt)
|
||||||
|
|||||||
Reference in New Issue
Block a user