Files
deepseek-harness/packages/subagent/tool-subagent/tests/scripted-provider.ts
Hypatia May a26d03960a feat(subagent): sibling delegations are concurrency-safe
One assistant message carrying several subagent calls now overlaps them
under the rolling pool (maxParallelToolCalls) instead of serializing each
foreground delegation behind an exclusive barrier; results still commit
in model order.

tool-subagent declares isConcurrencySafe: () => true for every call form:
children work in their own sessions, a run never mutates the parent
session, and sibling workspace coordination already belongs to the model
for background, continuable, and workflow children. The former
serial-execution pin flips to a parallel pin plus a gated genuine-overlap
test, and the authored subagent-parallel snapshot pins the assembled
transcript (tool/call, tool/call, tool/result, tool/result) with
interchangeable twin children, which replay race-free under the existing
first-call binding and createdAt harvest ordering.

The provider seam now requires concurrent starts and continuable
preparations for distinct children to isolate operation-local state,
cancellation, settlement, and cleanup; a continuable gate test proves a
cancelled preparation leaves no Agent or durable Session while its
sibling persists independently.

Closes deepseek-harness/deepseek-harness#1489
2026-08-11 12:41:24 +08:00

108 lines
3.9 KiB
TypeScript

/** Package-local scripted child boundary for deterministic tool-subagent tests. */
import type { Context } from '@deepseek-ai/cordis'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {
SubagentCapabilities,
SubagentProvider,
SubagentResult,
SubagentRun,
SubagentStartRequest,
SubagentStopReason,
} from '@deepseek-ai/dsh-subagent'
const DEFAULT_CAPABILITIES: SubagentCapabilities = {
outputSchema: true,
depthLimit: true,
toolFilter: true,
persona: true,
}
/** Options for one scripted provider fixture. */
export interface Config {
/** Registry name to register under. */
name: string
/** Final text returned by the scripted child. */
reply?: string
/** Terminal result reason. */
stopReason?: SubagentStopReason
/** Start-time features advertised by the provider. */
capabilities?: Partial<SubagentCapabilities>
/** Whether tool descriptions say the child inherits completed turns. */
inheritsParentContext?: boolean
/** Structured value returned when the request asks for one. */
structured?: unknown
/** Observes each start; the child's result additionally waits for the returned promise. */
onStart?: (request: SubagentStartRequest) => Promise<void> | void
}
/** Scripted provider whose result aborts if its signal or disposer wins first. */
class ScriptedSubagentProvider implements SubagentProvider {
readonly capabilities: SubagentCapabilities
readonly inheritsParentContext: boolean
constructor(
readonly name: string,
private readonly config: Config,
) {
this.capabilities = { ...DEFAULT_CAPABILITIES, ...config.capabilities }
this.inheritsParentContext = config.inheritsParentContext ?? false
}
async start(request: SubagentStartRequest): Promise<SubagentRun> {
if (request.signal.aborted) throw new Error('scripted subagent start aborted before publication')
const reply = this.config.reply ?? 'scripted subagent reply'
const output: ContentBlock[] = [{ type: 'text', text: reply }]
const wantsStructured = request.outputSchema !== undefined && this.capabilities.outputSchema
const stopReason = this.config.stopReason ?? 'completed'
const state = { cancelled: false }
const onAbort = (): void => { state.cancelled = true }
request.signal.addEventListener('abort', onAbort, { once: true })
await Promise.resolve()
if (state.cancelled) {
request.signal.removeEventListener('abort', onAbort)
throw new Error('scripted subagent start aborted before publication')
}
const resultFor = (): SubagentResult => ({
output,
...wantsStructured ? { structured: this.config.structured ?? { reply } } : {},
stopReason: state.cancelled ? 'aborted' : stopReason,
})
const gate = Promise.resolve(this.config.onStart?.(request))
const result = gate.then(() => new Promise<SubagentResult>((resolve) => {
setTimeout(() => { resolve(resultFor()) }, 0)
})).finally(() => {
request.signal.removeEventListener('abort', onAbort)
})
return {
id: SessionId(`scripted-subagent:${this.name}:${request.parent.id}`),
localAgent: undefined,
result,
dispose(): Promise<void> {
state.cancelled = true
request.signal.removeEventListener('abort', onAbort)
return Promise.resolve()
},
}
}
}
/**
* Mount one scripted provider through an effect-scoped local plugin.
* @param ctx - context carrying the real subagent registry.
* @param config - scripted provider identity and outcome.
* @returns the fixture plugin's disposable fiber.
*/
export function mountScriptedProvider(ctx: Context, config: Config) {
return ctx.plugin({
name: 'scripted-subagent-provider',
inject: ['subagents'],
apply(pluginCtx: Context): void {
pluginCtx.subagents.registerProvider(new ScriptedSubagentProvider(config.name, config))
},
})
}