ds-review-bot round-2 findings: (1) dsh-subagent's runtime import of @deepseek-ai/dsh-scope was undeclared in its manifest and tsconfig references (the root paths map masked it; the emitted package would import an undeclared dependency) — wired as peer+dev with the project reference, module graph regenerated. (2) The structured re-assert only ensured PRESENCE, so a downstream listener injecting a same-named entry with the wrong schema kept it model-visible while validateStructuredValue enforced the real one; it now REPLACES any same-named tool/section with the run's own. Pinned by a wrong-schema-injection test asserting exactly one entry carrying the run's schema.
@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) and then snapshotted withstructuredClonebefore any child exists — assertion first so a hostile value fails asOutputSchemaError(never a raw clone error), the snapshot so a post-start()caller mutation cannot drift the enforced schema; - 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 (package-internal runtime)
attachStructuredRuntime(childCtx, schema) registers the run's whole enforcement surface as SCOPED registrations on the child's agent.ctx — riding the child's fiber (a backend hot-reload mid-run cannot unregister anything; a disposed child leaves no residue) and visible to that child alone (two concurrent structured runs never interact; no placeholder schema, no strip-for-everyone-else, no refcounted global state):
- the
structured_outputcapture tool with the run's REAL schema as its registeredparameters, validating each call (validateStructuredValue) — violations become anINVALID_ARGSisError the model retries in-turn; a valid call STAGES the value keyed by its call id; - the calling instruction as an ordinary order-190 scoped prompt section (the demand travels with the tool, as prompt state of exactly one agent);
- a scoped
system-prompt/assemblere-assert (prepend: true= outermost): whatever downstream listeners mutate or replace, the child's assembly always carries its capture tool and instruction — the loop logs the rendered assembly as the step'srequest/header, so the demand is reconstructable log state; - a scoped
tools/post-executeCOMMIT (prepend: true): the staged value becomes the run's result only when the final decision accepts THE SAME CALL that staged it — call-keyed, so a stale stage orphaned by an outer short-circuiting listener is dropped, never promoted on a later call's acceptance; - a scoped
tools/pre-executedeny for any call arriving after the capture — terminal means terminal WITHIN the step; - a scoped
agent/turn-continuationveto (prepend: true) stopping the child's turn once its output is captured, so a successful capture doesn't buy a wasted extra model step.
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.