feat(subagent): deliver continuable child settlement to parents
A continuable child that stopped without reporting — an error, a token ceiling, cancellation, teardown — left its parent nothing to act on. The continuation manager now delivers an unconditional settlement notice to the durable direct parent before releasing ownership, folding consumed work (foldConsumedWork supersedes findLastMessageTurnEnd) so a claimed-but-unrun prompt reads as aborted rather than completed, waking an idle parent, steering a busy one, and never waking a closing tree.
This commit is contained in:
@@ -16,6 +16,7 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
import * as tool from '../src/list-agents.ts'
|
||||
import { parkParent } from './park-parent.ts'
|
||||
|
||||
/** One scripted response that may wait on a caller-released gate before streaming. */
|
||||
interface GatedEntry {
|
||||
@@ -63,6 +64,7 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
|
||||
await ctx.plugin(tool)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
|
||||
parkParent(ctx, parent)
|
||||
return { ctx, parent, adapter }
|
||||
}
|
||||
|
||||
|
||||
22
packages/subagent/tool-subagent-control/tests/park-parent.ts
Normal file
22
packages/subagent/tool-subagent-control/tests/park-parent.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Shared suite helper: keep this package's stand-in parent out of a scripted
|
||||
* model corpus.
|
||||
* @module park-parent
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/**
|
||||
* Reject every step of the stand-in parent. Each child settlement wakes its
|
||||
* parent, and these suites size their scripts for child turns only; the tests
|
||||
* assert on delivery rather than on the parent's own turn.
|
||||
* @param ctx - the booted test context.
|
||||
* @param parent - the stand-in parent whose turns must not reach the model.
|
||||
*/
|
||||
export function parkParent(ctx: Context, parent: { id: SessionId }): void {
|
||||
ctx.on('agent/pre-step', async ({ agent: subject }, next) => {
|
||||
if (subject.id !== parent.id) return next()
|
||||
return { kind: 'reject' as const }
|
||||
})
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
import * as tool from '../src/index.ts'
|
||||
import { parkParent } from './park-parent.ts'
|
||||
|
||||
/** One scripted response that may wait on a caller-released gate before streaming. */
|
||||
interface GatedEntry {
|
||||
@@ -62,6 +63,7 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
|
||||
await ctx.plugin(tool)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
|
||||
parkParent(ctx, parent)
|
||||
return { ctx, parent, adapter }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user