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.
This commit is contained in:
Tianyi Cui
2026-06-22 14:27:38 +08:00
parent 9c1048f2b5
commit 0c9ea3145f
18 changed files with 286 additions and 62 deletions

View File

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

View File

@@ -57,17 +57,21 @@
Use the subagent tool to delegate a focused, self-contained subtask 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 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 Check the [exit code: N] marker on every command; investigate
failures before moving on. Verify your work by running the code or failures before moving on. Verify your work by running the code or
tests. Keep answers brief and factual. tests. Keep answers brief and factual.
# The subagent seam + an in-process spawn backend + the model-facing `subagent` # The subagent seam + BOTH in-process backends + two model-facing tools, as leaf
# tool, as leaf entries after the app (which provides ctx.agents/ctx.tools). The # entries after the app (which provides ctx.agents/ctx.tools). spawn (a fresh
# tool is bound to the `spawn` backend: a delegated task runs as a fresh child # child) and fork (a child seeded with the parent's completed-turn prefix) are
# agent on this same process. (fork is available too — load dsh-subagent-fork # independent backends over the shared dsh-subagent-inprocess driver. Exposing
# and a second dsh-tool-subagent bound to it with a distinct toolName.) # 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 - id: subagent
name: '@deepseek-ai/dsh-subagent' name: '@deepseek-ai/dsh-subagent'
@@ -76,7 +80,19 @@
config: config:
providerName: spawn providerName: spawn
- id: subagent-fork
name: '@deepseek-ai/dsh-subagent-fork'
config:
providerName: fork
- id: tool-subagent - id: tool-subagent
name: '@deepseek-ai/dsh-tool-subagent' name: '@deepseek-ai/dsh-tool-subagent'
config: config:
provider: spawn provider: spawn
toolName: subagent
- id: tool-subagent-fork
name: '@deepseek-ai/dsh-tool-subagent'
config:
provider: fork
toolName: subagent_fork

View File

@@ -5,10 +5,11 @@ The subagent seam: an agent delegating work to a child agent. Like the [bash](..
| Package | Role | ctx key | | Package | Role | ctx key |
|---|---|---| |---|---|---|
| `subagent/` | Abstract subagent seam: named-provider registry + vocabulary | `ctx.subagents` | | `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`) | | `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`) | | `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). 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).

View File

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

View File

@@ -2,8 +2,10 @@
* The in-process FORK subagent backend: registers a {@link SubagentProvider} on * 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 * `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 * 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 * conversation context instead of starting fresh. The run mechanics live in
* `@deepseek-ai/dsh-subagent-spawn`; the only difference is the seed. * `@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` * 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 * 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 { SessionEvent } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent'
import type { SubagentCapabilities, SubagentProvider, SubagentStartRequest } from '@deepseek-ai/dsh-subagent' 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 name = 'subagent-fork'
export const inject = ['subagents', 'agents'] export const inject = ['subagents', 'agents']

View File

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

View File

@@ -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`.

View File

@@ -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"
}
}

View File

@@ -1,15 +1,16 @@
/** /**
* The shared in-process subagent run driver. A subagent backend that runs the * The shared in-process subagent run driver: run a child as a child
* child as a child {@link Agent} on the SAME cordis context (`ctx.agents`) — * {@link Agent} on the SAME cordis context (`ctx.agents`) — the cheapest
* the cheapest transport, reusing the agent factory's quiescent * transport, reusing the agent factory's quiescent {@link AgentHandle}
* {@link AgentHandle} teardown. Both in-process backends use this: * teardown. The concrete in-process backends are thin shells over this driver,
* `@deepseek-ai/dsh-subagent-spawn` (a fresh child) and * differing ONLY in the `seed` they pass (a fresh child vs. a child seeded with
* `@deepseek-ai/dsh-subagent-fork` (a child seeded with a prefix of the * a prefix of the parent's log); everything downstream — drive the child, read
* parent's log) differ ONLY in the `seed` they pass — everything downstream * its final output, map the stop reason, dispose — is identical and lives here.
* (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' import { randomUUID } from 'node:crypto'

View File

@@ -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()
})
})

View File

@@ -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"
}
]
}

View File

@@ -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. 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 ## What it does
`start(request)` → `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).
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.
## Capabilities ## Capabilities
@@ -23,7 +17,3 @@ It also exports the **shared in-process run driver** (`startInProcessRun`) that
| Key | Meaning | | Key | Meaning |
|---|---| |---|---|
| `providerName` | Registry name on `ctx.subagents` (default `spawn`). | | `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)`.

View File

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

View File

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

View File

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

View File

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

36
pnpm-lock.yaml generated
View File

@@ -360,6 +360,9 @@ importers:
'@deepseek-ai/dsh-subagent': '@deepseek-ai/dsh-subagent':
specifier: workspace:^ specifier: workspace:^
version: link:../subagent version: link:../subagent
'@deepseek-ai/dsh-subagent-inprocess':
specifier: workspace:^
version: link:../subagent-inprocess
'@deepseek-ai/dsh-subagent-spawn': '@deepseek-ai/dsh-subagent-spawn':
specifier: workspace:^ specifier: workspace:^
version: link:../subagent-spawn version: link:../subagent-spawn
@@ -373,6 +376,36 @@ importers:
specifier: ^4.0.0-rc.6 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) 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: packages/subagent/subagent-spawn:
dependencies: dependencies:
schemastery: schemastery:
@@ -406,6 +439,9 @@ importers:
'@deepseek-ai/dsh-subagent': '@deepseek-ai/dsh-subagent':
specifier: workspace:^ specifier: workspace:^
version: link:../subagent version: link:../subagent
'@deepseek-ai/dsh-subagent-inprocess':
specifier: workspace:^
version: link:../subagent-inprocess
'@deepseek-ai/dsh-system-prompt': '@deepseek-ai/dsh-system-prompt':
specifier: workspace:^ specifier: workspace:^
version: link:../../core/system-prompt version: link:../../core/system-prompt

View File

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