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:
@@ -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` |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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:^",
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"path": "../subagent"
|
"path": "../subagent"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../subagent-spawn"
|
"path": "../subagent-inprocess"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
28
packages/subagent/subagent-inprocess/README.md
Normal file
28
packages/subagent/subagent-inprocess/README.md
Normal 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`.
|
||||||
40
packages/subagent/subagent-inprocess/package.json
Normal file
40
packages/subagent/subagent-inprocess/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
@@ -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
packages/subagent/subagent-inprocess/tsconfig.json
Normal file
30
packages/subagent/subagent-inprocess/tsconfig.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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)`.
|
|
||||||
|
|||||||
@@ -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:^",
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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
36
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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" }
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user