Two human review directives: - No re-prompt. A structured child that finishes a turn cleanly without calling structured_output settles error to the parent immediately — readResult already carried that mapping; the nudge loop only delayed it. Deletes the loop, its cancellation-window guard, STRUCTURED_OUTPUT_NUDGE, and the structuredNudgeRetries Config on both backends. - FIXME in the structured module doc: per-agent/per-session tool registry and prompt assembly would dissolve the final-assembly enforcement dance (the placeholder tool, the swap, the strip, the global-registration lifetime).
@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 deployment persona needs no inheritance — it is a context-wide prompt section); - 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); there is deliberately NO re-prompt for a structured child that finished cleanly without callingstructured_output— the shortfall maps to anerrorresult for the parent; - 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[] } — the per-backend inputs: the provider name (for error context) and the optional child-session seed.
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 for one that has it always carries the run's OWN schema (as the tool'sparameters) plus the calling instruction appended to itssystemtext (the demand travels with the tool —AgentOptionshas no per-agent prompt field to carry 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 (alsoprepend: true— an earlier-registered force-continue listener returning withoutnext()must not decide the turn before the veto runs) 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.