Beyond the mechanical conflicts (provider capability lines vs master's new inheritsParentContext field; generated catalogs regenerated rather than hand-merged; knip/lockfile), three master-side reworks required semantic adaptation of this branch: - The persona rework removed AgentOptions.systemPrompt, which was the structured-output instruction's channel. The instruction now rides the SAME final-request enforcement listener that injects the schema'd tool: appended per request to final.system (per-request wire state, not agent prompt state). Tests assert the wire request (adapter.requests) instead of child.options; the bare-direct-dispatch test pins the no-system arm. - Tool guidance moved out of deployment prompts into per-tool prompt sections; the examples' workflow paragraph became a tool:<toolName> section contributed by dsh-tool-workflow (explicit-ask-only policy), and both example personas resolve to master's minimal identity+behavior form. tool-workflow gains inject: systemPrompt (+ peer dep, tsconfig ref); the export-shape guard updated. - The uniform-RFC-format gate: the dynamic-workflows RFC restructured to the implemented/ skeleton (bare Status line; Proposal -> Decision; What-was-rejected -> Alternatives considered; new Consequences), and the overall-run-timeout deferral is now recorded in the RFC's Deferred list. The doc-graphs atlas classification gains the workflows seam (workflow-vm implementation, tool-workflow consumer). Master's harness-identity section made "empty assembled prompt" states unreachable through the loop, so the instruction-append is a plain undefined-ternary and the structured tests assert append-not-replace. All snapshot goldens (including workflow-run) replay unchanged. Full local CI-equivalent gate sequence green on the merged tree.
@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); 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 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.