The seam vocabulary (SubagentStartRequest.outputSchema, SubagentResult .structured) existed but no in-process backend honored it — spawn/fork advertised outputSchema: false. This lands the missing half: - dsh-tools gains a structured-output JSON Schema subset (json-schema.ts): StructuredOutputSchema, assertSupportedOutputSchema (rejects loud outside the enforced subset, every violation listed), validateStructuredValue (path-qualified issues, total). outputSchema's seam type becomes this raw JSON-Schema subset instead of the author-facing SchemaSpec DSL — the schema travels verbatim to the model as a forced tool's parameters. - dsh-subagent-inprocess gains the shared structured runtime: one global structured_output capture tool (placeholder parameters) + a prepend:true agent/request listener doing FINAL-REQUEST enforcement (strip for plain agents, per-run schema for structured children — survives downstream request-replacing listeners) + an agent/turn-continuation veto that stops a child's turn once captured (no wasted extra model step). Lifetime is refcounted by backends (plugin lifetime) AND live runs (start→settle). - startInProcessRun drives the capture: subset asserted before the child exists, instruction appended to the child's system prompt, clean-finish nudge loop (structuredNudgeRetries, backend Config, default 1), captured value on result.structured; a clean finish without a capture settles 'error' (never a silent success with a missing field). - spawn + fork flip outputSchema: true and inject 'tools'.
5.1 KiB
@deepseek-ai/dsh-subagent-inprocess
The shared in-process subagent run driver. A pure library (no provider, no registration) that the in-process backends — spawn (a fresh child) and fork (a child seeded with a prefix of the parent's log) — both build on. The backends are thin shells that differ ONLY in the session seed they pass; everything downstream lives here, so neither backend depends on the other.
What it exports
startInProcessRun(ctx, request, options): SubagentRun
Runs a child as a child Agent on the same cordis context (ctx.agents):
- computes child depth =
depthOf(parent) + 1; ifrequest.maxDepthis set and exceeded, throwsSubagentDepthError(thedepthLimitcapability); arequest.outputSchemais asserted against the supported subset (assertSupportedOutputSchemafrom dsh-tools) before any child exists; - creates a child via
ctx.agents.createwith a freshAgentId/SessionId, the parent'scwd+parentSessionlineage, the optionaloptions.seed(fork's completed-turn prefix; omitted for a fresh child), andagentOptions(the child inherits the parent's model by default — a child with no model can't run — overridable viarequest.agentOptions.model; the system prompt is NOT inherited; a structured run appends thestructured_outputinstruction after the caller's prompt); - drives the one-shot:
child.send(prompt)thenawait child.whenIdle()(ordering matters —sendenqueues synchronously, sowhenIdleobserves the queued work and resolves on the child'srunning → idletransition, never before the turn starts); a structured child that finished a turn CLEANLY without callingstructured_outputis re-prompted (a nudge — a fresh turn) up tooptions.structuredNudgeRetriestimes; - reads the result, scoped to the child's OWN events (everything at or after
seedLength, so a seeded child that produced no message of its own never returns the seeded parent's last message): the lastassistant/messagecontent (deep-cloned — the log is frozen) and the lastturn/end.reasonmapped to aSubagentStopReason. A structured run surfaces the captured value asresult.structured; a structured child that finished cleanly WITHOUT ever capturing settleserror(a clean finish without the demanded result is a failure, not a success with a missing field).
dispose() delegates to AgentHandle.dispose() (stop loop → await quiescence → remove session); cancel() cancels the child's in-flight turn. A cancel landing before any turn/end (the pre-turn window) still settles aborted, honoring the cancel contract rather than the generic no-turn error.
InProcessRunOptions
{ providerName: string; seed?: SessionEvent[]; structuredNudgeRetries: number } — the per-backend inputs: the provider name (for error context), the optional child-session seed, and the structured-run nudge budget (REQUIRED, resolved from the backend's validated Config — the driver never fills it with a hidden default).
Structured output: acquireStructuredRuntime(ctx): StructuredAcquisition
The mechanism behind outputSchema for in-process children. One globally registered structured_output capture tool (its registered parameters are a placeholder) plus two listeners, registered once per root context and shared by every holder:
- an
agent/requestwaterfall listener registeredprepend: truethat post-processesawait next()— final-request enforcement: the request that hits the wire never carriesstructured_outputfor an agent without a structured run, and always carries the run's OWN schema (as the tool'sparameters) for one that has it. Per-agent shaping lives here because the tool registry and prompt assembly are context-global while schemas differ per concurrent child; cooperative mutate-then-next()would not survive a downstream listener returning a replacement request. - an
agent/turn-continuationlistener that stops a child's turn once its output is captured, so a successful capture doesn't buy a wasted extra model step.
The capture tool validates each call against the run's schema (validateStructuredValue) — violations become an INVALID_ARGS isError result the model retries in-turn; a valid call records the value.
Lifetime is refcounted with two kinds of holder: each backend acquires for its plugin lifetime (apply), and each structured RUN holds its own acquisition from start to settle — so unregistration can never precede a live run's settle, and the runtime disposes only when the last backend AND the last run are gone. release() is idempotent per acquisition.
depthOf(agent): number
Delegation depth rides on a merge-extensible AgentOptions.subagentDepth field (0 for a top-level agent, parent + 1 for a child), so a nested spawn reads its parent's depth from parent.options.subagentDepth. depthOf reads it (absent ⇒ 0).
SubagentDepthError
Thrown by startInProcessRun when a spawn would exceed the request's maxDepth cap; carries attemptedDepth and maxDepth.