Przeglądaj źródła

Extract the shared in-process driver into dsh-subagent-inprocess (review feedback)

The shared run driver lived inside dsh-subagent-spawn, so the spawn package
carried fork-aware seeding logic and dsh-subagent-fork depended backward on
dsh-subagent-spawn — the two in-process backends were not independent.

Move the driver (startInProcessRun, depthOf, SubagentDepthError,
InProcessRunOptions) into a new pure-library package
@deepseek-ai/dsh-subagent-inprocess that registers nothing. spawn and fork now
both depend only on that driver and neither knows about the other; spawn no
longer re-exports it and fork no longer imports from spawn.

Also wire BOTH backends in examples/coding-agent/cordis.yml (config-only): load
dsh-subagent-spawn + dsh-subagent-fork + two dsh-tool-subagent instances with
distinct toolNames (subagent → spawn, subagent_fork → fork), demonstrating that
exposing multiple transports needs no code change.
Tianyi Cui 3 miesięcy temu
rodzic
commit
0c9ea3145f

+ 10 - 7
docs/module-graph.md

@@ -60,13 +60,13 @@ graph TD
   agent-core --> system-prompt
   agent-core --> tool-bash
   agent-core --> tools
+  subagent-inprocess --> agent
+  subagent-inprocess --> llm
+  subagent-inprocess --> session
+  subagent-inprocess --> subagent
   subagent-mock --> agent
   subagent-mock --> llm
   subagent-mock --> subagent
-  subagent-spawn --> agent
-  subagent-spawn --> llm
-  subagent-spawn --> session
-  subagent-spawn --> subagent
   tool-subagent --> agent
   tool-subagent --> llm
   tool-subagent --> subagent
@@ -82,7 +82,9 @@ graph TD
   subagent-fork --> agent
   subagent-fork --> session
   subagent-fork --> subagent
-  subagent-fork --> subagent-spawn
+  subagent-fork --> subagent-inprocess
+  subagent-spawn --> subagent
+  subagent-spawn --> subagent-inprocess
 ```
 
 | Package | Depends on |
@@ -108,9 +110,10 @@ graph TD
 | `subagent` | `agent`, `llm`, `tools` |
 | `tool-bash` | `agent`, `bash`, `llm`, `tools` |
 | `agent-core` | `agent`, `agent-loop`, `invariants`, `llm`, `session`, `system-prompt`, `tool-bash`, `tools` |
+| `subagent-inprocess` | `agent`, `llm`, `session`, `subagent` |
 | `subagent-mock` | `agent`, `llm`, `subagent` |
-| `subagent-spawn` | `agent`, `llm`, `session`, `subagent` |
 | `tool-subagent` | `agent`, `llm`, `subagent`, `tools` |
 | `acp-agent` | `acp`, `agent-core`, `session-persistence-jsonl` |
 | `stdio-agent` | `agent`, `agent-core`, `session`, `session-persistence-jsonl`, `ui-stdio` |
-| `subagent-fork` | `agent`, `session`, `subagent`, `subagent-spawn` |
+| `subagent-fork` | `agent`, `session`, `subagent`, `subagent-inprocess` |
+| `subagent-spawn` | `subagent`, `subagent-inprocess` |

+ 22 - 6
examples/coding-agent/cordis.yml

@@ -57,17 +57,21 @@
 
       Use the subagent tool to delegate a focused, self-contained subtask
       to a fresh child agent (it works in its own context and returns only
-      its final result) — give it a complete, standalone instruction.
+      its final result) — give it a complete, standalone instruction. Use
+      subagent_fork instead when the subtask needs THIS conversation's
+      context: the child inherits the log so far.
 
       Check the [exit code: N] marker on every command; investigate
       failures before moving on. Verify your work by running the code or
       tests. Keep answers brief and factual.
 
-# The subagent seam + an in-process spawn backend + the model-facing `subagent`
-# tool, as leaf entries after the app (which provides ctx.agents/ctx.tools). The
-# tool is bound to the `spawn` backend: a delegated task runs as a fresh child
-# agent on this same process. (fork is available too — load dsh-subagent-fork
-# and a second dsh-tool-subagent bound to it with a distinct toolName.)
+# The subagent seam + BOTH in-process backends + two model-facing tools, as leaf
+# entries after the app (which provides ctx.agents/ctx.tools). spawn (a fresh
+# child) and fork (a child seeded with the parent's completed-turn prefix) are
+# independent backends over the shared dsh-subagent-inprocess driver. Exposing
+# both transports is pure config: load each backend, then load dsh-tool-subagent
+# once per backend with a distinct toolName (the tool registry rejects a
+# duplicate name) — no code change.
 - id: subagent
   name: '@deepseek-ai/dsh-subagent'
 
@@ -76,7 +80,19 @@
   config:
     providerName: spawn
 
+- id: subagent-fork
+  name: '@deepseek-ai/dsh-subagent-fork'
+  config:
+    providerName: fork
+
 - id: tool-subagent
   name: '@deepseek-ai/dsh-tool-subagent'
   config:
     provider: spawn
+    toolName: subagent
+
+- id: tool-subagent-fork
+  name: '@deepseek-ai/dsh-tool-subagent'
+  config:
+    provider: fork
+    toolName: subagent_fork

+ 3 - 2
packages/subagent/README.md

@@ -5,10 +5,11 @@ The subagent seam: an agent delegating work to a child agent. Like the [bash](..
 | Package | Role | ctx key |
 |---|---|---|
 | `subagent/` | Abstract subagent seam: named-provider registry + vocabulary | `ctx.subagents` |
-| `subagent-spawn/` | In-process backend: a fresh child agent (+ the shared run driver) | (registers on `ctx.subagents`) |
+| `subagent-inprocess/` | Shared in-process run driver (pure lib; registers nothing) | — |
+| `subagent-spawn/` | In-process backend: a fresh child agent | (registers on `ctx.subagents`) |
 | `subagent-fork/` | In-process backend: a child seeded with the parent's completed-turn prefix | (registers on `ctx.subagents`) |
 | `tool-subagent/` | Model-facing `subagent` delegation tool over `ctx.subagents` | (registers on `ctx.tools`) |
 
-The interface lives at `subagent/subagent/`. The in-process `subagent-spawn` / `subagent-fork` backends ship here; the out-of-process `dsh-subagent-acp` and the test-only `dsh-subagent-mock` (in [support](../support/README.md)) are separate. All **product** packages except the mock.
+The interface lives at `subagent/subagent/`. The in-process `subagent-spawn` / `subagent-fork` backends share the `subagent-inprocess` driver (a pure library — both depend on it, neither on the other) and ship here; the out-of-process `dsh-subagent-acp` and the test-only `dsh-subagent-mock` (in [support](../support/README.md)) are separate. All **product** packages except the mock.
 
 The proposal and design rationale: [docs/rfc/proposed/feature/2026-06-21-subagent-capability-seam.md](../../docs/rfc/proposed/feature/2026-06-21-subagent-capability-seam.md).

+ 2 - 1
packages/subagent/subagent-fork/package.json

@@ -23,7 +23,7 @@
     "@deepseek-ai/dsh-agent": "^0.0.1",
     "@deepseek-ai/dsh-session": "^0.0.1",
     "@deepseek-ai/dsh-subagent": "^0.0.1",
-    "@deepseek-ai/dsh-subagent-spawn": "^0.0.1",
+    "@deepseek-ai/dsh-subagent-inprocess": "^0.0.1",
     "cordis": "^4.0.0-rc.6"
   },
   "dependencies": {
@@ -36,6 +36,7 @@
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-subagent": "workspace:^",
+    "@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
     "@deepseek-ai/dsh-subagent-spawn": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",

+ 5 - 3
packages/subagent/subagent-fork/src/index.ts

@@ -2,8 +2,10 @@
  * The in-process FORK subagent backend: registers a {@link SubagentProvider} on
  * `ctx.subagents` that runs each child as a child {@link Agent} SEEDED with a
  * prefix of the parent's session log — so the child inherits the parent's
- * conversation context instead of starting fresh. Shares the run driver with
- * `@deepseek-ai/dsh-subagent-spawn`; the only difference is the seed.
+ * conversation context instead of starting fresh. The run mechanics live in
+ * `@deepseek-ai/dsh-subagent-inprocess` ({@link startInProcessRun}); this
+ * backend just computes the seed. The spawn backend is an independent peer over
+ * the same driver.
  *
  * The seed boundary is the crux: at the moment a subagent tool's `execute`
  * runs, the parent's CURRENT turn is open and unbalanced (it holds the
@@ -23,7 +25,7 @@ import z from 'schemastery'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import type { SubagentCapabilities, SubagentProvider, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
-import { startInProcessRun } from '@deepseek-ai/dsh-subagent-spawn'
+import { startInProcessRun } from '@deepseek-ai/dsh-subagent-inprocess'
 
 export const name = 'subagent-fork'
 export const inject = ['subagents', 'agents']

+ 1 - 1
packages/subagent/subagent-fork/tsconfig.json

@@ -27,7 +27,7 @@
       "path": "../subagent"
     },
     {
-      "path": "../subagent-spawn"
+      "path": "../subagent-inprocess"
     }
   ]
 }

+ 28 - 0
packages/subagent/subagent-inprocess/README.md

@@ -0,0 +1,28 @@
+# @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](../subagent-spawn/README.md) (a fresh child) and [fork](../subagent-fork/README.md) (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`](../../core/agent) on the same cordis context (`ctx.agents`):
+
+1. computes child depth = `depthOf(parent) + 1`; if `request.maxDepth` is set and exceeded, throws `SubagentDepthError` (the `depthLimit` capability);
+2. creates a child via `ctx.agents.create` with a fresh `AgentId`/`SessionId`, the parent's `cwd` + `parentSession` lineage, the optional `options.seed` (fork's completed-turn prefix; omitted for a fresh child), and `agentOptions` (the child inherits the **parent's model** by default — a child with no model can't run — overridable via `request.agentOptions.model`; the system prompt is NOT inherited);
+3. drives the one-shot: `child.send(prompt)` then `await child.whenIdle()` (ordering matters — `send` enqueues synchronously, so `whenIdle` observes the queued work and resolves on the child's `running → idle` transition, never before the turn starts);
+4. 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 last `assistant/message` content (deep-cloned — the log is frozen) and the last `turn/end.reason` mapped to a `SubagentStopReason`.
+
+`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.
+
+### `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`.

+ 40 - 0
packages/subagent/subagent-inprocess/package.json

@@ -0,0 +1,40 @@
+{
+  "name": "@deepseek-ai/dsh-subagent-inprocess",
+  "description": "Shared in-process subagent run driver: drives a child agent on ctx.agents (used by the spawn and fork backends)",
+  "version": "0.0.1",
+  "private": true,
+  "type": "module",
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./lib/index.d.ts",
+      "default": "./lib/index.js"
+    },
+    "./src/*": "./src/*",
+    "./package.json": "./package.json"
+  },
+  "files": [
+    "lib",
+    "src"
+  ],
+  "license": "BSD-3-Clause",
+  "peerDependencies": {
+    "@deepseek-ai/dsh-agent": "^0.0.1",
+    "@deepseek-ai/dsh-llm": "^0.0.1",
+    "@deepseek-ai/dsh-session": "^0.0.1",
+    "@deepseek-ai/dsh-subagent": "^0.0.1",
+    "cordis": "^4.0.0-rc.6"
+  },
+  "devDependencies": {
+    "@deepseek-ai/dsh-agent": "workspace:^",
+    "@deepseek-ai/dsh-agent-loop": "workspace:^",
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/dsh-llm": "workspace:^",
+    "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-subagent": "workspace:^",
+    "@deepseek-ai/dsh-system-prompt": "workspace:^",
+    "@deepseek-ai/dsh-tools": "workspace:^",
+    "cordis": "^4.0.0-rc.6"
+  }
+}

+ 11 - 10
packages/subagent/subagent-spawn/src/in-process.ts → packages/subagent/subagent-inprocess/src/index.ts

@@ -1,15 +1,16 @@
 /**
- * The shared in-process subagent run driver. A subagent backend that runs the
- * child as a child {@link Agent} on the SAME cordis context (`ctx.agents`) —
- * the cheapest transport, reusing the agent factory's quiescent
- * {@link AgentHandle} teardown. Both in-process backends use this:
- * `@deepseek-ai/dsh-subagent-spawn` (a fresh child) and
- * `@deepseek-ai/dsh-subagent-fork` (a child seeded with a prefix of the
- * parent's log) differ ONLY in the `seed` they pass — everything downstream
- * (drive the child, read its final output, map the stop reason, dispose) is
- * identical and lives here.
+ * The shared in-process subagent run driver: run a child as a child
+ * {@link Agent} on the SAME cordis context (`ctx.agents`) — the cheapest
+ * transport, reusing the agent factory's quiescent {@link AgentHandle}
+ * teardown. The concrete in-process backends are thin shells over this driver,
+ * differing ONLY in the `seed` they pass (a fresh child vs. a child seeded with
+ * a prefix of the parent's log); everything downstream — drive the child, read
+ * its final output, map the stop reason, dispose — is identical and lives here.
  *
- * @module @deepseek-ai/dsh-subagent-spawn/in-process
+ * This package owns no provider and registers nothing; it is a pure library the
+ * backend packages depend on, so neither backend needs to know about the other.
+ *
+ * @module @deepseek-ai/dsh-subagent-inprocess
  */
 
 import { randomUUID } from 'node:crypto'

+ 85 - 0
packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts

@@ -0,0 +1,85 @@
+import { describe, expect, it } from 'vitest'
+import { Context } from 'cordis'
+import LlmService from '@deepseek-ai/dsh-llm'
+import SessionStore from '@deepseek-ai/dsh-session'
+import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
+import ToolRegistry from '@deepseek-ai/dsh-tools'
+import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
+import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as Invariants from '@deepseek-ai/dsh-invariants'
+import SubagentService from '@deepseek-ai/dsh-subagent'
+import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
+import { depthOf, SubagentDepthError, startInProcessRun } from '../src/index.ts'
+
+type Script = ConstructorParameters<typeof MockAdapter>[0]
+
+/**
+ * Drives the shared in-process run driver DIRECTLY (no provider package), so the
+ * driver's own contract — depth read/cap, the one-shot drive, the result read —
+ * is covered independently of which backend (spawn/fork) calls it. The only
+ * mocked boundary is the model; the real agent loop, SubagentService, and
+ * dsh-invariants are mounted, so a malformed child session log fails the test.
+ */
+async function setup(script: Script) {
+  const ctx = new Context()
+  await ctx.plugin(LlmService)
+  await ctx.plugin(SessionStore)
+  await ctx.plugin(SystemPrompt)
+  await ctx.plugin(ToolRegistry)
+  await ctx.plugin(AgentRegistry)
+  await ctx.plugin(Invariants)
+  await ctx.plugin(AgentLoop, { agents: [] })
+  await ctx.plugin(SubagentService)
+  ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
+  const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' })
+  return { ctx, parent }
+}
+
+function text(blocks: { type: string; text?: string }[]): string {
+  return blocks.filter(b => b.type === 'text').map(b => b.text).join('')
+}
+
+describe('depthOf', () => {
+  it('reads 0 for an agent with no subagentDepth, the set value otherwise', async () => {
+    const { parent } = await setup([])
+    expect(depthOf(parent)).toBe(0)
+    const withDepth = { options: { subagentDepth: 3 } } as unknown as Agent
+    expect(depthOf(withDepth)).toBe(3)
+  })
+})
+
+describe('startInProcessRun', () => {
+  it('drives a fresh child (no seed) to completion and returns its output', async () => {
+    const { ctx, parent } = await setup([textResponse('driver child answer')])
+    const run = startInProcessRun(ctx, { prompt: [{ type: 'text', text: 'do X' }], parent }, { providerName: 'spawn' })
+    const result = await run.result
+    expect(result.stopReason).toBe('completed')
+    expect(text(result.output)).toBe('driver child answer')
+    expect(depthOf(ctx.agents.get(run.id)!)).toBe(1)
+    await run.dispose()
+  })
+
+  it('throws SubagentDepthError when the child would exceed maxDepth', async () => {
+    const { ctx, parent } = await setup([])
+    expect(() => startInProcessRun(ctx, { prompt: [{ type: 'text', text: 'p' }], parent, maxDepth: 0 }, { providerName: 'spawn' }))
+      .toThrow(SubagentDepthError)
+  })
+
+  it('seeds the child session when a seed is supplied', async () => {
+    // Drive the parent through one real turn, then seed the child with that
+    // completed-turn prefix — the child must SEE the parent's history but its
+    // result is scoped to its OWN events (not the seeded parent message).
+    const { ctx, parent } = await setup([textResponse('parent turn'), textResponse('seeded child reply')])
+    parent.send([{ type: 'text', text: 'parent q' }])
+    await parent.whenIdle()
+    const seed = parent.session.events.slice()
+    const run = startInProcessRun(ctx, { prompt: [{ type: 'text', text: 'child q' }], parent }, { providerName: 'fork', seed })
+    const result = await run.result
+    expect(result.stopReason).toBe('completed')
+    expect(text(result.output)).toBe('seeded child reply')
+    const child = ctx.agents.get(run.id)!
+    // The child inherited the parent's prefix.
+    expect(child.session.events.slice(0, seed.length).some(e => e.type === 'user/message')).toBe(true)
+    await run.dispose()
+  })
+})

+ 30 - 0
packages/subagent/subagent-inprocess/tsconfig.json

@@ -0,0 +1,30 @@
+{
+  "extends": "../../../tsconfig.base.json",
+  "compilerOptions": {
+    "rootDir": "src",
+    "outDir": "lib"
+  },
+  "include": [
+    "src"
+  ],
+  "references": [
+    {
+      "path": "../../../vendor/cosmokit"
+    },
+    {
+      "path": "../../../vendor/cordis"
+    },
+    {
+      "path": "../../core/agent"
+    },
+    {
+      "path": "../../llm/llm"
+    },
+    {
+      "path": "../../core/session"
+    },
+    {
+      "path": "../subagent"
+    }
+  ]
+}

+ 2 - 12
packages/subagent/subagent-spawn/README.md

@@ -2,17 +2,11 @@
 
 The in-process **spawn** subagent backend: a [`SubagentProvider`](../subagent/README.md) that runs each child as a **fresh** child [`Agent`](../../core/agent) on the same cordis context (`ctx.agents`) — its own session, its own (or the parent's) model, zero inherited conversation. The cheapest transport, reusing the agent factory's quiescent [`AgentHandle`](../../core/agent) teardown.
 
-It also exports the **shared in-process run driver** (`startInProcessRun`) that the [fork](../subagent-fork/README.md) backend builds on — spawn and fork differ only in the session seed.
+The run mechanics live in the shared [`@deepseek-ai/dsh-subagent-inprocess`](../subagent-inprocess/README.md) driver (`startInProcessRun`); this backend just passes **no seed** (a fresh child). The [fork](../subagent-fork/README.md) backend is an independent peer over the same driver — neither knows about the other.
 
 ## What it does
 
-`start(request)` →
-1. computes child depth = `depthOf(parent) + 1`; if `request.maxDepth` is set and exceeded, throws `SubagentDepthError` (the `depthLimit` capability);
-2. creates a child via `ctx.agents.create` with a fresh `AgentId`/`SessionId`, the parent's `cwd` + `parentSession` lineage, and `agentOptions` (the child inherits the **parent's model** by default — a child with no model can't run — overridable via `request.agentOptions.model`; the system prompt is NOT inherited);
-3. drives the one-shot: `child.send(prompt)` then `await child.whenIdle()` (ordering matters — `send` enqueues synchronously, so `whenIdle` observes the queued work and resolves on the child's `running → idle` transition, never before the turn starts);
-4. reads the result: the last `assistant/message` content (deep-cloned — the log is frozen) and the last `turn/end.reason` mapped to a `SubagentStopReason`.
-
-`dispose()` delegates to `AgentHandle.dispose()` (stop loop → await quiescence → remove session); `cancel()` cancels the child's in-flight turn.
+`start(request)` delegates to `startInProcessRun(ctx, request, { providerName })` with no seed: a fresh child agent with the parent's `cwd`/`parentSession` lineage and (by default) the parent's model. See the [driver README](../subagent-inprocess/README.md) for the full lifecycle (depth check, one-shot drive, result read, dispose).
 
 ## Capabilities
 
@@ -23,7 +17,3 @@ It also exports the **shared in-process run driver** (`startInProcessRun`) that
 | Key | Meaning |
 |---|---|
 | `providerName` | Registry name on `ctx.subagents` (default `spawn`). |
-
-## Depth tracking
-
-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`. Read it with the exported `depthOf(agent)`.

+ 2 - 3
packages/subagent/subagent-spawn/package.json

@@ -20,10 +20,8 @@
   ],
   "license": "BSD-3-Clause",
   "peerDependencies": {
-    "@deepseek-ai/dsh-agent": "^0.0.1",
-    "@deepseek-ai/dsh-llm": "^0.0.1",
-    "@deepseek-ai/dsh-session": "^0.0.1",
     "@deepseek-ai/dsh-subagent": "^0.0.1",
+    "@deepseek-ai/dsh-subagent-inprocess": "^0.0.1",
     "cordis": "^4.0.0-rc.6"
   },
   "dependencies": {
@@ -38,6 +36,7 @@
     "@deepseek-ai/dsh-llm-deepseek": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-subagent": "workspace:^",
+    "@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-tool-bash": "workspace:^",
     "@deepseek-ai/dsh-tool-subagent": "workspace:^",

+ 4 - 7
packages/subagent/subagent-spawn/src/index.ts

@@ -5,9 +5,9 @@
  * context). The cheapest transport, reusing the agent factory's quiescent
  * teardown.
  *
- * The fork sibling (`@deepseek-ai/dsh-subagent-fork`) shares this package's run
- * driver ({@link startInProcessRun}) and differs ONLY in seeding the child with
- * a prefix of the parent's log.
+ * The run mechanics live in `@deepseek-ai/dsh-subagent-inprocess`
+ * ({@link startInProcessRun}); this backend just passes NO seed (a fresh
+ * child). The fork backend is an independent peer over the same driver.
  *
  * Plugin export shape: named `name`/`inject`/`Config`/`apply`, NO default.
  *
@@ -17,10 +17,7 @@
 import type { Context } from 'cordis'
 import z from 'schemastery'
 import type { SubagentCapabilities, SubagentProvider, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
-import { startInProcessRun } from './in-process.ts'
-
-export { startInProcessRun, depthOf, SubagentDepthError } from './in-process.ts'
-export type { InProcessRunOptions } from './in-process.ts'
+import { startInProcessRun } from '@deepseek-ai/dsh-subagent-inprocess'
 
 export const name = 'subagent-spawn'
 export const inject = ['subagents', 'agents']

+ 1 - 1
packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts

@@ -12,7 +12,7 @@ import * as Invariants from '@deepseek-ai/dsh-invariants'
 import SubagentService from '@deepseek-ai/dsh-subagent'
 import { MockAdapter, maxTokensResponse, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
 import * as spawn from '../src/index.ts'
-import { depthOf, SubagentDepthError } from '../src/in-process.ts'
+import { depthOf, SubagentDepthError } from '@deepseek-ai/dsh-subagent-inprocess'
 
 type Script = ConstructorParameters<typeof MockAdapter>[0]
 

+ 2 - 8
packages/subagent/subagent-spawn/tsconfig.json

@@ -18,16 +18,10 @@
       "path": "../../../vendor/schemastery"
     },
     {
-      "path": "../../core/agent"
-    },
-    {
-      "path": "../../llm/llm"
-    },
-    {
-      "path": "../../core/session"
+      "path": "../subagent"
     },
     {
-      "path": "../subagent"
+      "path": "../subagent-inprocess"
     }
   ]
 }

+ 36 - 0
pnpm-lock.yaml

@@ -360,6 +360,9 @@ importers:
       '@deepseek-ai/dsh-subagent':
         specifier: workspace:^
         version: link:../subagent
+      '@deepseek-ai/dsh-subagent-inprocess':
+        specifier: workspace:^
+        version: link:../subagent-inprocess
       '@deepseek-ai/dsh-subagent-spawn':
         specifier: workspace:^
         version: link:../subagent-spawn
@@ -373,6 +376,36 @@ importers:
         specifier: ^4.0.0-rc.6
         version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
 
+  packages/subagent/subagent-inprocess:
+    devDependencies:
+      '@deepseek-ai/dsh-agent':
+        specifier: workspace:^
+        version: link:../../core/agent
+      '@deepseek-ai/dsh-agent-loop':
+        specifier: workspace:^
+        version: link:../../core/agent-loop
+      '@deepseek-ai/dsh-invariants':
+        specifier: workspace:^
+        version: link:../../support/invariants
+      '@deepseek-ai/dsh-llm':
+        specifier: workspace:^
+        version: link:../../llm/llm
+      '@deepseek-ai/dsh-session':
+        specifier: workspace:^
+        version: link:../../core/session
+      '@deepseek-ai/dsh-subagent':
+        specifier: workspace:^
+        version: link:../subagent
+      '@deepseek-ai/dsh-system-prompt':
+        specifier: workspace:^
+        version: link:../../core/system-prompt
+      '@deepseek-ai/dsh-tools':
+        specifier: workspace:^
+        version: link:../../core/tools
+      cordis:
+        specifier: ^4.0.0-rc.6
+        version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
+
   packages/subagent/subagent-spawn:
     dependencies:
       schemastery:
@@ -406,6 +439,9 @@ importers:
       '@deepseek-ai/dsh-subagent':
         specifier: workspace:^
         version: link:../subagent
+      '@deepseek-ai/dsh-subagent-inprocess':
+        specifier: workspace:^
+        version: link:../subagent-inprocess
       '@deepseek-ai/dsh-system-prompt':
         specifier: workspace:^
         version: link:../../core/system-prompt

+ 1 - 0
tsconfig.build.json

@@ -35,6 +35,7 @@
     { "path": "./packages/subagent/subagent" },
     { "path": "./packages/support/subagent-mock" },
     { "path": "./packages/subagent/tool-subagent" },
+    { "path": "./packages/subagent/subagent-inprocess" },
     { "path": "./packages/subagent/subagent-spawn" },
     { "path": "./packages/subagent/subagent-fork" }
   ]