Merge commit '5ae8c6487ad9556b8793b890a4fe5ece24c55805' into codex/product-subagent-one-shot-background

# Conflicts:
#	packages/subagent/subagent-codex/README.i18n.yaml
#	packages/subagent/subagent-codex/README.zh.md
#	packages/subagent/tool-subagent/tests/tool-subagent.spec.ts
This commit is contained in:
pku-xht
2026-08-13 15:50:23 +08:00
4174 changed files with 66693 additions and 36015 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/README.md
README.md: 134028c9993255464c08d912d305c65ed85d65c0
README.zh.md: ab4284d0525ceed349a778385c918a9dec19406f
README.md: a863ed3f5ef864b6eb6eb9a7a0c1ee2f40f247d6
README.zh.md: 1c9bf8ba0814a74c5774d81a34a3266daa9c375b

View File

@@ -7,9 +7,9 @@ This family lets an agent delegate work to child agents. Multiple named provider
| Package | Role | ctx key |
|---|---|---|
| [`subagent/`](subagent/README.md) | Defines provider registration, delegation, and continuation | `ctx.subagents` |
| [`subagent-inprocess/`](subagent-inprocess/README.md) | Provides the shared in-process run driver | — |
| [`subagent-spawn/`](subagent-spawn/README.md) | Starts a fresh in-process child | registers on `ctx.subagents` |
| [`subagent-fork/`](subagent-fork/README.md) | Starts an in-process child from the parent's completed history | registers on `ctx.subagents` |
| [`subagent-inprocess/`](subagent-in-process-driver/README.md) | Provides the shared in-process run driver | — |
| [`subagent-spawn-in-process/`](subagent-spawn-in-process/README.md) | Starts a fresh in-process child | registers on `ctx.subagents` |
| [`subagent-fork-in-process/`](subagent-fork-in-process/README.md) | Starts an in-process child from the parent's completed history | registers on `ctx.subagents` |
| [`subagent-acp/`](subagent-acp/README.md) | Starts an out-of-process child over ACP | registers on `ctx.subagents` |
| [`subagent-codex/`](subagent-codex/README.md) | Starts a real Codex app-server child | registers on `ctx.subagents` |
| [`subagent-claude-code/`](subagent-claude-code/README.md) | Starts a real Claude Code child through the official Claude Agent SDK | registers on `ctx.subagents` |

View File

@@ -7,9 +7,9 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
| [`subagent/`](subagent/README.md) | 定义提供方注册、委派和继续执行 | `ctx.subagents` |
| [`subagent-inprocess/`](subagent-inprocess/README.md) | 提供共享的进程内运行驱动器 | 无 |
| [`subagent-spawn/`](subagent-spawn/README.md) | 启动全新的进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-fork/`](subagent-fork/README.md) | 从父 agent 已完成的历史记录启动进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-inprocess/`](subagent-in-process-driver/README.md) | 提供共享的进程内运行驱动器 | 无 |
| [`subagent-spawn-in-process/`](subagent-spawn-in-process/README.md) | 启动全新的进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-fork-in-process/`](subagent-fork-in-process/README.md) | 从父 agent 已完成的历史记录启动进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-acp/`](subagent-acp/README.md) | 通过 ACP(Agent Client Protocol)启动进程外子 agent | 注册到 `ctx.subagents` |
| [`subagent-codex/`](subagent-codex/README.md) | 启动真实的 Codex app-server 子 agent | 注册到 `ctx.subagents` |
| [`subagent-claude-code/`](subagent-claude-code/README.md) | 通过官方 Claude Agent SDK 启动真实的 Claude Code 子 agent | 注册到 `ctx.subagents` |
@@ -20,4 +20,4 @@
参见有关[能力家族](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续执行的子级](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)和[控制工具](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)的决策。
子系统参考——启动请求、结果、实时运行、提供方约定、可续跑后台子 agent——见 [docs/subsystems/subagent.md](../../docs/subsystems/subagent.md);设计依据见 [subagent 能力 seam](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可续跑后台 subagent](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md) 与[合并 subagent 控制服务](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md) Agent Note。
子系统参考——启动请求、结果、实时运行、提供方约定、可续跑后台子 agent——见 [docs/subsystems/subagent.md](../../docs/subsystems/subagent.md);设计依据见 [subagent 能力 seam](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可续跑后台 subagent](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)与[合并 subagent 控制服务](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md) Agent Note。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-acp/README.md
README.md: 3bccddbca021bed1f8bf5766b9575f3bd7441669
README.zh.md: 80afd65e5f4815042f05c22e4597bbb2677fb4fc
README.zh.md: 38c882f240dcdd02213acecf266f5a2424b37cce

View File

@@ -6,7 +6,7 @@ ACP(Agent Client Protocol)提供方会在全新的子进程中运行每个 s
## 启动与所有权
`start(request)` 先解析子 agent 的工作目录,再依次执行 `spawn` → ACP `initialize` → `newSession`,然后才兑现。因此,兑现表示远程会话已就绪,所有权也已转移给调用方。spawn、初始化、新建会话或发布前取消失败时,只有在子进程已回收后才会拒绝;工作目录解析失败则会在尚未 spawn 任何内容时拒绝。
`start(request)` 先解析子 agent 的工作目录,再依次执行 `spawn` → ACP `initialize` → `newSession`,然后才兑现。因此,兑现表示远程会话已就绪,所有权也已转移给调用方。spawn 失败、初始化失败、新建会话失败或因发布前取消而失败时,只有在子进程已回收后才会拒绝;工作目录解析失败则会在尚未 spawn 任何进程时拒绝。
工作目录优先使用已配置的 `cwd` 覆盖值,否则使用执行委派的父会话 cwd,绝不使用服务器进程自身的 cwd,因为同一个服务器进程会服务来自多个工作区的会话。从父级取得的值必须是绝对路径,指向 harness 可以进入的目录(具备搜索权限,这是子进程 cwd 的要求);解析后的同一路径同时作为子进程 cwd 和 ACP `session/new` 工作区。
@@ -14,7 +14,7 @@ ACP(Agent Client Protocol)提供方会在全新的子进程中运行每个 s
发布后,提供方发送提示词,并把流式 `agent_message_chunk` 文本收集到 `SubagentResult.output`。提示词/传输失败会以 `stopReason: 'error'` 兑现;如果必需的请求信号或 dispose(资源释放)请求了取消,则以 `aborted` 兑现。
`dispose()` 是幂等的。它会移除信号监听器,在可行时请求 ACP 取消,然后经由该 seam 的动词运行本后端自有的拆卸阶梯(`disposeAcpChild`):先关闭 stdin 并等待 `disposeEofGraceMs` 让子进程协作式完全停稳,再触发句柄的 `terminate()` 升级(SIGTERM、spawn 宽限期、SIGKILL——Windows 直接强制终止),并等待子进程责任方给出整棵进程树的退出证明。每次运行都使用全新进程;尚未实现进程池。
`dispose()` 是幂等的。它会移除信号监听器,在可行时请求 ACP 取消,然后使用该 seam 定义的操作运行本后端自有的拆卸阶梯(`disposeAcpChild`):先关闭 stdin 并等待 `disposeEofGraceMs` 让子进程协作式完全停稳,再触发句柄的 `terminate()` 升级(SIGTERM、spawn 宽限期、SIGKILL——Windows 直接强制终止),并等待子进程责任方给出整棵进程树的退出证明。每次运行都使用全新进程;尚未实现进程池。
## 能力与上下文
@@ -94,7 +94,7 @@ ACP 不声明任何启动时能力,因为当前进程无法强制执行远程
## 已知限制与暂缓事项
- **每次运行使用全新进程**:持久进程池属于后续优化(见 [seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md))。
- **仅支持本地工作区**:解析后的 cwd 是交给同一台机器上子进程的本地路径;远程 ACP agent 的工作区映射需要独立的后端能力,本包尚未设计。
- **仅支持本地工作区**:解析后的 cwd 是交给同一台机器上子进程的本地路径;远程 ACP agent 的工作区映射需要独立的后端能力,此处尚未设计这种能力。
- **不支持可选启动时能力**:该提供方无法在远程进程内应用本地 harness 的 `outputSchema`、深度上限、工具过滤器或 persona,因此不会声明这些能力;服务会拒绝需要它们的请求。
- **只收集已提交的 `agent_message_chunk` 文本**:自动化服务器把推理(reasoning)、工具活动、计划和其他 trace 数据保留在子 agent 会话日志中,不通过 ACP 发出。
- **权限提示自动回答**(`permission: allow | reject`):不会把子 agent 的 `session/request_permission` 呈现给人。

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-subagent-acp",
"description": "Out-of-process ACP subagent backend: drives a child agent in a spawned subprocess over the Agent Client Protocol",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -5,8 +5,8 @@ import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
import * as acp from '../src/index.ts'
@@ -52,8 +52,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('ACP backend with-key e2e (drive
it('drives the real acp-agent example process to answer a prompt', async () => {
workdir = await mkdtemp(join(tmpdir(), 'dsh-subagent-acp-e2e-'))
ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: childLaunch.command,
@@ -82,8 +82,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('ACP backend with-key e2e (drive
it('drives the child to do real file work via its own bash tool', async () => {
workdir = await mkdtemp(join(tmpdir(), 'dsh-subagent-acp-e2e-'))
ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: childLaunch.command,

View File

@@ -5,13 +5,13 @@ import { chmodSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import type { SubprocessOutcome } from '@deepseek-ai/dsh-subprocess'
import * as acp from '../src/index.ts'
import { acpStopReason, acpContentText, DEFAULT_DISPOSE_EOF_GRACE_MS, DEFAULT_DISPOSE_GRACE_MS, disposeAcpChild, startAcpRun, toAcpPrompt, type AcpRunSpec } from '../src/run.ts'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { spawnSubprocess } from '@deepseek-ai/dsh-subprocess-local/src/spawn.ts'
/**
@@ -43,8 +43,8 @@ interface SetupEnv {
*/
async function setup(mockEnv: SetupEnv = {}, permission: 'allow' | 'reject' = 'reject') {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: process.execPath,
@@ -222,8 +222,8 @@ describe('cwd resolution', () => {
const sentinel = join(tmp, 'spawned')
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
// A command that would create the sentinel if the child were ever spawned.
await ctx.plugin(acp, { providerName: 'acp', command: 'touch', args: [sentinel], permission: 'reject', env: {} })
const parent = { id: 'parent', session: { header: {} } } as unknown as Agent
@@ -241,8 +241,8 @@ describe('cwd resolution', () => {
const parentDir = realpathSync(mkdtempSync(join(tmpdir(), 'acp-parent-cwd-')))
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: process.execPath,
@@ -269,8 +269,8 @@ describe('cwd resolution', () => {
const relative = 'packages/subagent/subagent-acp'
const absolute = resolve(relative)
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: process.execPath,
@@ -289,8 +289,8 @@ describe('cwd resolution', () => {
// `path.resolve('')` is the process cwd, so an empty string would silently
// reintroduce the launch-directory fallback this resolution removed.
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await expect(ctx.plugin(acp, {
providerName: 'acp',
command: 'true',
@@ -310,8 +310,8 @@ describe('cwd resolution', () => {
chmodSync(tmp, 0o600)
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await expect(ctx.plugin(acp, {
providerName: 'acp',
command: 'true',
@@ -329,8 +329,8 @@ describe('cwd resolution', () => {
it('rejects a config cwd that is not an accessible directory at load', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await expect(ctx.plugin(acp, {
providerName: 'acp',
command: 'true',
@@ -371,8 +371,8 @@ describe('cwd resolution', () => {
const sentinel = join(tmp, 'spawned')
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, { providerName: 'acp', command: 'touch', args: [sentinel], permission: 'reject', env: {} })
const parent = { id: 'parent', session: { header: { cwd: join(tmp, 'vanished') } } } as unknown as Agent
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
@@ -694,8 +694,8 @@ describe('dsh-subagent-acp', () => {
const ready = join(tmp, 'trap-armed')
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: process.execPath,
@@ -727,8 +727,8 @@ describe('dsh-subagent-acp', () => {
{ disposeGraceMs: MAX_TIMER_DELAY_MS + 1 },
]) {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await expect(ctx.plugin(acp, { providerName: 'acp', command: 'true', args: [], permission: 'reject', env: {}, ...bad }))
.rejects.toThrow(new RegExp(`subagent-acp: dispose(?:Eof)?GraceMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`))
await ctx.fiber.dispose()
@@ -737,8 +737,8 @@ describe('dsh-subagent-acp', () => {
it('rejects a startup failure via the provider load path', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: '/nonexistent/acp-agent-binary',
@@ -868,8 +868,8 @@ describe('dsh-subagent-acp', () => {
it('unregisters the provider when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const fiber = await ctx.plugin(acp, { providerName: 'acp', command: 'x', args: [], permission: 'reject', env: {} })
expect(ctx.subagents.list()).toEqual(['acp'])
await fiber.dispose()

View File

@@ -33,10 +33,10 @@
"path": "../../util/timeout"
},
{
"path": "../../support/loader-smoke"
"path": "../../test-support/loader-smoke"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md
README.md: f0f6e39820920e0b266802585b74a953a068828a
README.zh.md: 52baee6c1158bb38505fd0cf454a820853e0cf89
README.md: 816d89229711021fc2e3c315cec88d7b7d846793
README.zh.md: 53c9c2a2d3192ac0ed0d23f348a54c8dff21845f

View File

@@ -31,7 +31,7 @@ The provider advertises no optional start-time capabilities and reports `inherit
Production resolves `claude` from the subprocess execution world's credential-scrubbed `PATH`, with explicit `env` entries applied, and passes the resulting path to the SDK as `pathToClaudeCodeExecutable`. On Windows, a resolved `.cmd` or `.bat` path is carried as a quoted, per-spawn environment value that `cmd.exe /v:off` expands once, so valid path metacharacters remain data. The pinned SDK's fixed flags then occupy cmd's command tail and contain no cmd metacharacters; they are not ordinary Windows argv. Native settings and authentication remain authoritative. The plugin does not install another CLI, select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed before the explicit `env` overlay is applied, so an API key or token intended for the child must be supplied there. Non-credential endpoint variables such as `ANTHROPIC_BASE_URL`, along with ordinary ambient values such as `PATH` and `HOME`, remain inherited unless overridden.
Shipped profiles load this provider once on the host and start no Claude process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Task id for `task_output` or `task_kill`. Shipped full profiles already provide the task registry and controls; a custom composition must load the same generic task provider and consumer when it enables this background route.
Shipped profiles load this provider once on the host and start no Claude process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. Shipped full profiles already provide the job registry and controls; a custom composition must load the same generic job provider and consumer when it enables this background route.
```yaml
- id: subagent-claude-code
@@ -40,11 +40,11 @@ Shipped profiles load this provider once on the host and start no Claude process
env:
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
- id: tasks
name: '@deepseek-ai/dsh-tasks-local'
- id: jobs
name: '@deepseek-ai/dsh-jobs-local'
- id: tool-tasks
name: '@deepseek-ai/dsh-tool-tasks'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
- id: tool-subagent-claude-code
name: '@deepseek-ai/dsh-tool-subagent'
@@ -82,15 +82,15 @@ Independent of the parent request cache. Reuse depends only on Claude Code's own
#### What the model sees
Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Job id; the generic job controls later deliver a completion notice, expose the final answer and status through `job_output`, and let `job_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
#### Token effect
Foreground input grows by the retained final answer or error. Background input also includes the start acknowledgement, completion notice, and any `task_output`, `task_kill`, or later status results; child tokens still do not enter the parent context. This provider adds no parent tool schema by itself.
Foreground input grows by the retained final answer or error. Background input also includes the start acknowledgement, completion notice, and any `job_output`, `job_kill`, or later status results; child tokens still do not enter the parent context. This provider adds no parent tool schema by itself.
#### KV Cache effect
Append-only: foreground adds one result after the reusable parent prefix, while background appends the Task acknowledgement, notice, and later control or collection results. Background scheduling can add a notice-driven turn, but none of these messages rewrites the earlier prefix.
Append-only: foreground adds one result after the reusable parent prefix, while background appends the Job acknowledgement, notice, and later control or collection results. Background scheduling can add a notice-driven turn, but none of these messages rewrites the earlier prefix.
## Known Limitations and Deferred Work
@@ -99,6 +99,6 @@ Append-only: foreground adds one result after the reusable parent prefix, while
- **Product installation and account state remain native** — a missing or incompatible `claude`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer or login flow.
- **The SDK platform CLI remains in the install closure** — production ignores it in favor of the host `claude`, but the current SDK optional dependency is still installed and supplies the keyless compatibility fixture. Removing that payload belongs to the separate product installation-closure follow-up.
- **No human interaction path** — `AskUserQuestion` is disabled and other interactive callbacks are absent, so tasks requiring new approval or input fail instead of suspending.
- **Product payload is final text only** — reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Task ids, notices, and status come from the shared task runtime.
- **Product payload is final text only** — reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Job ids, notices, and status come from the shared job runtime.
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.
- **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored.

View File

@@ -10,7 +10,7 @@
SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK 消息流,而且只接受满足以下条件的 `result` 消息:其 `subtype: "success"`、`is_error: false` 且 `result` 非空白,之后迭代器还须正常结束。所有 SDK 错误子类型、标记为错误的成功消息、缺失答案、迭代器失败、协议失败或进程失败都映射为 `error`;该提供方不会产生 `max-tokens` 或 `refusal`。
本地取消会在结果竞态中胜出并映射为 `aborted`。`dispose()` 具有幂等性:它会中止此次运行、请求 SDK query 关闭、调用共享的进程树逐级终止机制,并等待整棵进程树退出。SDK 的优雅关闭只表达协议意图;进程是否完全停稳仍以子进程句柄为准。结果失败与独立的清理失败仍彼此分离。
本地取消会在结果竞态中胜出并映射为 `aborted`。`dispose()`(资源释放)具有幂等性:它会中止此次运行、请求 SDK query 关闭、调用共享的进程树逐级终止机制,并等待整棵进程树退出。SDK 的优雅关闭只表达协议意图;进程是否完全停稳仍以子进程句柄为准。结果失败与独立的清理失败仍彼此分离。
## 原生设置与交互
@@ -31,7 +31,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
生产环境从子进程执行世界清除凭证后的 `PATH` 解析 `claude`,再应用显式 `env` 条目,并把所得路径作为 `pathToClaudeCodeExecutable` 交给 SDK。在 Windows 上,解析到的 `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境值交给 `cmd.exe /v:off` 展开一次,因此合法路径中的元字符仍只是数据。锁定版本的 SDK 随后把固定命令行选项放在 cmd 的命令尾部;这些选项不含 cmd 元字符,也并不是普通的 Windows argv。原生设置与身份验证继续是权威来源。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录,也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH` 和 `HOME` 等普通环境变量仍会被继承。
随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Claude 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Task ID,供 `task_output` 或 `task_kill` 使用。随附完整 profile 已提供任务注册表和控制工具;自定义组装若启用该后台路径,也必须加载同一通用任务提供方与消费方。
随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Claude 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。随附完整 profile 已提供作业注册表和控制工具;自定义组装若启用该后台路径,也必须加载同一通用作业提供方与消费方。
```yaml
- id: subagent-claude-code
@@ -40,11 +40,11 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
env:
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
- id: tasks
name: '@deepseek-ai/dsh-tasks-local'
- id: jobs
name: '@deepseek-ai/dsh-jobs-local'
- id: tool-tasks
name: '@deepseek-ai/dsh-tool-tasks'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
- id: tool-subagent-claude-code
name: '@deepseek-ai/dsh-tool-subagent'
@@ -60,19 +60,19 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`。生产运行使用原生 `claude` 安装。无密钥真实产品测试使用由 SDK 分发的 Claude Code 2.1.220 CLI 作为确定性 fixture(测试前置数据),并通过同一套原生可执行文件解析路径与 Windows batch shim 路径运行;这项测试不声称兼容每个独立安装的版本。Loader 组合证明两个产品包能够共存且不会启动任一产品。
项目所有者按身份范围授权分发官方 SDK 及每个 SDK 版本声明的官方 CLI/平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) 会披露当前可选载荷闭包,但不会把其声明条款归类为宽松许可证;其他无关的非宽松运行时依赖仍会使第三方声明门禁失败。
限定于项目所有者身份的分发授权涵盖官方 SDK 及每个 SDK 版本声明的官方 CLI/平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) 会披露当前可选载荷闭包,但不会认定其中声明的条款属于宽松许可;其他无关的非宽松运行时依赖仍会使第三方声明门禁失败。
## 模型体验
### 子任务请求
### 子级请求
#### 模型看到的内容
Claude Code 子任务会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、权限和身份验证来自宿主机原生 Claude 设置与产品安装。
Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、权限和身份验证来自宿主机原生 Claude 设置与产品安装。
#### 对 token 的影响
子任务需为独立的 Claude Code 上下文和 query 承担 token 开销。子任务 token 不会进入父级上下文。
子级需为独立的 Claude Code 上下文和 query 承担 token 开销。子级 token 不会进入父级上下文。
#### 对 KV Cache 的影响
@@ -82,15 +82,15 @@ Claude Code 子任务会在一个全新的 SDK query 中接收独立文本任务
#### 模型看到的内容
通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Job id;随后通用作业控制面会送达完成通知,通过 `job_output` 公开最终答案与状态,并允许 `job_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
#### 对 token 的影响
前台输入会增加工具结果中保留的最终答案或错误内容。后台输入还会包含启动确认、完成通知,以及 `task_output`、`task_kill` 或后续状态结果;子任务 token 仍不会进入父级上下文。本提供方自身不添加父级工具 schema。
前台输入会增加工具结果中保留的最终答案或错误内容。后台输入还会包含启动确认、完成通知,以及 `job_output`、`job_kill` 或后续状态结果;子任务 token 仍不会进入父级上下文。本提供方自身不添加父级工具 schema。
#### 对 KV Cache 的影响
仅追加:前台会在可复用的父请求前缀后增加一个结果,后台则会继续追加 Task 启动确认、通知以及后续控制或收集结果。后台调度可能增加一个由通知唤醒的轮次,但这些消息都不会改写更早的前缀。
仅追加:前台会在可复用的父请求前缀后增加一个结果,后台则会继续追加 Job 启动确认、通知以及后续控制或收集结果。后台调度可能增加一个由通知唤醒的轮次,但这些消息都不会改写更早的前缀。
## 已知限制与后续工作
@@ -99,6 +99,6 @@ Claude Code 子任务会在一个全新的 SDK query 中接收独立文本任务
- **产品安装与账户状态仍由原生机制管理**:`claude` 缺失或不兼容、配置错误或身份验证失败都会呈现为启动错误或运行错误;本插件不提供安装程序或登录流程。
- **SDK 平台 CLI 仍在安装闭包内**:生产环境会忽略它,改用宿主提供的 `claude`,但当前 SDK 的可选依赖仍会安装,并提供无密钥兼容性 fixture。移除该载荷属于独立的产品安装闭包后续项。
- **没有人工交互路径**:`AskUserQuestion` 被禁用,其他交互回调也不存在,因此需要新审批或输入的任务会失败而不会挂起。
- **产品载荷仅包含最终文本**:推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Task id、通知与状态来自共享任务运行时。
- **产品载荷仅包含最终文本**:推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Job id、通知与状态来自共享作业运行时。
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。
- **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-subagent-claude-code",
"description": "One-shot Claude Code subagent provider over the official Agent SDK",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",

View File

@@ -15,7 +15,7 @@ const configPath = join(fixtureDir, 'cordis.yml')
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
describe('product-provider public Loader composition', () => {
it('loads both opt-in packages, one-shot task tools, and task controls without starting either product', async () => {
it('loads both opt-in packages, one-shot task tools, and job controls without starting either product', async () => {
const { stdout, stderr } = await runLoaderSmoke({
label: 'product-provider Loader composition',
tempDirPrefix: 'dsh-product-provider-loader-',
@@ -66,7 +66,7 @@ describe('product-provider public Loader composition', () => {
required: ['description', 'prompt'],
},
],
taskTools: ['task_kill', 'task_list', 'task_output'],
jobTools: ['job_kill', 'job_list', 'job_output'],
starts: 0,
})
}, LOADER_SMOKE_TEST_TIMEOUT_MS)

View File

@@ -13,9 +13,9 @@ import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as claudeCode from '../src/index.ts'
const execFileAsync = promisify(execFile)
@@ -113,8 +113,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const handles: SubprocessHandle[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {

View File

@@ -19,9 +19,9 @@ import type {
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as claudeCode from '../src/index.ts'
import {
startMessagesFixture,
@@ -150,8 +150,8 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const handles: SubprocessHandle[] = []
const spawnSpecs: SubprocessSpawnSpec[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)

View File

@@ -20,13 +20,13 @@ import {
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type {
SubprocessHandle,
SubprocessOutcome,
SubprocessSpawnSpec,
} from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import * as claudeCode from '../src/index.ts'
import * as invariant from '../src/invariant.ts'
@@ -296,8 +296,8 @@ describe('task admission and package contracts', () => {
it('registers one fixed descriptor, validates config, and unregisters on HMR', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const fiber = await ctx.plugin(claudeCode, {})
expect(ctx.subagents.getProvider('claude-code')).toMatchObject({
name: 'claude-code',
@@ -327,8 +327,8 @@ describe('task admission and package contracts', () => {
it('starts through the registered provider with its resolved config and diagnostics', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const child = fakeChild()
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
.mockImplementation(() => child.handle)

View File

@@ -31,7 +31,7 @@
"path": "../../util/timeout"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md
README.md: 570a2346b121a3d100600285f225a0906c38aeec
README.zh.md: 9fded2ceb35b276f0c698c937720ea13e13c6acc
README.md: 271130f41a8d4c9f22c5dfb030f60c3addea51c7
README.zh.md: 48fa219dcf2c9f55359d5b65d9a9eddb5320753a

View File

@@ -27,7 +27,7 @@ The provider advertises no optional start-time capabilities and reports `inherit
Production resolves `codex` from `PATH` and uses the host's native Codex configuration and authentication. The plugin does not install Codex, select a model, create `CODEX_HOME`, log in, or probe a version. Credential-shaped ambient variables are removed by the subprocess seam, so an API key intended for the child must be supplied explicitly in `env`; ordinary ambient values such as `PATH` and `HOME` remain available unless overridden.
Shipped profiles load this provider once on the host and start no Codex process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Task id for `task_output` or `task_kill`. Shipped full profiles already provide the task registry and controls; a custom composition must load the same generic task provider and consumer when it enables this background route.
Shipped profiles load this provider once on the host and start no Codex process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. Shipped full profiles already provide the job registry and controls; a custom composition must load the same generic job provider and consumer when it enables this background route.
```yaml
- id: subagent-codex
@@ -36,11 +36,11 @@ Shipped profiles load this provider once on the host and start no Codex process
env:
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
- id: tasks
name: '@deepseek-ai/dsh-tasks-local'
- id: jobs
name: '@deepseek-ai/dsh-jobs-local'
- id: tool-tasks
name: '@deepseek-ai/dsh-tool-tasks'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
@@ -76,15 +76,15 @@ Independent of the parent request cache. Reuse depends only on Codex's own provi
#### What the model sees
Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Job id; the generic job controls later deliver a completion notice, expose the final answer and status through `job_output`, and let `job_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
#### Token effect
Foreground input grows by the retained final answer or error. Background input also includes the start acknowledgement, completion notice, and any `task_output`, `task_kill`, or later status results; child tokens still do not enter the parent context. This provider adds no parent tool schema by itself.
Foreground input grows by the retained final answer or error. Background input also includes the start acknowledgement, completion notice, and any `job_output`, `job_kill`, or later status results; child tokens still do not enter the parent context. This provider adds no parent tool schema by itself.
#### KV Cache effect
Append-only: foreground adds one result after the reusable parent prefix, while background appends the Task acknowledgement, notice, and later control or collection results. Background scheduling can add a notice-driven turn, but none of these messages rewrites the earlier prefix.
Append-only: foreground adds one result after the reusable parent prefix, while background appends the Job acknowledgement, notice, and later control or collection results. Background scheduling can add a notice-driven turn, but none of these messages rewrites the earlier prefix.
## Known Limitations and Deferred Work
@@ -92,6 +92,6 @@ Append-only: foreground adds one result after the reusable parent prefix, while
- **Host-managed product installation and account state** — a missing or incompatible `codex`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer, login flow, or runtime version gate.
- **Compatibility is pinned by development evidence** — upgrading from the verified 0.147.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests.
- **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; deployments cannot configure an allow policy through this package.
- **Product payload is final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Task ids, notices, and status come from the shared task runtime.
- **Product payload is final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Job ids, notices, and status come from the shared job runtime.
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.
- **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
本包(package)注册固定的 `codex` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中启动官方 `codex app-server --stdio` 命令,创建一个临时 Codex 线程,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
本包注册固定的 `codex` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中启动官方 `codex app-server --stdio` 命令,创建一个临时 Codex 线程,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
## 启动与所有权
@@ -12,7 +12,7 @@
对于命令与文件审批,无人值守的提供方会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.147.0 请求形态没有决策选项列表,因此回退到 `decline`。它对权限请求返回作用域限于当前轮次的空权限集,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败。
本地取消会在结果竞态中胜出并映射为 `aborted`。失败轮次的 `codexErrorInfo` 若为 `contextWindowExceeded`,则映射为 `max-tokens`;其他任何远端中断或失败轮次都映射为 `error`,且该提供方不会产生 `refusal`。`dispose()` 具有幂等性:如果当前的两个标识符均已知,它会尽力请求 `turn/interrupt`,关闭 JSON-RPC 通信链路,结束标准输入,调用共享的进程树逐级终止机制,并等待整棵进程树退出。结果失败与独立的清理失败仍彼此分离。
本地取消会在结果竞态中胜出并映射为 `aborted`。失败轮次的 `codexErrorInfo` 若为 `contextWindowExceeded`,则映射为 `max-tokens`;其他任何远端中断或失败轮次都映射为 `error`,且该提供方不会产生 `refusal`。`dispose()`(资源释放)具有幂等性:如果当前的两个标识符均已知,它会尽力请求 `turn/interrupt`,关闭 JSON-RPC 通信链路,结束标准输入,调用共享的进程树逐级终止机制,并等待整棵进程树退出。结果失败与独立的清理失败仍彼此分离。
## 能力与上下文
@@ -27,7 +27,7 @@
生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH` 和 `HOME` 等普通环境变量值仍然可用。
随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Codex 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Task ID,供 `task_output` 或 `task_kill` 使用。随附完整 profile 已提供任务注册表和控制工具;自定义组装若启用该后台路径,也必须加载同一通用任务提供方与消费方。
随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Codex 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。随附完整 profile 已提供作业注册表和控制工具;自定义组装若启用该后台路径,也必须加载同一通用作业提供方与消费方。
```yaml
- id: subagent-codex
@@ -36,11 +36,11 @@
env:
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
- id: tasks
name: '@deepseek-ai/dsh-tasks-local'
- id: jobs
name: '@deepseek-ai/dsh-jobs-local'
- id: tool-tasks
name: '@deepseek-ai/dsh-tool-tasks'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
@@ -58,15 +58,15 @@
## 模型体验
### 子任务请求
### 子级请求
#### 模型看到的内容
Codex 子任务会在一个全新的临时线程中,以单个轮次接收这些独立文本块。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自原生 Codex 安装与配置。
Codex 子级会在一个全新的临时线程中,以单个轮次接收这些独立文本块。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自原生 Codex 安装与配置。
#### 对 token 的影响
子任务需为独立的 Codex 上下文和轮次承担 token 开销。子任务 token 不会进入父级上下文。
子级需为独立的 Codex 上下文和轮次承担 token 开销。子级 token 不会进入父级上下文。
#### 对 KV Cache 的影响
@@ -76,15 +76,15 @@ Codex 子任务会在一个全新的临时线程中,以单个轮次接收这
#### 模型看到的内容
通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Codex 的过程说明、推理(reasoning)、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Job id;随后通用作业控制面会送达完成通知,通过 `job_output` 公开最终答案与状态,并允许 `job_kill` 请求取消。Codex 的过程说明、推理(reasoning)、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
#### 对 token 的影响
前台输入会增加工具结果中保留的最终答案或错误内容。后台输入还会包含启动确认、完成通知,以及 `task_output`、`task_kill` 或后续状态结果;子任务 token 仍不会进入父级上下文。本提供方自身不添加父级工具 schema。
前台输入会增加工具结果中保留的最终答案或错误内容。后台输入还会包含启动确认、完成通知,以及 `job_output`、`job_kill` 或后续状态结果;子任务 token 仍不会进入父级上下文。本提供方自身不添加父级工具 schema。
#### 对 KV Cache 的影响
仅追加:前台会在可复用的父请求前缀后增加一个结果,后台则会继续追加 Task 启动确认、通知以及后续控制或收集结果。后台调度可能增加一个由通知唤醒的轮次,但这些消息都不会改写更早的前缀。
仅追加:前台会在可复用的父请求前缀后增加一个结果,后台则会继续追加 Job 启动确认、通知以及后续控制或收集结果。后台调度可能增加一个由通知唤醒的轮次,但这些消息都不会改写更早的前缀。
## 已知限制与后续工作
@@ -92,6 +92,6 @@ Codex 子任务会在一个全新的临时线程中,以单个轮次接收这
- **产品安装和账户状态由宿主管理**:`codex` 缺失或不兼容、配置错误或身份验证失败,都会呈现为启动错误或运行错误;本插件不提供安装程序、登录流程或运行时版本门禁。
- **兼容性由开发证据锁定**:若要从已验证的 0.147.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
- **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;部署方无法通过本包配置允许策略。
- **产品载荷仅包含最终文本**:推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Task id、通知与状态来自共享任务运行时。
- **产品载荷仅包含最终文本**:推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Job id、通知与状态来自共享作业运行时。
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。
- **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-subagent-codex",
"description": "One-shot Codex subagent provider over the official app-server protocol",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",

View File

@@ -15,7 +15,7 @@ const configPath = join(fixtureDir, 'cordis.yml')
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
describe('Codex provider public Loader composition', () => {
it('loads the opt-in package, one-shot task tool, and task controls without starting Codex', async () => {
it('loads the opt-in package, one-shot task tool, and job controls without starting Codex', async () => {
const { stdout, stderr } = await runLoaderSmoke({
label: 'subagent-codex Loader composition',
tempDirPrefix: 'dsh-subagent-codex-loader-',
@@ -47,7 +47,7 @@ describe('Codex provider public Loader composition', () => {
parameterNames: ['description', 'prompt', 'run_in_background'],
required: ['description', 'prompt'],
},
taskTools: ['task_kill', 'task_list', 'task_output'],
jobTools: ['job_kill', 'job_list', 'job_output'],
starts: 0,
})
}, LOADER_SMOKE_TEST_TIMEOUT_MS)

View File

@@ -14,9 +14,9 @@ import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
import {
startDeepSeekResponsesBridge,
@@ -96,8 +96,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const handles: SubprocessHandle[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {

View File

@@ -14,9 +14,9 @@ import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
import {
startResponsesFixture,
@@ -97,8 +97,8 @@ async function realHarness(script: readonly ResponsesBehavior[]): Promise<{
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const handles: SubprocessHandle[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {

View File

@@ -5,13 +5,13 @@ import { describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import type {
SubprocessHandle,
SubprocessOutcome,
} from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
import * as invariant from '../src/invariant.ts'
import {
@@ -287,8 +287,8 @@ describe('task admission and package contracts', () => {
it('registers one fixed descriptor, validates config, and unregisters on HMR', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const fiber = await ctx.plugin(codex, {})
const provider = ctx.subagents.getProvider('codex')!
expect(provider).toMatchObject({
@@ -316,8 +316,8 @@ describe('task admission and package contracts', () => {
it('requires a parent session cwd without suggesting unsupported config', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
await ctx.plugin(codex, {})
@@ -1014,8 +1014,8 @@ describe('run lifecycle and quiescence', () => {
it('uses the registered provider config and logs flattened errors', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const child = fakeChild()
const spawn = vi.spyOn(ctx.subprocess, 'spawn').mockReturnValue(child.handle)
const warnings: string[] = []

View File

@@ -39,7 +39,7 @@
"path": "../../util/timeout"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-subagent-dsh-sdk",
"description": "Out-of-process SDK subagent backend: drives a child DeepSeek Harness runtime subprocess over stdio JSON-RPC through the TypeScript SDK client",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -90,7 +90,7 @@ type ResolvedConfig = Required<Omit<Config, 'cwd' | 'maxTokens'>> & Pick<Config,
* child cannot honor `outputSchema`/`maxDepth`/`toolFilter`/`persona` (the
* service rejects a request needing any of them before `start` runs).
*/
class SdkProvider implements SubagentProvider {
class SdkSubagentProvider implements SubagentProvider {
readonly capabilities: SubagentCapabilities = NO_START_CAPABILITIES
// Context contract: an out-of-process SDK child starts fresh — no parent conversation crosses the process boundary.
readonly inheritsParentContext = false
@@ -134,5 +134,5 @@ export function apply(ctx: Context, config: Config): void {
const validated: ResolvedConfig = configuredCwd === undefined
? resolved
: { ...resolved, cwd: configuredCwd }
ctx.subagents.registerProvider(new SdkProvider(validated.providerName, ctx, validated))
ctx.subagents.registerProvider(new SdkSubagentProvider(validated.providerName, ctx, validated))
}

View File

@@ -12,7 +12,7 @@ import { existsSync, mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import * as sdk from '../src/index.ts'
import {
@@ -36,7 +36,7 @@ function request(text = 'p', signal = new AbortController().signal) {
/** Mount the SDK backend pointed at the fake runtime, scripted by `fakeEnv`. */
async function setup(fakeEnv: Record<string, string> = {}, config: Partial<sdk.Config> = {}) {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
// The Config type models the post-validation shape, so the default registry
// name is stated here; the Loader-composition fixture omits providerName and
// exercises the schemastery default end to end.
@@ -361,7 +361,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
it('registers under the configured provider name and unregisters on fiber dispose (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(sdk, {
providerName: 'sdk-hmr',
command: process.execPath,
@@ -385,7 +385,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
it('rejects non-positive timing bounds at load', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const base = { providerName: 'sdk', command: 'true', args: [], provider: 'p', model: 'm', env: {} }
await expect(ctx.plugin(sdk, { ...base, shutdownTimeoutMs: 0 })).rejects.toThrow('shutdownTimeoutMs must be a positive finite number')
await expect(ctx.plugin(sdk, { ...base, disposeEofGraceMs: -1 })).rejects.toThrow('disposeEofGraceMs must be a positive finite number')
@@ -397,7 +397,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
'rejects invalid maxTokens %s at load',
async (maxTokens) => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await expect(ctx.plugin(sdk, {
providerName: 'sdk',
command: 'true',
@@ -415,7 +415,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
'defensively rejects invalid maxTokens %s when apply is called directly',
async (maxTokens) => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
expect(() => { sdk.apply(ctx, {
providerName: 'sdk',
command: 'true',
@@ -434,7 +434,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
it('rejects an empty config cwd at load', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await expect(ctx.plugin(sdk, {
providerName: 'sdk',
command: 'true',

View File

@@ -36,13 +36,13 @@
"path": "../subagent"
},
{
"path": "../../support/loader-smoke"
"path": "../../test-support/loader-smoke"
},
{
"path": "../../subprocess/subprocess"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-fork/README.md
README.md: 2bd72058ab7d112f8f317a33842fc4d95b719017
README.zh.md: 40f9e34c5a8ae8c74de2ae0c9e4676866f0bd083
# pnpm run verify-translation-pairing --write packages/subagent/subagent-fork-in-process/README.md
README.md: 74c27ff10c76aa711ed3e954e806c00a27aacfa5
README.zh.md: bca890769bab1f898abd0b71fc8aa321b66e93c8

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-fork
# @deepseek-ai/dsh-subagent-fork-in-process
English | [中文](README.zh.md)
@@ -14,7 +14,7 @@ The seed transfers conversation history only. The child still receives a fresh f
## Start and capabilities
`start(request)` passes the completed-turn seed to [`startInProcessRun`](../subagent-inprocess/README.md) and awaits child publication. The shared driver owns cancellation, depth, customization, result reading, and disposal.
`start(request)` passes the completed-turn seed to [`startInProcessRun`](../subagent-in-process-driver/README.md) and awaits child publication. The shared driver owns cancellation, depth, customization, result reading, and disposal.
Fork advertises `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`, identical to spawn.
@@ -23,7 +23,7 @@ Fork advertises `{ outputSchema: true, depthLimit: true, toolFilter: true, perso
| Key | Meaning |
|---|---|
| `providerName` | Registry name on `ctx.subagents` (default `fork`). |
See [`dsh-subagent-spawn`](../subagent-spawn/README.md) for the run lifecycle, model inheritance, and depth tracking — all shared.
See [`dsh-subagent-spawn-in-process`](../subagent-spawn-in-process/README.md) for the run lifecycle, model inheritance, and depth tracking — all shared.
## Model Experience

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-fork
# @deepseek-ai/dsh-subagent-fork-in-process
[English](README.md) | 中文
@@ -14,7 +14,7 @@ subagent 启动时,父 agent 当前的工具调用轮次仍未结束:其日
## 启动与能力
`start(request)` 将已完成轮次的初始内容传给 [`startInProcessRun`](../subagent-inprocess/README.md),并等待子 agent 发布。共享驱动器负责取消、深度、定制、结果读取和 dispose(资源释放)。
`start(request)` 将已完成轮次的初始内容传给 [`startInProcessRun`](../subagent-in-process-driver/README.md),并等待子 agent 发布。共享驱动器负责取消、深度、定制、结果读取和 dispose(资源释放)。
fork 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`,与 spawn 相同。
@@ -23,7 +23,7 @@ fork 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona:
| 键 | 含义 |
|---|---|
| `providerName` | `ctx.subagents` 上的注册表名称(默认 `fork`)。 |
运行生命周期、模型继承与深度跟踪均为共享行为,见 [`dsh-subagent-spawn`](../subagent-spawn/README.md)。
运行生命周期、模型继承与深度跟踪均为共享行为,见 [`dsh-subagent-spawn-in-process`](../subagent-spawn-in-process/README.md)。
## 模型体验

View File

@@ -1,14 +1,14 @@
{
"name": "@deepseek-ai/dsh-subagent-fork",
"name": "@deepseek-ai/dsh-subagent-fork-in-process",
"description": "In-process fork subagent backend: runs a child agent seeded with a prefix of the parent's log",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/subagent/subagent-fork"
"directory": "packages/subagent/subagent-fork-in-process"
},
"type": "module",
"main": "lib/index.js",
@@ -30,13 +30,13 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
"@deepseek-ai/dsh-subagent-in-process-driver": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
@@ -51,8 +51,8 @@
"@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-subagent-in-process-driver": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -4,7 +4,7 @@
* parent's session log — so the child inherits the parent's conversation context instead of
* starting fresh. The seed ends at the last `turn/end`: the current tool-call turn is
* unbalanced and cannot be replayed as a valid child session.
* @module @deepseek-ai/dsh-subagent-fork
* @module @deepseek-ai/dsh-subagent-fork-in-process
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -18,10 +18,10 @@ import type {
SubagentCapabilities,
SubagentProvider,
} from '@deepseek-ai/dsh-subagent'
import { startInProcessRun } from '@deepseek-ai/dsh-subagent-inprocess'
import { startInProcessRun } from '@deepseek-ai/dsh-subagent-in-process-driver'
export const name = 'subagent-fork'
// `tools` is deliberately NOT injected — same rationale as subagent-spawn: the
export const name = 'subagent-fork-in-process'
// `tools` is deliberately NOT injected — same rationale as subagent-spawn-in-process: the
// per-run structured runtime gates its capture-tool registration on `tools`
// itself, so this backend's apply timing (and the delegation tool's position
// in the model-visible tool list) is unchanged by structured output.
@@ -58,7 +58,7 @@ function completedTurnPrefix(parent: Agent): SessionEvent[] {
* in-process structured runtime), plus `toolFilter`/`persona` (scoped
* restrict() and a scoped shadowing persona section).
*/
class ForkProvider implements SubagentProvider {
class ForkInProcessProvider implements SubagentProvider {
readonly capabilities: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }
// Context contract: a forked child IS seeded with the parent's completed-turn prefix.
readonly inheritsParentContext = true
@@ -90,5 +90,5 @@ class ForkProvider implements SubagentProvider {
}
export function apply(ctx: Context, config: Config): void {
ctx.subagents.registerProvider(new ForkProvider(config.providerName))
ctx.subagents.registerProvider(new ForkInProcessProvider(config.providerName))
}

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-fork`.
* @module @deepseek-ai/dsh-subagent-fork/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-fork-in-process`.
* @module @deepseek-ai/dsh-subagent-fork-in-process/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-fork'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-fork-in-process'
/** Cordis companion plugin name. */
export const name = 'subagent-fork-invariant'
export const name = 'subagent-fork-in-process-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -4,19 +4,19 @@ import { Context } from '@deepseek-ai/cordis'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn'
import SubagentRuntime, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as fork from '../src/index.ts'
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -37,7 +37,7 @@ async function setup(script: Script) {
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(Spawn, { providerName: 'spawn' })
await ctx.plugin(fork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))

View File

@@ -6,20 +6,20 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
import * as fork from '../src/index.ts'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-inprocess'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-in-process-driver'
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -44,7 +44,7 @@ async function setup(script: Script) {
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(fork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -55,7 +55,7 @@ function text(blocks: { type: string; text?: string }[]): string {
return blocks.filter(b => b.type === 'text').map(b => b.text).join('')
}
describe('dsh-subagent-fork', () => {
describe('dsh-subagent-fork-in-process', () => {
it('emits subagent/start only after the seeded child is published', async () => {
const { ctx, parent } = await setup([textResponse('child answer')])
let childAtStart: ReturnType<typeof ctx.agents.get>
@@ -201,7 +201,7 @@ describe('dsh-subagent-fork', () => {
it('unregisters the provider when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(AgentRegistry)
const fiber = await ctx.plugin(fork, { providerName: 'fork' })
expect(ctx.subagents.list()).toEqual(['fork'])
@@ -240,12 +240,12 @@ describe('dsh-subagent-fork', () => {
it('has the namespace-plugin export shape (no stray default)', () => {
expect('default' in fork).toBe(false)
expect(fork.name).toBe('subagent-fork')
expect(fork.name).toBe('subagent-fork-in-process')
expect(fork.inject).toEqual(['subagents'])
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(fork) as Record<string, unknown>
expect(unwrapped).toBe(fork)
expect(unwrapped.name).toBe('subagent-fork')
expect(unwrapped.name).toBe('subagent-fork-in-process')
expect(unwrapped.inject).toEqual(['subagents'])
expect(typeof unwrapped.apply).toBe('function')
})

View File

@@ -27,10 +27,10 @@
"path": "../subagent"
},
{
"path": "../subagent-inprocess"
"path": "../subagent-in-process-driver"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-spawn/README.md
README.md: 811f19e6e68362bd14e75d0a9059ee61fda3f015
README.zh.md: 2736143214039daa3129fd114d4294dc5fcb7d5e
# pnpm run verify-translation-pairing --write packages/subagent/subagent-in-process-driver/README.md
README.md: 47a5c09fc1c80c5dc3062be82e7355b874a627d3
README.zh.md: bcd6a2cf31c351722dfefe55e8b8ff75d4252c40

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-inprocess
# @deepseek-ai/dsh-subagent-in-process-driver
English | [中文](README.zh.md)

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-inprocess
# @deepseek-ai/dsh-subagent-in-process-driver
[English](README.md) | 中文

View File

@@ -1,14 +1,14 @@
{
"name": "@deepseek-ai/dsh-subagent-inprocess",
"name": "@deepseek-ai/dsh-subagent-in-process-driver",
"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-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/subagent/subagent-inprocess"
"directory": "packages/subagent/subagent-in-process-driver"
},
"type": "module",
"main": "lib/index.js",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -8,7 +8,7 @@
* composes and drives them directly, so this driver owns exactly one turn with
* one result.
*
* @module @deepseek-ai/dsh-subagent-inprocess
* @module @deepseek-ai/dsh-subagent-in-process-driver
*/
import { randomUUID } from 'node:crypto'

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-spawn`.
* @module @deepseek-ai/dsh-subagent-spawn/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-in-process-driver`.
* @module @deepseek-ai/dsh-subagent-in-process-driver/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-spawn'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-in-process-driver'
/** Cordis companion plugin name. */
export const name = 'subagent-spawn-invariant'
export const name = 'subagent-in-process-driver-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -7,7 +7,7 @@
* Capture commits only after the authoritative `tools/result` succeeds; Code Mode capture also
* waits for the enclosing `run_code` result. The terminal result marker and monotonic tool
* guard prevent later calls from reopening a completed structured run.
* @module @deepseek-ai/dsh-subagent-inprocess/structured
* @module @deepseek-ai/dsh-subagent-in-process-driver/structured
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -88,7 +88,7 @@ export function attachStructuredRuntime(childCtx: Context, schema: ObjectJsonSch
// within the same turn, exactly like a schema-validated defineTool call.
if (violations.length > 0) throw new ToolArgsError(violations)
// Two-phase commit, keyed by THIS execution: later transformable
// waterfalls may still turn the success into an error. ToolRegistry has
// waterfalls may still turn the success into an error. ToolRuntime has
// already frozen model-bound arguments at the actual input boundary.
staged.set(exec, { value: args })
exec.concludeTurn()

View File

@@ -4,11 +4,11 @@ import { createUserMessage, CallId, type ContentBlock, type GenerateOptions } fr
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, {
import SubagentRuntime, {
type ResolvedSubagentStartRequest,
type SubagentStartRequest,
} from '@deepseek-ai/dsh-subagent'
@@ -26,7 +26,7 @@ const testToolSignal = new AbortController().signal
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -67,7 +67,7 @@ async function setup(script: Script, options: SetupOptions = {}) {
}
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const disposeProvider = ctx.subagents.registerProvider({
name: 'spawn',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: false, persona: false },

View File

@@ -5,11 +5,11 @@ import { type Agent, type AgentOptions } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { maxTokensResponse, MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { startInProcessRun } from '../src/index.ts'
@@ -17,7 +17,7 @@ import { startInProcessRun } from '../src/index.ts'
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -28,7 +28,7 @@ async function setup(script: Script, parentOptions: Partial<AgentOptions> = {})
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const adapter = new MockAdapter(script)
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock', ...parentOptions })

View File

@@ -33,7 +33,7 @@
"path": "../../core/tools"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-inprocess/README.md
README.md: 69def8bf8f41e3685d017ac4b003b26a37f064ef
README.zh.md: bf5e7cb5cc8517ee7020695ef10e3b58d613541b
# pnpm run verify-translation-pairing --write packages/subagent/subagent-spawn-in-process/README.md
README.md: f1fb96f2230359cb3ff55c630f29fd34345dbed7
README.zh.md: 940701d6fac78ce75fda24519fa22b268085ea1a

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-spawn
# @deepseek-ai/dsh-subagent-spawn-in-process
English | [中文](README.zh.md)
@@ -6,7 +6,7 @@ The spawn provider creates a fresh child `Agent` in the current process. The chi
## Behavior
`start(request)` delegates to [`startInProcessRun`](../subagent-inprocess/README.md) with no seed and awaits publication before returning. The child receives parent working-directory/session lineage and inherits the parent model unless overridden, but starts with an empty conversation.
`start(request)` delegates to [`startInProcessRun`](../subagent-in-process-driver/README.md) with no seed and awaits publication before returning. The child receives parent working-directory/session lineage and inherits the parent model unless overridden, but starts with an empty conversation.
The shared driver owns depth checking, persona and tool-filter setup, structured output, required-signal cancellation, one-shot execution, result reading, and quiescent disposal. A startup rejection leaves no published child; provider unload after fulfillment does not revoke the holder-owned run.

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-spawn
# @deepseek-ai/dsh-subagent-spawn-in-process
[English](README.md) | 中文
@@ -6,7 +6,7 @@ spawn 提供方会在当前进程中创建一个全新的子 `Agent`。子 agent
## 行为
`start(request)` 不传入 seed,直接委托给 [`startInProcessRun`](../subagent-inprocess/README.md),并在子 agent 发布后才返回。子 agent 获得父 agent 的工作目录/会话谱系,并默认继承父 agent 模型(除非覆盖),但以空对话开始运行。
`start(request)` 不传入 seed,直接委托给 [`startInProcessRun`](../subagent-in-process-driver/README.md),并在子 agent 发布后才返回。子 agent 获得父 agent 的工作目录/会话谱系,并默认继承父 agent 模型(除非覆盖),但以空对话开始运行。
共享驱动器负责深度检查、persona 与工具过滤器设置、结构化输出、通过必需的信号执行取消、单次执行、结果读取和完全停稳后的 dispose(资源释放)。启动遭拒不会留下已发布的子 agent;启动调用兑现后卸载提供方,也不会撤销由持有方拥有的运行。

View File

@@ -1,14 +1,14 @@
{
"name": "@deepseek-ai/dsh-subagent-spawn",
"name": "@deepseek-ai/dsh-subagent-spawn-in-process",
"description": "In-process spawn subagent backend: runs a fresh child agent on ctx.agents",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/subagent/subagent-spawn"
"directory": "packages/subagent/subagent-spawn-in-process"
},
"type": "module",
"main": "lib/index.js",
@@ -30,11 +30,11 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
"@deepseek-ai/dsh-subagent-in-process-driver": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
@@ -52,7 +52,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-subagent-in-process-driver": "workspace:^",
"@deepseek-ai/dsh-tool-bash": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"

View File

@@ -3,7 +3,7 @@
* `ctx.subagents` that runs each child as a fresh child {@link Agent} on the same cordis
* context (its own session, own system prompt, zero parent context). The cheapest transport,
* reusing the agent factory's quiescent teardown.
* @module @deepseek-ai/dsh-subagent-spawn
* @module @deepseek-ai/dsh-subagent-spawn-in-process
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -14,9 +14,9 @@ import type {
SubagentCapabilities,
SubagentProvider,
} from '@deepseek-ai/dsh-subagent'
import { startInProcessRun } from '@deepseek-ai/dsh-subagent-inprocess'
import { startInProcessRun } from '@deepseek-ai/dsh-subagent-in-process-driver'
export const name = 'subagent-spawn'
export const name = 'subagent-spawn-in-process'
// `tools` is deliberately not injected: the child factory already provides it during setup,
// and adding it here would unnecessarily change this provider's apply timing.
export const inject = ['subagents']
@@ -38,7 +38,7 @@ export const Config: z<Config> = z.object({
* `restrict()` and a scoped shadowing persona section, applied in the child's
* creation window).
*/
class SpawnProvider implements SubagentProvider {
class SpawnInProcessProvider implements SubagentProvider {
readonly capabilities: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }
// Context contract: a spawned child starts fresh — it never sees the parent conversation.
readonly inheritsParentContext = false
@@ -60,5 +60,5 @@ class SpawnProvider implements SubagentProvider {
}
export function apply(ctx: Context, config: Config): void {
ctx.subagents.registerProvider(new SpawnProvider(config.providerName))
ctx.subagents.registerProvider(new SpawnInProcessProvider(config.providerName))
}

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-inprocess`.
* @module @deepseek-ai/dsh-subagent-inprocess/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-spawn-in-process`.
* @module @deepseek-ai/dsh-subagent-spawn-in-process/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-inprocess'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-spawn-in-process'
/** Cordis companion plugin name. */
export const name = 'subagent-insubprocess-invariant'
export const name = 'subagent-spawn-in-process-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -3,11 +3,11 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import * as BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as Spawn from '../src/index.ts'
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
@@ -29,11 +29,11 @@ export async function spawnHarness(workdir: string): Promise<Context> {
})
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LlmDeepSeek)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(BashEnvPlugin)
await ctx.plugin(LocalBashExecutor, { cwd: workdir, timeoutMs: 30_000 })
await ctx.plugin(ToolBash)
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(Spawn, { providerName: 'spawn' })
// The model-facing subagent tool, bound to the spawn backend.
await ctx.plugin(ToolSubagent, { provider: 'spawn' })

View File

@@ -6,20 +6,20 @@ import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as spawn from '../src/index.ts'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-inprocess'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-in-process-driver'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -27,7 +27,7 @@ async function mountInvariants(ctx: Context): Promise<void> {
/**
* Drives the REAL spawn backend end-to-end: a real agent loop + a scripted mock
* MODEL (the only mocked boundary) + the real SubagentService + the real
* MODEL (the only mocked boundary) + the real SubagentRuntime + the real
* invariant service plus package companions (so a malformed child session log would fail the test).
* The parent is a real config agent; the spawn provider creates a real child
* agent on the same context and we assert its output.
@@ -38,7 +38,7 @@ async function setup(script: Script) {
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(spawn, { providerName: 'spawn' })
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -64,7 +64,7 @@ function disposeChildLifecycle(parent: Agent): void {
void lifecycle()
}
describe('dsh-subagent-spawn', () => {
describe('dsh-subagent-spawn-in-process', () => {
it('runs a fresh child to completion and returns its final assistant output', async () => {
// One model call for the child: a plain text answer.
const { ctx, parent } = await setup([textResponse('child answer')])
@@ -290,7 +290,7 @@ describe('dsh-subagent-spawn', () => {
it('unregisters the provider when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(AgentRegistry)
const fiber = await ctx.plugin(spawn, { providerName: 'spawn' })
expect(ctx.subagents.list()).toEqual(['spawn'])
@@ -322,7 +322,7 @@ describe('dsh-subagent-spawn', () => {
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(spawn, { providerName: 'spawn' })
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -350,7 +350,7 @@ describe('dsh-subagent-spawn', () => {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(spawn, { providerName: 'spawn' })
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
const parentEffects = parent.ctx.fiber.getEffects().length
@@ -370,12 +370,12 @@ describe('dsh-subagent-spawn', () => {
it('has the namespace-plugin export shape (no stray default)', () => {
expect('default' in spawn).toBe(false)
expect(spawn.name).toBe('subagent-spawn')
expect(spawn.name).toBe('subagent-spawn-in-process')
expect(spawn.inject).toEqual(['subagents'])
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(spawn) as Record<string, unknown>
expect(unwrapped).toBe(spawn)
expect(unwrapped.name).toBe('subagent-spawn')
expect(unwrapped.name).toBe('subagent-spawn-in-process')
expect(unwrapped.inject).toEqual(['subagents'])
expect(typeof unwrapped.apply).toBe('function')
})

View File

@@ -21,10 +21,10 @@
"path": "../subagent"
},
{
"path": "../subagent-inprocess"
"path": "../subagent-in-process-driver"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md
README.md: edddb39a2c197eb7c8acb27f5b7f6b8c6016c517
README.zh.md: e28a8794cfb227dd475ea2b4c42605a90ec7fa1d
README.md: ed4a9123a2dfa5b2fa5abc67f4513547feb3d140
README.zh.md: e117ac7a6078d5b63759560eef35381b43b00dea

View File

@@ -8,7 +8,7 @@ The [subagent family overview](../README.md) maps implementations and model-faci
## Service API
`SubagentService` has these operations:
`SubagentRuntime` has these operations:
| Member | Meaning |
|---|---|
@@ -52,7 +52,7 @@ The Service Definition owns the versioned `subagent/descriptor` session event vo
## Delegation depth
The seam owns the depth vocabulary shared by Service providers and Consumers: the `AgentOptions.subagentDepth` declaration, `assertSubagentMaxDepth`, and `delegationDepthOf(agent)`. The persisted `SessionHeader.delegationDepth` is authoritative and monotone — runtime options may deepen the count but never lower it, so a resumed child cannot be re-counted as top-level.
The seam owns the depth vocabulary shared by Service Providers and Consumers: the `AgentOptions.subagentDepth` declaration, `assertSubagentMaxDepth`, and `delegationDepthOf(agent)`. The persisted `SessionHeader.delegationDepth` is authoritative and monotone — runtime options may deepen the count but never lower it, so a resumed child cannot be re-counted as top-level.
`inheritsParentContext` is descriptive rather than enforceable. It says only whether the child sees completed parent conversation history (`fork` does; `spawn` and the out-of-process one-shot providers do not), not whether it inherits tools, services, or authority.
@@ -94,7 +94,7 @@ Run events are scoped to the delegating parent. Every listener is independently
Provider additions and removals also emit `subagent/provider-added` and `subagent/provider-removed`. Consumers such as the model-facing tool use those events because Cordis may load sibling plugins concurrently; configuration order does not prove registration order.
Continuable children do not create `SubagentRun` or Tasks. The continuation manager directly owns one process-local Activation and retained `AgentHandle` per resident child Session, uses the Agent inbox as the only FIFO, and cold-resumes from the durable descriptor. Exact live direct-parent identity authorizes parent-to-child delivery. Exact live child identity authorizes reports; the manager derives the recipient from durable `parentSession`, and `MessageSource` records the sender without granting authority. Interrupt authority is deliberately wider than delivery authority: a human presents the durable direct-parent address so a live child stays stoppable while its parent Agent is offline, and any exact live ancestor recorded in the Activation's materialization lineage may stop its descendant, because stopping a turn is idempotent and delivers no content.
Continuable children do not create `SubagentRun` or Jobs. The continuation manager directly owns one process-local Activation and retained `AgentHandle` per resident child Session, uses the Agent inbox as the only FIFO, and cold-resumes from the durable descriptor. Exact live direct-parent identity authorizes parent-to-child delivery. Exact live child identity authorizes reports; the manager derives the recipient from durable `parentSession`, and `MessageSource` records the sender without granting authority. Interrupt authority is deliberately wider than delivery authority: a human presents the durable direct-parent address so a live child stays stoppable while its parent Agent is offline, and any exact live ancestor recorded in the Activation's materialization lineage may stop its descendant, because stopping a turn is idempotent and delivers no content.
When `ctx.sessionProjections` is available, the service registers two projection units. `subagentTiming` resets at each descriptor so a fork seed's ancestor work cannot enter the child's total, then accumulates `turn/start` → `turn/end` active time and retains same-cut `active.since` and `active.through` bounds for an open turn; while that turn remains open, `active.through` follows the latest folded event, giving an inactive consumer a conservative crash bound without mixing in newer session metadata. `subagent` folds the durable identity — mode plus creation label — from `subagent/descriptor` events with the same last-wins reset discipline, so a fork seed's ancestor descriptor stands only until the child's own overrides it; a malformed or unrecognized-version payload folds to the serializable `null` sentinel — indistinguishable from a log with no descriptor, and surviving every JSON push frame so a consumer replaces a stale identity instead of keeping it — and never throws.
@@ -131,7 +131,7 @@ Every in-process child's runtime-context snapshot carries the `subagent:delegati
##### The delegation-scope statement
```markdown
You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.
You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the job needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.
```
#### Token effect

View File

@@ -8,7 +8,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
## 服务 API
`SubagentService` 具有以下操作:
`SubagentRuntime` 具有以下操作:
| 成员 | 含义 |
|---|---|
@@ -16,14 +16,14 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
| `getProvider(name)` | 返回提供方;不存在时返回 `undefined`。 |
| `list()` | 按插入顺序返回提供方名称。 |
| `start(name, request)` | 校验普通调用方请求,解析其分离的 `one-shot` 描述符,然后等待提供方,直到真实的一次性子 agent 发布。兑现时返回由持有方拥有的 `SubagentRun`;拒绝表示提供方已清理所有未发布的启动资源,而发布后的轮次或基础设施故障会通过该 run 结算。可继续子 agent 绝不通过此操作进入。 |
| `startContinuable(spec)` | 建立一个持久化可继续子 agent,并投递其初始提示词。子 agent 的 inbox 接受该提示词时,兑现为 `{ childId, messageId }`,无需等待轮次开始或消息写入 Session 日志;此前任何失败都会以无 id 拒绝,并完全回滚该子 agent。要求 `ctx.agents`、会话持久化以及具备 `prepareContinuable` 能力的提供方。 |
| `followup(parent, childId, content, { source, signal })` | 将来自确切在线直接父级的一条后续消息作为子 agent 的下一个 FIFO 轮次投递,术语与 `Agent.followup()` 一致,并返回被接受的 `MessageId`。驻留中的子 agent 由其 inbox 直接接受(唤醒处于 waiting 的 Activation);不驻留的则从其持久化 Session 冷恢复。要求 `ctx.agents`;冷恢复还要求会话持久化。 |
| `interrupt(targetSessionId, authority)` | 以人类持久化 parent 地址(`{ kind: 'user', parentSessionId }`)或确切在线 ancestor Agent(`{ kind: 'ancestor', agent }`)为授权,中断一个在线可继续 child 的当前轮次。准入同步完成、生效异步进行:它发出 `Agent.cancel(cause, { keepInbox: true })` 后立即返回,不等待目标观察到信号。尚未领取的待处理 inbox 工作、Activation 与已发布的后代均保持不变;已被领取进入中断轮次的工作不会重新入队。目标不存在时接受为 no-op;错误的 parent 地址以及过期、指向自身或非祖先调用方以 `UNAUTHORIZED` 拒绝。 |
| `startContinuable(spec)` | 建立一个持久化可继续子 agent,并投递其初始提示词。子 agent 的 inbox 接受该提示词时,兑现为 `{ childId, messageId }`,无需等待轮次开始或消息写入会话日志;此前任何失败都会以无 id 拒绝,并完全回滚该子 agent。要求 `ctx.agents`、会话持久化以及具备 `prepareContinuable` 能力的提供方。 |
| `followup(parent, childId, content, { source, signal })` | 将来自确切在线直接父级的一条后续消息作为子 agent 的下一个 FIFO 轮次投递,术语与 `Agent.followup()` 一致,并返回被接受的 `MessageId`。驻留中的子 agent 由其 inbox 直接接受(唤醒处于 waiting 的 Activation);不驻留的则从其持久化会话冷恢复。要求 `ctx.agents`;冷恢复还要求会话持久化。 |
| `interrupt(targetSessionId, authority)` | 以人类持久化 parent 地址(`{ kind: 'user', parentSessionId }`)或确切在线 ancestor Agent(`{ kind: 'ancestor', agent }`)为授权,中断一个在线可继续 child 的当前轮次。准入同步完成、生效异步进行:它发出 `Agent.cancel(cause, { keepInbox: true })` 后立即返回,不等待目标观察到信号。尚未领取的待处理 inbox 工作、Activation 与已发布的后代均保持不变;已被领取进入中断轮次的工作不会重新入队。目标不存在时接受为 no-op;错误的 parent 地址以及陈旧、指向自身或非祖先调用方以 `UNAUTHORIZED` 拒绝。 |
| `reportFrom(child, content, { delivery, signal })` | 从确切在线可继续 child 向其确切在线直接 parent 投递一条选中消息,并返回已接受的稳定 `MessageId`。静默投递会注入上下文;唤醒投递会提交一个后续 parent 轮次。 |
| `registerContinuableSetup(contribution)` | 把一项可选部署能力组合到每个可继续 child 尚未发布的作用域中,并支持从驻留 child 立即撤销。 |
| `drainContinuableDescendants(parents)` | 在由 host 确切拥有的在线 parent Agent 之下关闭准入,只停止其可见的可继续后代,等待在这些根之下已获准的物化过程完成发布或回滚,再按 child-first 顺序释放所选森林。该截止状态会持续到每个确切 parent 离开注册表;无关的 parent 森林和管理器全局准入保持在线。 |
| `listChildren(parentSessionId, signal?)` | 按 `createdAt` 再按 id 的顺序列出由会话支撑的直接 subagent,包括其 `one-shot`/`continuable` 模式、`running`/`inactive` 活动状态、基于 origin 分类的一层 `hasChildren` 提示与逐 child diagnostic,且不会加载或恢复它们。直接读取在线会话存储与可选的会话持久化(持久化缺席时仅枚举在线 child),并要求已挂载 `sessionProjections` 注册表;不要求 `ctx.agents`、继续执行管理器或任何查询服务。 |
| `listDescendants(rootSessionId, signal?)` | 从同一份实时优先语料按稳定 pre-order 展平根的完整会话树,并为每个 subagent 条目附加持久 `parentId` 与相对根的 `depth`。普通会话与一次性 child 仍作为遍历节点,因此其下的可继续后代仍可发现。身份、diagnostic、依赖与取消约定均沿用 `listChildren()`。 |
| `listDescendants(rootSessionId, signal?)` | 从同一份在线优先语料按稳定 pre-order 展平根的完整会话树,并为每个 subagent 条目附加持久 `parentId` 与相对根的 `depth`。普通会话与一次性 child 仍作为遍历节点,因此其下的可继续后代仍可发现。身份、diagnostic、依赖与取消约定均沿用 `listChildren()`。 |
`SubagentStartRequest.label` 是由会话支撑的一次性 child 所使用的可选简短持久化显示标签。面向模型的委派会提供其已有的 `description`;底层调用方无需凭空构造展示元数据。可继续启动始终携带自身的必填标签。`signal` 是必填项,也是一次性 `start` 的规范取消通道。发布前中止会使 `start()` 在回滚后拒绝;发布后中止会取消已返回 run 的剩余轮次工作,但不会隐藏其 id。请求还可以选择模型、要求结构化输出、限制委派深度、约束子 agent 工具或设置子 agent persona。对于可继续启动或后续操作,调用方信号只在 inbox 接受之前掌管查找、物化和准入;此后由管理器独立拥有 Activation,因此调用方后续取消既不会取消已接受的轮次,也不会 dispose(资源释放)子 agent。
@@ -52,7 +52,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
## 委派深度
该 seam 拥有 Service provider 和 Consumer 共享的深度词汇:`AgentOptions.subagentDepth` 声明、`assertSubagentMaxDepth` 和 `delegationDepthOf(agent)`。持久化的 `SessionHeader.delegationDepth` 具有权威性且单调:运行时选项可以加深计数,但绝不能降低它,因此恢复后的子 agent 不会被重新计为顶层。
该 seam 拥有 Service Provider 和 Consumer 共享的深度词汇:`AgentOptions.subagentDepth` 声明、`assertSubagentMaxDepth` 和 `delegationDepthOf(agent)`。持久化的 `SessionHeader.delegationDepth` 具有权威性且单调:运行时选项可以加深计数,但绝不能降低它,因此恢复后的子 agent 不会被重新计为顶层。
`inheritsParentContext` 只用于描述,不能强制执行。它仅说明子 agent 是否能看到父级已完成的对话历史(`fork` 可以;`spawn` 和各进程外一次性提供方不可以),不表示是否继承工具、服务或权限。
@@ -70,11 +70,11 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
## 可继续子 agent 与 Activation
可继续子 agent 拥有一个持久化 Session 和至多一个进程内 **Activation**——即被重建的子 agent 的一个驻留时段,而不是请求、结果、取消或 Task 边界。Agent inbox 是唯一的轮次队列,因此继续执行管理器负责驻留,而 agent loop(智能体循环)负责所有轮次排序与执行。任何可继续路径都不会创建 Task 或中间的承载结果的包装层。
可继续子 agent 拥有一个持久化会话和至多一个进程内 **Activation**——即被重建的子 agent 的一个驻留时段,而不是请求、结果、取消或 Task 边界。Agent inbox 是唯一的轮次队列,因此继续执行管理器负责驻留,而 agent loop(智能体循环)负责所有轮次排序与执行。任何可继续路径都不会创建 Task 或中间的承载结果的包装层。
管理器根据 Agent 停稳状态和所拥有的子 agent 集合推导三个内部驻留条件,而非维护第二个状态机:running(存在活跃准入、进行中的轮次或唤醒型 inbox 工作)、waiting(已停稳但仍拥有至少一个未 dispose 的子 agent)、settled(已停稳且所有拥有的子 agent 都已 dispose,因此管理器 dispose `AgentHandle` 并移除 Activation)。每条后续消息都使用 `Agent.followup()` 并成为一个 FIFO 轮次,且不会对当前轮次进行 steering(中途引导)。路由只取决于驻留状态:running 入队、waiting 唤醒同一 Agent,无 Activation 时则冷恢复一个新的。
管理器根据 Agent 完全停稳状态和所拥有的子 agent 集合推导三个内部驻留条件,而非维护第二个状态机:running(存在活跃准入、进行中的轮次或唤醒型 inbox 工作)、waiting(已停稳但仍拥有至少一个未 dispose 的子 agent)、settled(已停稳且所有拥有的子 agent 都已 dispose,因此管理器 dispose `AgentHandle` 并移除 Activation)。每条后续消息都使用 `Agent.followup()` 并成为一个 FIFO 轮次,且不会对当前轮次进行 steering(中途引导)。路由只取决于驻留状态:running 入队、waiting 唤醒同一 Agent,无 Activation 时则冷恢复一个新的。
管理器预留子 agent 身份、解析持久化描述符,通过私有的 activation-owner 作用域调用 `ctx.agents.create()`(冷恢复时为 `ctx.agents.resume()`),把返回的 `AgentHandle` 安装到 Activation 中,建立任何可继续父级所有权,然后提交提示词。冷恢复绝不通过提供方分发,因为持久化 Session 已持有初始前缀,折叠后的描述符即是全部重建输入。
管理器预留子 agent 身份、解析持久化描述符,通过私有的 activation-owner 作用域调用 `ctx.agents.create()`(冷恢复时为 `ctx.agents.resume()`),把返回的 `AgentHandle` 安装到 Activation 中,建立任何可继续父级所有权,然后提交提示词。冷恢复绝不通过提供方分发,因为持久化会话已持有初始前缀,折叠后的描述符即是全部重建输入。
### 结算投递
@@ -84,7 +84,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
空闲父级会以一个普通的后续轮次收到该通知。繁忙父级则被 steer 到其最近的 step 边界,因此同时结算的多个子级只消耗一个 step,而不是各自一个轮次;采用 steer 而非 inject 还意味着:即便驱动在状态读取与发送之间退出,该消息仍会被认领。若父级自身所在的谱系已在 draining,则该通知改为 inject 送达,完全不唤醒:对静息父级调用 `Agent.followup()` 会开启一个轮次,而 `cancel()` 不会对之后的轮次设防,因此在拆卸期间唤醒,会在宿主即将 dispose 的 Agent 上白花一次模型请求——而且每层树各一次,因为每层自己的通知又会唤醒它上面那层。被 inject 的消息会送达仍在读取自身 inbox 的父级,而无论如何日志都会记录这份记账;但它不会比该父级自身的 dispose 活得更久:`AgentHandle.dispose()` 是一次 `keepInbox: false` 的 cancel,会持久地取消尚未被认领的通知。因此 resume 后的父级没有待处理通知可读:`list_agents` 只告诉它有哪些子级、各自是在线还是仅存于存储;结局本身留在子级自己的 Session 里,一次 `send_message` 会通过 resume 该子级把它取回。已离开注册表的父级不算错误:通知被丢弃,子级自身的 Session 仍是持久记录。投递绝不会阻塞或使拆卸失败——发送被拒只会记录日志,因为为了重试一条通知而保留子级,会把它的整条祖先链永久钉在 `waiting` 上。
受继续执行管理的父级 Activation 会在子 agent 能够运行之前,把每个子 agent 的 Session id 记录到 `ownedChildren` 集合中,并且只有在每个所拥有的子 agent Activation 完成 `AgentHandle` dispose 之后才会 dispose(子先于父)。拆卸会先自顶向下传播 Agent 取消,再等待缓慢的后代,而 handle 释放仍保持 child-first。顶层及其他非继续执行的 Agent 没有 Activation,处于该等待图之外。最终结算会在 dispose handle 前等待 best-effort 的 `ctx.sessions.flush(child.session)`。listener rejection 会被记录,但不会使 Activation 失败,因为 listener 是否参与无法标识持久化后端;因此,恢复时持久化状态可能缺失或陈旧。
受继续执行管理的父级 Activation 会在子 agent 能够运行之前,把每个子 agent 的会话 id 记录到 `ownedChildren` 集合中,并且只有在每个所拥有的子 agent Activation 完成 `AgentHandle` dispose 之后才会 dispose(子先于父)。拆卸会先自顶向下传播 Agent 取消,再等待缓慢的后代,而 handle 释放仍保持 child-first。顶层及其他非继续执行的 Agent 没有 Activation,处于该等待图之外。最终结算会在 dispose handle 前等待 best-effort 的 `ctx.sessions.flush(child.session)`。listener rejection 会被记录,但不会使 Activation 失败,因为 listener 是否参与无法标识持久化后端;因此,恢复时持久化状态可能缺失或陈旧。
## 生命周期事件
@@ -94,15 +94,15 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
提供方新增和移除还会发出 `subagent/provider-added` 与 `subagent/provider-removed`。面向模型的工具等消费方使用这些事件,因为 Cordis 可能并发加载同级插件;配置顺序不能证明注册顺序。
可继续子级不会创建 `SubagentRun` 或 Task。继续执行管理器为每个驻留子 Session 直接拥有一个仅存在于当前进程的 Activation 和一个留存的 `AgentHandle`,使用 Agent inbox 作为唯一 FIFO,并从持久化描述符冷恢复。父到子投递由确切在线的直接父级身份授权。上报则由确切在线的子级身份授权;管理器根据持久化的 `parentSession` 推导接收方,`MessageSource` 记录发送方,但不授予权限。中断权限被刻意设计得比投递权限更宽:人类出示持久化直接 parent 地址,因此即使 parent Agent 离线,在线 child 仍可被停止;Activation 物化时记录的任何确切在线 ancestor 也可以停止其后代,因为停止一个轮次是幂等的,且不投递任何内容。
可继续子级不会创建 `SubagentRun` 或 Task。继续执行管理器为每个驻留子会话直接拥有一个仅存在于当前进程的 Activation 和一个留存的 `AgentHandle`,使用 Agent inbox 作为唯一 FIFO,并从持久化描述符冷恢复。父到子投递由确切在线的直接父级身份授权。上报则由确切在线的子级身份授权;管理器根据持久化的 `parentSession` 推导接收方,`MessageSource` 记录发送方,但不授予权限。中断权限被刻意设计得比投递权限更宽:人类出示持久化直接 parent 地址,因此即使 parent Agent 离线,在线 child 仍可被停止;Activation 物化时记录的任何确切在线 ancestor 也可以停止其后代,因为停止一个轮次是幂等的,且不投递任何内容。
当 `ctx.sessionProjections` 可用时,服务会注册两个投影单元。`subagentTiming` 会在每个描述符处重置,使 fork 种子中的祖先工作不会计入 child 总量,随后累加 `turn/start` → `turn/end` 活跃时间,并为未结束的轮次保留同一切面的 `active.since` 和 `active.through` 边界;在该轮次保持未结束期间,`active.through` 会跟随最近折叠的事件,从而为 inactive 消费方提供保守的崩溃上界,又不会混入更新的会话元数据。`subagent` 以同样的 last-wins 重置纪律从 `subagent/descriptor` 事件折叠持久化身份——模式与创建标签——因此 fork 种子中的祖先描述符只在 child 自身的描述符覆盖之前有效;畸形或版本不识别的载荷折叠为可序列化的 `null` 哨兵——与没有描述符的日志不可区分,且能完好通过每个 JSON 推送帧,让消费方以之替换掉手中过时的身份而非永久滞留——绝不抛错。
当 `ctx.sessionProjections` 可用时,服务会注册两个投影单元。`subagentTiming` 会在每个描述符处重置,使 fork 种子中的祖先工作不会计入 child 总量,随后累加 `turn/start` → `turn/end` 活跃时间,并为未结束的轮次保留同一切面的 `active.since` 和 `active.through` 边界;在该轮次保持未结束期间,`active.through` 会跟随最近折叠的事件,从而为 inactive 消费方提供保守的崩溃上界,又不会混入更新的会话元数据。`subagent` 以同样的 last-wins 重置纪律从 `subagent/descriptor` 事件折叠持久化身份——模式与创建标签——因此 fork 种子中的祖先描述符只在 child 自身的描述符覆盖之前有效;畸形或版本无法识别的载荷折叠为可序列化的 `null` 哨兵——与没有描述符的日志不可区分,且能完好通过每个 JSON 推送帧,让消费方以之替换掉手中的陈旧身份而非继续保留该身份——绝不抛错。
`registerContinuableSetup()` 允许可选包添加子级作用域能力,而无需让继续执行管理器知道这些能力的名称。贡献会在 Activation 发布前同步安装,在设置失败时一并回滚,并随子级作用域释放。新授权须等到下一个 Activation,移除贡献则会立即撤销每个驻留安装项。
## 收集模型
面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、也没有结果 promise——调用方通过 `send_message` 后续操作工具发送后续工作,`interrupt()` 只停止当前轮次而不 dispose 子 agent,而持久化子 agent Session 仍是子 agent 详细输出的来源。只有 `ctx.agents` 可用时,继续执行管理器才会存在,而会话持久化按每项继续执行操作解析。与此独立,`listChildren()` 枚举在线会话存储与可选会话持久化的在线优先合并——持久化缺席时仅枚举在线 child,因为那时冷 child 本就无法恢复——并由已注册的 `subagent` 投影单元供给每个 child 的持久化模式与标签:在线 child 取注册表的水位快照;冷 child 先取可选投影缓存的持久化行,且仅当其 `seq` 门证明该值折叠自 child 自身后缀(fork 种子之后——自有描述符一经追加即不可变)才直接采用,否则经一次有界并发的持久化 inspect 再经注册表折叠,且 inspect 结果必须仍指向枚举时的生命周期(同 id 被重新发布的会话降级为 `corrupt` diagnostic)。缓存读取抛错不产生判决——缓存是派生数据——静默落到该权威重折。投影折叠是唯一的分类权威;列表自身不解析任何描述符。取得身份值即产出 child 行;已定局而折叠未产出身份的候选是 `corrupt` diagnostic,inspect 失败是瞬时的 `unavailable`(下次列表重试),运行中而暂无身份值的候选整行省略(描述符尚未追加的创建窗口)。它不查询继续执行管理器、Agent 注册信息、Activation 或提供方。每个 child 行都会根据合并结果中携带持久化 `origin: 'subagent'` 的 header 派生读取时的 `hasChildren` 提示;它不会读取后代事件日志,展开后仍以描述符支撑的 child 目录为权威依据。UI 等服务消费方可以保留两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 工具只投影 `continuable` 条目,通过在线 Agent 注册表细化状态,并把仅存于存储的状态映射为可恢复而非终态的 `ready`(`running`/`idle`/`ready`),并在 `descendants` scope 下遍历 `listDescendants()`。列表操作会把调用方的取消信号转发到每次持久化读取,在这些 await 前后检查取消,并将每次检测到的中止报告为 `SubagentError` 错误码 `CANCELLED`;投影注册表未挂载则以 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 响亮失败,会话存储缺失则以 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 响亮失败。完整约定见[后台 subagent 任务 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)、[持久化目录 Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)、[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)、[能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)和 `src/types.ts`。
面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、也没有结果 promise——调用方通过 `send_message` 后续操作工具发送后续工作,`interrupt()` 只停止当前轮次而不 dispose 子 agent,而持久化子 agent 会话仍是子 agent 详细输出的来源。只有 `ctx.agents` 可用时,继续执行管理器才会存在,而会话持久化按每项继续执行操作解析。与此独立,`listChildren()` 枚举在线会话存储与可选会话持久化的在线优先合并——持久化缺席时仅枚举在线 child,因为那时冷 child 本就无法恢复——并由已注册的 `subagent` 投影单元供给每个 child 的持久化模式与标签:在线 child 取注册表的水位快照;冷 child 先取可选投影缓存的持久化行,且仅当其 `seq` 门证明该值折叠自 child 自身后缀(fork 种子之后——自有描述符一经追加即不可变)才直接采用,否则经一次有界并发的持久化 inspect 再经注册表折叠,且 inspect 结果必须仍指向枚举时的生命周期(同 id 被重新发布的会话降级为 `corrupt` diagnostic)。缓存读取抛错不产生判决——缓存是派生数据——静默落到该权威重折。投影折叠是唯一的分类权威;列表自身不解析任何描述符。取得身份值即产出 child 行;已定局而折叠未产出身份的候选是 `corrupt` diagnostic,inspect 失败是瞬时的 `unavailable`(下次列表重试),运行中而暂无身份值的候选整行省略(描述符尚未追加的创建窗口)。它不查询继续执行管理器、Agent 注册信息、Activation 或提供方。每个 child 行都会根据合并结果中携带持久化 `origin: 'subagent'` 的 header 派生读取时的 `hasChildren` 提示;它不会读取后代事件日志,展开后仍以描述符支撑的 child 目录为权威依据。UI 等服务消费方可以保留两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 工具只投影 `continuable` 条目,通过在线 Agent 注册表细化状态,并把仅存于存储的状态映射为可恢复而非终态的 `ready`(`running`/`idle`/`ready`),并在 `descendants` scope 下遍历 `listDescendants()`。列表操作会把调用方的取消信号转发到每次持久化读取,在这些 await 前后检查取消,并将每次检测到的中止报告为 `SubagentError` 错误码 `CANCELLED`;投影注册表未挂载则以 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 响亮失败,会话存储缺失则以 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 响亮失败。完整约定见[后台 subagent 任务 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)、[持久化目录 Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)、[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)、[能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)和 `src/types.ts`。
可继续 Activation 会等待 best-effort 的最终会话 flush,但不会把 listener 参与视为持久性确认。一次性运行保留尽力执行的会话检查点,因此已完成的一次性 child 只有在其会话确实进入持久化存储时,才可在 dispose 后继续被发现;如果该检查点缺失,服务不会根据 Task 历史虚构目录条目。
@@ -131,7 +131,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
##### 委派范围声明
```markdown
You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.
You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the job needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.
```
#### Token 影响
@@ -144,11 +144,11 @@ You are a delegated subagent: your permission scope was fixed when you were star
## 已知限制与暂缓事项
- **ACP 子 agent 仍为一次性,且无法通过追踪枚举**:ACP 运行在 parent 会话语料中没有本地 child 会话。ACP 的 `prepareContinuable` 需要在提供方专用描述符数据中持久化远端会话 id,以及逐子 agent 的继续执行能力声明,因为 ACP 的 `loadSession` 支持按子 agent 协商,而不是通过方法是否存在来确定。远程提供方还需要一份独立的 Activation 所有权约定,具备等效的经认证控制和子先于父的停稳保证,才能支持可继续子 agent。
- **ACP 子 agent 仍为一次性,且无法通过追踪枚举**:ACP 运行在 parent 会话语料中没有本地 child 会话。ACP 的 `prepareContinuable` 需要在提供方专用描述符数据中持久化远端会话 id,以及逐子 agent 的继续执行能力声明,因为 ACP 的 `loadSession` 支持按子 agent 协商,而不是通过方法是否存在来确定。远程提供方还需要一份独立的 Activation 所有权约定,具备等效的经认证控制和子先于父的完全停稳保证,才能支持可继续子 agent。
- **无 host-user 继续执行**:`followup()` 要求确切在线直接父级。只有 `interrupt()` 接受持久化 parent 地址形式的用户授权,因为停止一个轮次是幂等的且不投递任何内容;未来 host 适配器需要具体的经认证交互,才能让该 seam 获得用户投递能力。
- **不对当前轮次进行 steering**:可继续消息和唤醒式 report 会排入后续轮次,均不会重定向正在进行的轮次。
- **取消收敛期间存在唤醒缺口**:中断信号发出后、活动 driver 进入 idle 前被接受的唤醒型 follow-up 会保持排队,直到另一条唤醒发送到达。Issue #1838 负责 agent-loop 的唤醒锁存;普通会话取消也受此影响。
- **驻留仅限进程内**:Activation inbox 与所有权图不会在两个 harness 进程之间协调;对单个持久化存储的并发访问仍然需要持久化邮箱和跨进程租约协议。
- **不重放已接受但未记录的消息**:只有写入子 agent Session 日志的消息才能连同提供该消息的来源一起重建。崩溃可能丢失从未写入日志、已被接受的初始提示词或后续消息;此后一条经授权的消息可以冷恢复该子 agent,但丢失的消息不会自动重放。
- **没有持久化的上报 mailbox**:上报需要实时直接父级,提供的是接受标识,不保证恰好一次投递,也不提供已读回执。
- **不回放已接受但未记录的消息**:只有写入子 agent 会话日志的消息才能连同提供该消息的来源一起重建。崩溃可能丢失从未写入日志、已被接受的初始提示词或后续消息;此后一条经授权的消息可以冷恢复该子 agent,但丢失的消息不会自动回放。
- **没有持久化的上报 mailbox**:上报需要在线直接父级,提供的是接受标识,不保证恰好一次投递,也不提供已读回执。
- **生命周期事件只供观察**:影响运行的 `subagent/end` 延续或决策接口仍需等待具体消费方。

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-subagent",
"description": "Abstract subagent seam (ctx.subagents): named-provider registry for delegating to child agents",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
@@ -35,7 +35,7 @@
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"dependencies": {
"zod": "^4.4.3"
},
@@ -52,7 +52,7 @@
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
@@ -76,7 +76,7 @@
"@deepseek-ai/dsh-session-projection-cache": {
"optional": true
},
"@deepseek-ai/dsh-tasks": {
"@deepseek-ai/dsh-jobs": {
"optional": true
},
"@deepseek-ai/dsh-user-approval": {
@@ -98,7 +98,7 @@
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
"@deepseek-ai/dsh-storage": "workspace:^",
"@deepseek-ai/dsh-storage-domain": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"

View File

@@ -161,7 +161,7 @@ type ActivationState = 'running' | 'waiting' | 'settled'
/**
* Hooks the manager needs from the owning service. Declared here, by the
* dependent, so the manager states exactly what it requires instead of
* depending back on the whole {@link SubagentService}. Package-private: no
* depending back on the whole {@link SubagentRuntime}. Package-private: no
* consumer outside this package supplies a host.
*/
interface ContinuationHost {

View File

@@ -7,10 +7,10 @@
* Unlike the bash seam (one executor per context, second load throws), MULTIPLE
* providers coexist here: each registers under a unique name and a caller picks
* one by name. The shape mirrors the LLM adapter registry
* (`LlmService.registerAdapter`), not the single-service bash executor.
* (`LlmRuntime.registerAdapter`), not the single-service bash executor.
*
* This package owns the Service Definition role of the capability seam. Service providers
* (`@deepseek-ai/dsh-subagent-spawn`, `-fork`, `-acp`) and the model-facing
* This package owns the Service Definition role of the capability seam. Service Providers
* (`@deepseek-ai/dsh-subagent-spawn-in-process`, `-fork`, `-acp`) and the model-facing
* consumer (`@deepseek-ai/dsh-tool-subagent`) are separate packages.
*
* Public operations express caller intent: `start` returns one published owned
@@ -128,7 +128,7 @@ export type { SubagentIdentityProjection, SubagentTimingProjection } from './pro
declare module '@deepseek-ai/cordis' {
interface Context {
subagents: SubagentService
subagents: SubagentRuntime
}
interface Events {
@@ -154,7 +154,7 @@ declare module '@deepseek-ai/cordis' {
* @dshScopeScan unsupported
* @mode emit
*/
'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void
'subagent/start'(this: Scoped<SubagentRuntime>, info: SubagentRunInfo): void
/**
* A published child settled. Scope-filtered dispatch uses the same delegating
* parent carrier as `subagent/start`, so the lifecycle pair reaches the
@@ -163,12 +163,12 @@ declare module '@deepseek-ai/cordis' {
* @dshScopeScan unsupported
* @mode emit
*/
'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void
'subagent/end'(this: Scoped<SubagentRuntime>, info: SubagentRunEndInfo): void
}
}
/** Named provider registry with one-shot runs, durable discovery, and continuable-child operations. */
export class SubagentService extends Service {
export class SubagentRuntime extends Service {
private providers = new Map<string, SubagentProvider>()
private continuations: SubagentContinuationManager | undefined
/** Deployment contributions composed into unpublished continuable children. */
@@ -369,7 +369,7 @@ export class SubagentService extends Service {
registerProvider(provider: SubagentProvider): () => void {
const name = provider.name
// oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
return this.ctx.effect(function* (this: SubagentService) {
return this.ctx.effect(function* (this: SubagentRuntime) {
if (this.providers.has(name)) {
throw new SubagentError(`a subagent provider named "${name}" is already registered`, 'DUPLICATE_PROVIDER')
}
@@ -496,4 +496,4 @@ export class SubagentService extends Service {
}
}
export default SubagentService
export default SubagentRuntime

View File

@@ -122,7 +122,7 @@ interface PositionedCandidate {
* projection-cache row when it serves an own-suffix identity (the seq gate),
* else one bounded-concurrency persistence inspection folded through the
* registry.
* @see SubagentService.listChildren for the public cancellation and failure contract.
* @see SubagentRuntime.listChildren for the public cancellation and failure contract.
* @param ctx - context carrying the session store, the projection registry,
* optional persistence, and the optional projection cache.
* @param parentSessionId - parent session whose direct children are listed.
@@ -151,7 +151,7 @@ export async function listChildren(
* continuable child below either is still discovered. Classification uses the
* same projection-backed runtime as {@link listChildren}; no Agent is loaded or
* resumed.
* @see SubagentService.listDescendants for the public cancellation and failure contract.
* @see SubagentRuntime.listDescendants for the public cancellation and failure contract.
* @param ctx - context carrying the session store, projection registry, and optional persistence/cache.
* @param rootSessionId - session whose complete descendant tree is listed.
* @param signal - caller-owned cancellation observed around every persistence read.

View File

@@ -1,13 +1,13 @@
/**
* Settlement of one ONE-SHOT subagent run into a background-Task outcome. Only
* the one-shot background path uses Tasks; continuable children have no Task,
* the one-shot background path uses Jobs; continuable children have no Task,
* no per-message result, and no Task cancellation.
*
* @module @deepseek-ai/dsh-subagent/run-settlement
*/
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { TaskOutcome } from '@deepseek-ai/dsh-tasks'
import type { JobOutcome } from '@deepseek-ai/dsh-jobs'
import type { SubagentResult, SubagentRun } from './types.ts'
/** Flatten a child's final output blocks to the task's final text. */
@@ -22,9 +22,9 @@ function finalText(blocks: ContentBlock[]): string {
* Map a child result to the task outcome: completed carries final text,
* aborted is killed, and every other reason is failed without partial output.
* @param result - child terminal result.
* @returns outcome for the `ctx.tasks` registration.
* @returns outcome for the `ctx.jobs` registration.
*/
function runOutcome(result: SubagentResult): TaskOutcome {
function runOutcome(result: SubagentResult): JobOutcome {
switch (result.stopReason) {
case 'completed':
return { status: 'completed', output: finalText(result.output) }
@@ -46,8 +46,8 @@ function runOutcome(result: SubagentResult): TaskOutcome {
* @param run - live run to settle and release.
* @returns outcome after child resources are released.
*/
export async function settleRun(run: SubagentRun): Promise<TaskOutcome> {
let outcome: TaskOutcome
export async function settleRun(run: SubagentRun): Promise<JobOutcome> {
let outcome: JobOutcome
try {
outcome = runOutcome(await run.result)
} catch (error: unknown) {

View File

@@ -149,7 +149,7 @@ export interface SubagentStartRequest {
}
/**
* Provider-facing one-shot request after {@link SubagentService.start} resolves
* Provider-facing one-shot request after {@link SubagentRuntime.start} resolves
* the durable child descriptor.
*/
export interface ResolvedSubagentStartRequest extends SubagentStartRequest {

View File

@@ -19,11 +19,11 @@ import SandboxPolicyService, { effectiveSandboxMode, setSandboxMode } from '@dee
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import ApprovalService, { effectiveApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import SubagentService from '../src/index.ts'
import SubagentRuntime from '../src/index.ts'
type Script = ConstructorParameters<typeof MockAdapter>[0]
@@ -45,7 +45,7 @@ async function setup(script: Script) {
await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: root })
await ctx.plugin(ApprovalService)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(SubagentFork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))

View File

@@ -9,14 +9,14 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import type { GenerateOptions, MessageId, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import SubagentService, {
import SubagentRuntime, {
SubagentError,
SUBAGENT_DESCRIPTOR_VERSION,
} from '../src/index.ts'
@@ -69,7 +69,7 @@ async function setupWith(adapter: LlmAdapter, options: { persistence?: boolean }
disposePersistence = () => persistenceFiber.dispose()
}
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(SubagentFork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -167,7 +167,7 @@ function observeCancel(agent: Agent, callback: () => void): void {
})
}
describe('SubagentService.startContinuable', () => {
describe('SubagentRuntime.startContinuable', () => {
it('returns both identities at inbox acceptance, without waiting for the turn or the log', async () => {
const { ctx, parent, adapter } = await setup([textResponse('first answer')])
const enqueued: { id: MessageId; loggedYet: boolean }[] = []
@@ -374,7 +374,7 @@ describe('SubagentService.startContinuable', () => {
await mountAgentLoopTestDependencies(fresh)
await fresh.plugin(JsonlSessionPersistence, { root: root! })
await fresh.plugin(AgentLoop, { agents: [] })
await fresh.plugin(SubagentService)
await fresh.plugin(SubagentRuntime)
await fresh.plugin(SubagentSpawn, { providerName: 'spawn' })
const freshParent = fresh.agentLoop.create(SessionId('routeless-resume'), {})
await followup(fresh, freshParent, started.childId, message('resume routeless'))
@@ -436,7 +436,7 @@ describe('SubagentService.startContinuable', () => {
})
})
describe('SubagentService.followup residency routing', () => {
describe('SubagentRuntime.followup residency routing', () => {
it('enqueues in the same Activation while it is running, preserving one inbox FIFO', async () => {
const releaseFirst = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([
@@ -480,7 +480,7 @@ describe('SubagentService.followup residency routing', () => {
it('cold-resumes after the initial provider unregisters', async () => {
const { ctx, parent } = await setup([textResponse('first'), textResponse('after resume')])
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SubagentInvariant)
const disposeProvider = ctx.subagents.registerProvider({
name: 'retired',
@@ -2287,7 +2287,7 @@ describe('continuable errors', () => {
roots.push(root)
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
const serviceFiber = await ctx.plugin(SubagentService)
const serviceFiber = await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -2302,7 +2302,7 @@ describe('continuable errors', () => {
})
})
describe('SubagentService.interrupt', () => {
describe('SubagentRuntime.interrupt', () => {
it('aborts the current turn durably, parks accepted follow-ups, and resumes them only on a waking send', async () => {
const releaseFirst = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([

View File

@@ -2,19 +2,19 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentService, { SubagentRunId } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { SubagentRunId } from '@deepseek-ai/dsh-subagent'
import type {
SubagentProvider,
SubagentRunEndInfo,
SubagentRunInfo,
} from '@deepseek-ai/dsh-subagent'
import * as SubagentInvariant from '@deepseek-ai/dsh-subagent/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(InvariantService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SubagentInvariant)
return ctx
}

View File

@@ -16,12 +16,12 @@ import SessionProjectionCache from '@deepseek-ai/dsh-session-projection-cache'
import Storage from '@deepseek-ai/dsh-storage'
import { DomainFacility } from '@deepseek-ai/dsh-storage-domain'
import { MemoryMediaPool, MemoryStorageBackend } from '../../../storage/storage-domain/tests/helpers/memory-backend.ts'
import SubagentService, {
import SubagentRuntime, {
SUBAGENT_DESCRIPTOR_VERSION,
SubagentError,
} from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
type Script = ConstructorParameters<typeof MockAdapter>[0]
@@ -51,7 +51,7 @@ async function setup(
ctx.provide('storageDomain', facility)
await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
}
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(SubagentFork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
@@ -145,13 +145,13 @@ const hostileProjectionDefinition: ProjectionDefinition<'subagentListHostileProb
stateVersion: 1,
}
describe('SubagentService.listChildren', () => {
describe('SubagentRuntime.listChildren', () => {
it('lists live children without persistence, query services, or the continuation runtime', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SubagentService)
expect(ctx.get('tasks')).toBeUndefined()
await ctx.plugin(SubagentRuntime)
expect(ctx.get('jobs')).toBeUndefined()
expect(ctx.get('agents')).toBeUndefined()
expect(ctx.get('sessionPersistence')).toBeUndefined()
@@ -184,7 +184,7 @@ describe('SubagentService.listChildren', () => {
it('fails loud when the session store is not mounted', async () => {
const ctx = new Context()
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await expect(ctx.subagents.listChildren(SessionId('no-store-parent'))).rejects.toThrow(
expect.objectContaining({ code: 'SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE' }) as Error,
)
@@ -973,7 +973,7 @@ describe('SubagentService.listChildren', () => {
})
})
describe('SubagentService.listDescendants', () => {
describe('SubagentRuntime.listDescendants', () => {
it('flattens the complete tree in stable pre-order with verified parent and depth', async () => {
const { ctx, parent } = await setup([])
const childA = await authorChild(ctx, '00000000-0000-4000-8000-00000000aaa1', {

View File

@@ -4,7 +4,7 @@ import { type Agent } from '@deepseek-ai/dsh-agent'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import { carrierKeyOf } from '@deepseek-ai/dsh-scope'
import SubagentService, {
import SubagentRuntime, {
foldSubagentDescriptor,
snapshotSubagentDescriptor,
SUBAGENT_DESCRIPTOR_VERSION,
@@ -62,13 +62,13 @@ class StubProvider implements SubagentProvider {
}
}
async function service(): Promise<{ ctx: Context; subagents: SubagentService }> {
async function service(): Promise<{ ctx: Context; subagents: SubagentRuntime }> {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
return { ctx, subagents: ctx.subagents }
}
describe('SubagentService', () => {
describe('SubagentRuntime', () => {
it('registers, lists, looks up, starts, and removes providers', async () => {
const { ctx, subagents } = await service()
const added: string[] = []
@@ -122,7 +122,7 @@ describe('SubagentService', () => {
},
})
expect(provider.lastRequest).not.toBe(request)
expectTypeOf<Parameters<SubagentService['start']>[1]>().toExtend<SubagentStartRequest>()
expectTypeOf<Parameters<SubagentRuntime['start']>[1]>().toExtend<SubagentStartRequest>()
expect('resume' in subagents).toBe(false)
expect('resume' in provider).toBe(false)
})

View File

@@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis'
import SessionStore from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentService from '../src/index.ts'
import SubagentRuntime from '../src/index.ts'
import { subagentTimingProjectionDefinition } from '../src/projection.ts'
function event(type: SessionEvent['type'], seq: number, time: number): SessionEvent {
@@ -21,7 +21,7 @@ describe('subagent timing projection', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
const serviceFiber = await ctx.plugin(SubagentService)
const serviceFiber = await ctx.plugin(SubagentRuntime)
const before = ctx.sessionProjections.snapshot(ctx.sessions.create()).values
expect(before.subagentTiming).toEqual({ settledMs: 0 })

View File

@@ -48,10 +48,10 @@
"path": "../../session/session-projection-cache"
},
{
"path": "../../tasks/tasks"
"path": "../../jobs/jobs"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/tool-subagent-control/README.md
README.md: 91f8d23ac092049e5315418070cdaae025054860
README.zh.md: 981e3f4efa2a904918d3f4174a2188fc2965c626
README.zh.md: 66d4b5a446fc9e412c5fec7bccf27bdefd1f5f77

View File

@@ -4,9 +4,9 @@
可选的全局具名 `send_message`、`interrupt_agent` 与 `list_agents` 工具是 `ctx.subagents` 之上的轻量适配器。绑定提供方的 `@deepseek-ai/dsh-tool-subagent` 实例会为每种传输注册不同的委派工具;这个单独加载的包只注册一次共享控制工具,因此多个委派工具绝不会重复注册全局控制工具。根插件注册 `send_message` 与 `interrupt_agent`,且只要求 `subagents`;可单独加载的 `./list-agents` 插件注册 `list_agents`,并将 `subagents` 与 `agents` 声明为加载时依赖。其目录读取在调用时还要求会话存储与投影注册表,但不要求任何查询服务。部署可保留根插件工具并省略列表工具。是否加载这些工具不会决定委派工具是否启动可继续工作。这些工具只负责父到子的方向;单独安装的 [`@deepseek-ai/dsh-tool-subagent-report`](../tool-subagent-report/README.md) 负责子到父的方向。
本工具不执行生命周期路由:驻留与冷恢复归 subagent 服务所有。它将 `exec.agent` 作为授权投递的确切在线父级传入,并把每条消息的来源记录为 `{ kind: 'coordinator', senderSessionId: parent.id }`;服务会保留该来源,但绝不将其视为权限。每条消息都会通过 `Agent.followup()` 成为子 agent(智能体)的下一个 FIFO 轮次:如果子 agent 仍在工作,该消息会等待其当前轮次结束,因此无法重定向已经在进行的工作。本工具会转发其执行信号,该信号只在 inbox 接受之前掌管准入;一旦子 agent 接受消息,已接受的轮次便无法再通过本工具取消。本次调用不会返回子 agent 的回复;通过该 id 查看其 transcript(文本记录),才是了解它完成了哪些工作的真源。拥有 `report` 的子 agent 会自行把内容作为一条单独的父级消息发回。投递失败会变为出错的工具结果,并明确说明消息未送达。
本工具不执行生命周期路由:驻留与冷恢复归 subagent 服务所有。它将 `exec.agent` 作为授权投递的确切在线父级传入,并把每条消息的来源记录为 `{ kind: 'coordinator', senderSessionId: parent.id }`;服务会保留该来源,但绝不将其视为权限。每条消息都会通过 `Agent.followup()` 成为 subagent 的下一个 FIFO 轮次:如果子 agent(智能体)仍在工作,该消息会等待其当前轮次结束,因此无法重定向已经在进行的工作。本工具会转发其执行信号,该信号只在 inbox 接受之前掌管准入;一旦子 agent 接受消息,已接受的轮次便无法再通过本工具取消。本次调用不会返回子 agent 的回复;通过该 id 查看其 transcript(文本记录),才是了解它完成了哪些工作的真源。拥有 `report` 的子 agent 会自行把内容作为一条单独的父级消息发回。投递失败会变为出错的工具结果,并明确说明消息未送达。
`interrupt_agent(agent_id)` 将 `exec.agent` 作为 `ctx.subagents.interrupt()` 的确切在线 ancestor 授权传入:目标可以是直接 child 或更深的后代,由服务——而不是本工具——依据目标 Activation 记录的 lineage 校验调用方。只有目标的当前轮次会停止(`keepInbox`):已排队的消息保持暂停直到之后的 `send_message`,已发布的后代继续运行,child 也仍可接受后续消息。调用在停止请求被接受后立即返回,不等待目标完全停稳;目标不存在或已结算是被接受的 no-op,而 self、sibling、过期与非 ancestor 调用方会成为出错结果。
`interrupt_agent(agent_id)` 将 `exec.agent` 作为 `ctx.subagents.interrupt()` 的确切在线 ancestor 授权传入:目标可以是直接 child 或更深的后代,由服务——而不是本工具——依据目标 Activation 记录的 lineage 校验调用方。只有目标的当前轮次会停止(`keepInbox`):已排队的消息保持暂停直到之后的 `send_message`,已发布的后代继续运行,child 也仍可接受后续消息。调用在停止请求被接受后立即返回,不等待目标完全停稳;目标不存在或已结算是被接受的 no-op,而 self、sibling、陈旧与非 ancestor 调用方会成为出错结果。
`list_agents` 接受一个可选的 `scope` 参数,会从调用它的 agent 推导根 id,并且不使用 cursor,将服务目录投影为可继续 child。默认的 `children` scope 读取 `ctx.subagents.listChildren()`;`descendants` 读取 `ctx.subagents.listDescendants()`,其单份语料的遍历会穿过普通会话与一次性 child,并按稳定 pre-order 以 `parent=<id> depth=<n>` 渲染保留下来的条目。`parent` 注释是持久化直接 parent 会话 id,可能指向输出中省略的普通会话。对于调用本工具的 agent,只有 depth-1 child 条目可作为 `send_message` 候选;更深的 child 条目只能作为 `interrupt_agent` 候选。状态来自在线 Agent 注册表:`running`(driver 活跃)、`idle`(驻留但处于轮次之间,可能在等待它启动的 agent)或 `ready`(仅存于存储,表示可恢复而非终态)。服务结果还包含由会话支撑的一次性 subagent,以供 UI 等消费方使用;但这些条目无法接受 `send_message`,因此会从这个模型工具中排除。diagnostic 仍然可见,并在 descendants scope 中带有位置。持久化身份和模式来自每个子 agent 的描述符,消息送达时的鉴权和 Activation 所有权检查仍归服务负责。
@@ -16,7 +16,7 @@
#### 模型看到的内容
已生成的 [schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control):`send_message` 包含 `subagent_id` 和 `message`,说明消息会成为子 agent 的下一个轮次、本次调用不会返回子 agent 的回答,以及失败即表示消息未送达;`interrupt_agent` 包含 `agent_id`,说明只有当前轮次会停止、已排队消息保持暂停、后代继续运行,以及接受先于实际停止;`list_agents` 包含可选的 `scope` 枚举。
已生成的 [schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control):`send_message` 包含 `subagent_id` 和 `message`,说明消息会成为 subagent 的下一个轮次、本次调用不会返回 subagent 的回答,以及失败即表示消息未送达;`interrupt_agent` 包含 `agent_id`,说明只有当前轮次会停止、已排队消息保持暂停、后代继续运行,以及接受先于实际停止;`list_agents` 包含可选的 `scope` 枚举。
#### Token 影响
@@ -30,7 +30,7 @@
#### 模型看到的内容
接受时返回 `interrupt requested for agent <agent_id>`。未授权的调用方——self、sibling、过期或非 ancestor——会成为指明拒绝原因的出错结果;目标不存在或已结算仍渲染接受行。
接受时返回 `interrupt requested for agent <agent_id>`。未授权的调用方——self、sibling、陈旧或非 ancestor——会成为指明拒绝原因的出错结果;目标不存在或已结算仍渲染接受行。
#### Token 影响
@@ -70,7 +70,7 @@
## 已知限制与暂缓事项
- **已排队的消息没有独立结果**:接受时只返回其 inbox `messageId`;子 agent 的工作会落入持久化子 agent 会话,绝不会通过本工具收集。获得 `report` 的子 agent 可以单独发回选定内容,但该消息不是本次调用的结果。
- **已排队的消息没有独立结果**:接受时只返回其 inbox `messageId`;subagent 的工作会落入持久化子 agent 会话,绝不会通过本工具收集。获得 `report` 的子 agent 可以单独发回选定内容,但该消息不是本次调用的结果。
- **不对当前轮次进行 steering(中途引导)**:每条消息都会开启后续 FIFO 轮次,因此在子 agent 工作时发送的消息只会在其当前轮次结束后运行,无法将其重定向。
- **列表是快照,而非投递承诺**:它可能与发布、dispose(资源释放)或后续消息发生竞态,另一个进程也可能激活当前进程报告为 `ready` 的 child;跨进程准确性需要共享租约。`interrupt_agent` 自己执行权威的在线 lineage 检查,因此过期的发现结果不会授予权限。
- **没有分页或删除**:系统返回完整且稳定排序的集合;只要 child 会话仍在持久化存储中,它就会继续出现在列表中,服务级上限或删除操作留待后续产品决策。

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-subagent-control",
"description": "Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
@@ -35,7 +35,7 @@
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
@@ -55,7 +55,7 @@
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}

View File

@@ -9,9 +9,9 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentListEntry } from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -59,7 +59,7 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(tool)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -247,7 +247,7 @@ describe('dsh-tool-subagent-control/list-agents', () => {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(tool)
expect(ctx.tools.schemas().some(schema => schema.name === 'list_agents')).toBe(true)
await fiber.dispose()

View File

@@ -9,8 +9,8 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -58,7 +58,7 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(tool)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -107,8 +107,8 @@ describe('dsh-tool-subagent-control', () => {
const props = (schemas[0]!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['message', 'subagent_id'])
// The continuable path has no Task, so the schema must not promise one.
expect(schemas[0]!.description).not.toContain('task_output')
expect(schemas[0]!.description).not.toContain('task id')
expect(schemas[0]!.description).not.toContain('job_output')
expect(schemas[0]!.description).not.toContain('job id')
// Follow-up ordering is model-visible: it cannot redirect the open turn.
expect(schemas[0]!.description).toContain('next turn')
})
@@ -207,7 +207,7 @@ describe('dsh-tool-subagent-control', () => {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(tool)
expect(ctx.tools.schemas().some(schema => schema.name === 'send_message')).toBe(true)
expect(ctx.tools.schemas().some(schema => schema.name === 'interrupt_agent')).toBe(true)

View File

@@ -27,7 +27,7 @@
"path": "../subagent"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/tool-subagent-report/README.md
README.md: b38b6b541d9c03174de1515df7a0babe5da055b4
README.zh.md: 0c5c063eded1e9057ee8886f03dd6494dd9eeed8
README.zh.md: d8557a43bca4b9a403aac31b24812425e0586fcf

View File

@@ -32,7 +32,7 @@
#### 模型看到的内容
接受时返回 `report accepted by the agent that started you as message <messageId>`;规范输出携带稳定的 `messageId`。发送方未授权、父级不可用或生命周期正在关闭时,失败会成为出错的结果。描述中会说明,失败的调用仍可能已经送达,因为 `reportFrom()` 接受消息后,后续 `tools/post-execute` 失败可能替换工具结果。
接受时返回 `report accepted by the agent that started you as message <messageId>`;规范输出携带稳定的 `messageId`。发送方未授权、父级不可用或生命周期正在关闭时,会返回错误结果。描述中会说明,失败的调用仍可能已经送达,因为 `reportFrom()` 接受消息后,后续 `tools/post-execute` 失败可能替换工具结果。
#### Token 影响
@@ -60,7 +60,7 @@
- **父级可能在宿主启动 dispose 后继续接受报告**:`AgentHandle.dispose()` 会先取消并等待完全停稳,然后才撤销作用域并离开注册表;它不公开「dispose 已开始」信号。在该窗口内接受的报告会追加到父级 transcript,但该父级不会在本进程中处理它。对于由继续执行管理器拥有的父级,管理器的准入边界会在整片森林拆卸期间拒绝该上报。
- **接受弱于持久投递**:没有持久化 mailbox、幂等键、投递回执、重试协议,也不保证恰好一次。任一侧记录接受后若进程失败,结果都不明确;外部重试可能产生重复上报。
- **暂存的静默报告无法立即重建**:接受时会返回其稳定 `MessageId`,但只有当待处理上下文到达普通日志边界后,父级 Session 才能重建带前缀的内容。
- **暂存的静默报告无法立即重建**:接受时会返回其稳定 `MessageId`,但只有当待处理上下文到达普通日志边界后,父级会话才能重建带前缀的内容。
- **授权须等到下一个 Activation,撤销则立即生效**:子级驻留后再安装本包,只会在该子级的下一个 Activation 中授予 `report` 及其指引;移除本包则会立即从驻留子级撤销两者。
- **嵌套上报只向上到达一条直接边**:孙级只向作为其直接父级的子级上报,不会直接到达顶层协调器;该直接父级必须随后显式发出一条衍生更新。
- **没有速率限制**:嵌套子级频繁上报时,默认的 `wakeup` 模式会放大模型工作量;宁可接受报告无人阅读也要避免这种放大的部署应选择 `quiet`。

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-subagent-report",
"description": "Child-scoped report tool over ctx.subagents continuations",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
@@ -52,7 +52,7 @@
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",

View File

@@ -12,8 +12,8 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as control from '@deepseek-ai/dsh-tool-subagent-control'
import { textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as tool from '../src/index.ts'
@@ -51,7 +51,7 @@ async function setup(options: { load?: boolean; config?: tool.Config } = {}) {
const root = mkdtempSync(join(tmpdir(), 'dsh-tool-subagent-report-'))
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
const fiber = options.load === false
? undefined
@@ -541,7 +541,7 @@ function userTexts(events: readonly SessionEvent[]): string[] {
}
describe('dsh-tool-subagent-report result independence', () => {
it('does not report a final assistant answer automatically or create Tasks', async () => {
it('does not report a final assistant answer automatically or create Jobs', async () => {
const { ctx, parent, adapter } = await setup()
const { started } = await startChild(ctx, parent)
adapter.release()
@@ -554,6 +554,6 @@ describe('dsh-tool-subagent-report result independence', () => {
// Nothing turns the child's final answer into a report it did not send.
expect(reports(parent)).toEqual([])
expect(userTexts((await ctx.sessionPersistence.load(started.childId)).events)).toEqual(['child task'])
expect(ctx.get('tasks')).toBeUndefined()
expect(ctx.get('jobs')).toBeUndefined()
})
})

View File

@@ -27,7 +27,7 @@
"path": "../subagent"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/tool-subagent/README.md
README.md: cc872f3decc4c85f26bd997293c8dc4140eb221c
README.zh.md: f7616dc677d10c141b1be606dae36d7dc7bb23ac
README.md: 9d7ed2e364f6a9dff26a1c9006535f898bdaabcc
README.zh.md: 8650ee35588c2615e4d6c016cb672030ee2e8194

View File

@@ -10,7 +10,7 @@ Each plugin instance binds one `provider` to one `toolName`; the model receives
A foreground call passes the execution signal through startup and execution, awaits `run.result`, and always awaits `run.dispose()` before returning. Only `completed` returns the canonical `{ kind: 'foreground', runId, output: JsonValue[] }`, rendered as the same final text; abort, refusal, token limit, and other failures become errored tool results whose message appends the child's preserved partial text (the `SubagentResult.output` selection) after the stop-reason headline, so a truncated answer is never reported as success yet never silently lost. If result collection and disposal both reject, the errored result preserves both diagnostics.
`backgroundMode` selects both the background route and the omitted `run_in_background` default. `one-shot` waits in the foreground by default; an explicit `true` registers a plain parent-owned Task and returns canonical `{ kind: 'background', taskId }`, rendered as `started background subagent task <id>`, even when the provider supports continuable children. Generic task tools own its later status, collection, cancellation, and notices. `continuable` runs in the background when the argument is omitted or `true`; an explicit `false` waits for the result in the foreground. Its background route requires a provider with the `prepareContinuable` capability, calls `ctx.subagents.startContinuable()`, and returns `{ kind: 'continuable', subagentId }`, rendered as `started subagent <childId>`. The route resolves at inbox acceptance: the child owns its own turns from there, so this call neither waits for nor collects a result. The child's transcript by that id remains the source of its detailed output, and the optional global `send_message` tool sends it more work. The continuation service delivers one settlement notice whenever the child's Activation ends, containing its outcome and any final assistant message independently of `report`. Starting continuable work does not require `send_message` to be loaded. See the [background subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), and the [background-first delegation Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md).
`backgroundMode` selects both the background route and the omitted `run_in_background` default. `one-shot` waits in the foreground by default; an explicit `true` registers a plain parent-owned Task and returns canonical `{ kind: 'background', jobId }`, rendered as `started background subagent job <id>`, even when the provider supports continuable children. Generic task tools own its later status, collection, cancellation, and notices. `continuable` runs in the background when the argument is omitted or `true`; an explicit `false` waits for the result in the foreground. Its background route requires a provider with the `prepareContinuable` capability, calls `ctx.subagents.startContinuable()`, and returns `{ kind: 'continuable', subagentId }`, rendered as `started subagent <childId>`. The route resolves at inbox acceptance: the child owns its own turns from there, so this call neither waits for nor collects a result. The child's transcript by that id remains the source of its detailed output, and the optional global `send_message` tool sends it more work. The continuation service delivers one settlement notice whenever the child's Activation ends, containing its outcome and any final assistant message independently of `report`. Starting continuable work does not require `send_message` to be loaded. See the [background subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), and the [background-first delegation Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md).
`toolFilter` changes the child's global tool layer but is not a parent-derived authority ceiling. See the [agent-scope security non-goal](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals).
@@ -29,7 +29,7 @@ A foreground call passes the execution signal through startup and execution, awa
## Concurrency
Foreground and background calls are concurrency-safe: sibling delegations in one assistant message overlap under the loop's rolling pool (`maxParallelToolCalls`), and results still commit in model order. Children work in their own sessions and a run never mutates the parent session; the one-shot background form's one parent-owned write — registering a Task — is a synchronous, commutative insertion that tolerates concurrent dispatch, so overlapping background calls acquire their task ids in dispatch-race order. Coordinating sibling workspace effects belongs to the model, exactly as it already does for background and continuable children. See the [parallel subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md) and the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md).
Foreground and background calls are concurrency-safe: sibling delegations in one assistant message overlap under the loop's rolling pool (`maxParallelToolCalls`), and results still commit in model order. Children work in their own sessions and a run never mutates the parent session; the one-shot background form's one parent-owned write — registering a Task — is a synchronous, commutative insertion that tolerates concurrent dispatch, so overlapping background calls acquire their job ids in dispatch-race order. Coordinating sibling workspace effects belongs to the model, exactly as it already does for background and continuable children. See the [parallel subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md) and the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md).
## Model Experience
@@ -37,7 +37,7 @@ Foreground and background calls are concurrency-safe: sibling delegations in one
#### What the model sees
The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) under this instance's configured name while its provider exists. Provider context inheritance changes the tool and prompt descriptions. Enabled background mode adds `run_in_background`: continuable mode documents its `true` default, runtime settlement notice, and explicit foreground override, while one-shot mode documents its `false` default and the task id collected with `task_output` or stopped with `task_kill`. While the tool is visible in an assembly's scope, a `tool:<toolName>` system-prompt section tells the model to start independent continuable delegations together, keep working while they run, and choose foreground only when its next action depends on the result; a tool restriction removes both its schema and this guidance.
The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) under this instance's configured name while its provider exists. Provider context inheritance changes the tool and prompt descriptions. Enabled background mode adds `run_in_background`: continuable mode documents its `true` default, runtime settlement notice, and explicit foreground override, while one-shot mode documents its `false` default and the job id collected with `job_output` or stopped with `job_kill`. While the tool is visible in an assembly's scope, a `tool:<toolName>` system-prompt section tells the model to start independent continuable delegations together, keep working while they run, and choose foreground only when its next action depends on the result; a tool restriction removes both its schema and this guidance.
#### Token effect
@@ -65,7 +65,7 @@ Append-only; newly visible content follows the reusable request prefix and does
#### What the model sees
Start returns exactly `started subagent <childId>` in configured continuable mode, or `started background subagent task <id>` in configured one-shot mode. In one-shot mode the generic task surface provides later status, final output, cancellation responses, and notices. In continuable mode this tool returns no result of its own; the child's settlement reaches the parent as a [service-owned notice](../subagent/README.md#settlement-notice), an independently loaded `send_message` tool delivers follow-ups, and the child's transcript by its id is the source of its detailed output.
Start returns exactly `started subagent <childId>` in configured continuable mode, or `started background subagent job <id>` in configured one-shot mode. In one-shot mode the generic task surface provides later status, final output, cancellation responses, and notices. In continuable mode this tool returns no result of its own; the child's settlement reaches the parent as a [service-owned notice](../subagent/README.md#settlement-notice), an independently loaded `send_message` tool delivers follow-ups, and the child's transcript by its id is the source of its detailed output.
#### Token effect

View File

@@ -8,9 +8,9 @@
每个插件实例把一个 `provider` 绑定到一个 `toolName`;模型不会收到提供方选择器。如需公开另一种传输,请加载另一个名称不同的实例。工具只在其提供方存在时注册,从而避免对同级加载顺序和提供方重新加载的依赖。工具描述遵循 `provider.inheritsParentContext`:新建子 agent(智能体)需要独立提示词,而 fork 子 agent 已能看到父级已完成轮次。
前台调用会让执行信号贯穿启动和执行,等待 `run.result`,并且在返回前总会等待 `run.dispose()`。只有 `completed` 会返回规范值 `{ kind: 'foreground', runId, output: JsonValue[] }`,并渲染为相同的最终文本;中止、拒绝、token 上限和其他失败都会变成出错的工具结果,其消息在终止原因标题之后附带子代理保留下来的部分文本(即 `SubagentResult.output` 的选取结果)——被截断的回答不会被报告为成功,也绝不会被悄悄丢弃。如果结果收集与 dispose(资源释放)都 reject,出错的结果会保留两项诊断信息。
前台调用会让执行信号贯穿启动和执行,等待 `run.result`,并且在返回前总会等待 `run.dispose()`。只有 `completed` 会返回规范值 `{ kind: 'foreground', runId, output: JsonValue[] }`,并渲染为相同的最终文本;中止、拒绝、token 上限和其他失败都会变成出错的工具结果,其消息在终止原因标题之后附带子 agent 保留下来的部分文本(即 `SubagentResult.output` 的选取结果)——被截断的回答不会被报告为成功,也绝不会被悄悄丢弃。如果结果收集与 dispose(资源释放)都 reject,出错的结果会保留两项诊断信息。
`backgroundMode` 同时选择后台路由与省略 `run_in_background` 时的默认行为。`one-shot` 默认在前台等待;显式传入 `true` 时,它会注册一个归父级所有的普通 Task,并返回规范值 `{ kind: 'background', taskId }`,渲染为 `started background subagent task <id>`,即使提供方支持可继续子 agent 也不例外。通用 Task 工具负责其后续状态、收集、取消和通知。`continuable` 在参数省略或为 `true` 时于后台运行;显式传入 `false` 时则在前台等待结果。其后台路由要求提供方具备 `prepareContinuable` 能力,调用 `ctx.subagents.startContinuable()`,并返回 `{ kind: 'continuable', subagentId }`,渲染为 `started subagent <childId>`。该路由在 inbox 接受时结算:子 agent 自此拥有自己的轮次,因此该调用既不等待也不收集结果。通过该 id 查看其 transcript(文本记录)仍是其详细输出的来源,可选的全局 `send_message` 工具则向其发送更多工作。每当子 agent 的 Activation 结束,继续执行服务都会投递一条结算通知,其中包含结束结果及可能存在的最终 assistant 消息,且这项投递不依赖 `report`。启动可继续工作不要求加载 `send_message`。见[后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续的 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)和[后台优先委派 Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md)。
`backgroundMode` 同时选择后台路由与省略 `run_in_background` 时的默认行为。`one-shot` 默认在前台等待;显式传入 `true` 时,它会注册一个归父级所有的普通 Task,并返回规范值 `{ kind: 'background', jobId }`,渲染为 `started background subagent job <id>`,即使提供方支持可继续子 agent 也不例外。通用 Task 工具负责其后续状态、收集、取消和通知。`continuable` 在参数省略或为 `true` 时于后台运行;显式传入 `false` 时则在前台等待结果。其后台路由要求提供方具备 `prepareContinuable` 能力,调用 `ctx.subagents.startContinuable()`,并返回 `{ kind: 'continuable', subagentId }`,渲染为 `started subagent <childId>`。该路由在 inbox 接受时结算:子 agent 自此拥有自己的轮次,因此该调用既不等待也不收集结果。通过该 id 查看其 transcript(文本记录)仍是其详细输出的来源,可选的全局 `send_message` 工具则向其发送更多工作。每当子 agent 的 Activation 结束,继续执行服务都会投递一条结算通知,其中包含结束结果及可能存在的最终 assistant 消息,且这项投递不依赖 `report`。启动可继续工作不要求加载 `send_message`。见[后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续的 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)和[后台优先委派 Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md)。
`toolFilter` 会改变子 agent 的全局工具层,但不是从父级派生的权限上限。见 [agent 作用域的安全非目标](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals)。
@@ -29,7 +29,7 @@
## 并发
前台调用和后台调用均并发安全:同一条 assistant 消息中的同级委派会在循环的滚动池(`maxParallelToolCalls`)下重叠执行,结果仍按模型顺序提交。子 agent 在各自的会话中工作,一次运行绝不变更父会话;一次性后台形态对父级拥有状态的唯一写入是注册一个 Task——这是一次同步、可交换、能容忍并发分发的插入,因此重叠的后台调用按分发竞态顺序获得各自的 task id。协调同级工作区效果由模型负责,正如模型已经对后台和可继续子 agent 所承担的那样。见 [并行 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md) 和 [并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。
前台调用和后台调用均并发安全:同一条 assistant 消息中的同级委派会在循环的滚动池(`maxParallelToolCalls`)下重叠执行,结果仍按模型顺序提交。子 agent 在各自的会话中工作,一次运行绝不变更父会话;一次性后台形态对父级拥有状态的唯一写入是注册一个 Task——这是一次同步、可交换、能容忍并发分发的插入,因此重叠的后台调用按分发竞态顺序获得各自的 job id。协调同级工作区效果由模型负责,正如模型已经对后台和可继续子 agent 所承担的那样。见 [并行 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md) 和 [并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。
## 模型体验
@@ -37,7 +37,7 @@
#### 模型看到的内容
当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent)。提供方是否继承上下文会改变工具描述和提示词描述。启用后台模式会添加 `run_in_background`:可继续模式会记录其默认值为 `true`、运行时结算通知与显式前台覆盖;一次性模式会记录其默认值为 `false`,以及用 `task_output` 收集或用 `task_kill` 停止的 task id。当工具在本次组装的作用域中可见时,一个 `tool:<toolName>` 系统提示词 section 会指示模型同时启动相互独立的可继续委派、在它们运行时继续工作,并且仅当下一步动作依赖结果时选择前台;工具限制会同时移除其 schema 和这段指引。
当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent)。提供方是否继承上下文会改变工具描述和提示词描述。启用后台模式会添加 `run_in_background`:可继续模式会记录其默认值为 `true`、运行时结算通知与显式前台覆盖;一次性模式会记录其默认值为 `false`,以及用 `job_output` 收集或用 `job_kill` 停止的 job id。当工具在本次组装的作用域中可见时,一个 `tool:<toolName>` 系统提示词 section 会指示模型同时启动相互独立的可继续委派、在它们运行时继续工作,并且仅当下一步动作依赖结果时选择前台;工具限制会同时移除其 schema 和这段指引。
#### Token 影响
@@ -65,7 +65,7 @@
#### 模型看到的内容
在配置的可继续模式下,启动时返回内容恰为 `started subagent <childId>`;在配置的一次性模式下,则返回 `started background subagent task <id>`。一次性模式下,通用 Task 接口提供后续状态、最终输出、取消响应和通知。可继续模式下,本工具不返回自己的结果;子 agent 的结算会以[服务负责的通知](../subagent/README.md#settlement-notice)到达父级,独立加载的 `send_message` 工具会投递后续消息,而通过其 id 查看子 agent 的 transcript 即是其详细输出来源。
在配置的可继续模式下,启动时返回内容恰为 `started subagent <childId>`;在配置的一次性模式下,则返回 `started background subagent job <id>`。一次性模式下,通用 Task 接口提供后续状态、最终输出、取消响应和通知。可继续模式下,本工具不返回自己的结果;子 agent 的结算会以[服务负责的通知](../subagent/README.md#settlement-notice)到达父级,独立加载的 `send_message` 工具会投递后续消息,而通过其 id 查看子 agent 的 transcript 即是其详细输出来源。
#### Token 影响

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tool-subagent",
"description": "Model-facing subagent delegation tool over the ctx.subagents seam",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},
@@ -30,14 +30,14 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
@@ -53,11 +53,11 @@
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-tasks-local": "workspace:^",
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-jobs-local": "workspace:^",
"@deepseek-ai/dsh-tool-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}

View File

@@ -16,7 +16,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import { assertSubagentMaxDepth, settleRun } from '@deepseek-ai/dsh-subagent'
import type { SubagentProvider, SubagentResult, SubagentRun } from '@deepseek-ai/dsh-subagent'
import type { TaskOutcome } from '@deepseek-ai/dsh-tasks'
import type { JobOutcome } from '@deepseek-ai/dsh-jobs'
import type {} from '@deepseek-ai/dsh-system-prompt'
export const name = 'tool-subagent'
@@ -109,12 +109,12 @@ function outputValueText(values: JsonValue[]): string {
}
/** Settle pending startup without rejecting the task producer contract. */
async function settleStart(start: Promise<SubagentRun>, signal: AbortSignal): Promise<TaskOutcome> {
async function settleStart(start: Promise<SubagentRun>, signal: AbortSignal): Promise<JobOutcome> {
try {
return await settleRun(await start)
} catch (error: unknown) {
// Product providers aggregate startup and rollback failures. Cancellation
// must not turn a failed cleanup into a cleanly killed Task.
// must not turn a failed cleanup into a cleanly killed Job.
return signal.aborted && !(error instanceof AggregateError)
? { status: 'killed' }
: { status: 'failed', detail: String(error) }
@@ -304,7 +304,7 @@ export function apply(ctx: Context, config: Config): void {
// continuable background path is reachable at all.
? continuable
? ' This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.'
: ' This call waits for the result by default. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.'
: ' This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`.'
: ' This call waits for the subagent and returns its result.'),
parameters: {
description: {
@@ -322,7 +322,7 @@ export function apply(ctx: Context, config: Config): void {
type: 'boolean' as const,
description: continuable
? 'Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it.'
: 'Whether to run as a background task and return its id. Defaults to false; collect with task_output or stop with task_kill.',
: 'Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill.',
},
} : {},
},
@@ -334,7 +334,7 @@ export function apply(ctx: Context, config: Config): void {
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, const: 'background' },
taskId: { type: 'string', required: true },
jobId: { type: 'string', required: true },
},
},
{
@@ -359,7 +359,7 @@ export function apply(ctx: Context, config: Config): void {
render: (_args, value) => [{
type: 'text',
text: value.kind === 'background'
? `started background subagent task ${value.taskId}`
? `started background subagent task ${value.jobId}`
: value.kind === 'continuable'
? `started subagent ${value.subagentId}`
: outputValueText(value.output),
@@ -399,13 +399,13 @@ export function apply(ctx: Context, config: Config): void {
})
return { kind: 'continuable' as const, subagentId: started.childId }
}
const tasks = ctx.get('tasks')
if (tasks === undefined) {
throw new Error('background tasks unavailable: load @deepseek-ai/dsh-tasks and @deepseek-ai/dsh-tool-tasks')
const jobs = ctx.get('jobs')
if (jobs === undefined) {
throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs')
}
// One-shot background child: task preflight finishes before the
// One-shot background child: job preflight finishes before the
// starter can spawn, and the task-owned signal covers startup.
const id = tasks.start({
const id = jobs.start({
kind: 'subagent',
label: args.description,
owner: parent,
@@ -421,7 +421,7 @@ export function apply(ctx: Context, config: Config): void {
}
},
})
return { kind: 'background' as const, taskId: id }
return { kind: 'background' as const, jobId: id }
}
const run: SubagentRun = await ctx.subagents.start(config.provider, {

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { type Agent } from '@deepseek-ai/dsh-agent'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { SessionId } from '@deepseek-ai/dsh-session'
import * as scripted from './scripted-provider.ts'
@@ -21,7 +21,7 @@ function baseRequest(over: Partial<SubagentStartRequest> = {}): SubagentStartReq
async function mount(config: Partial<scripted.Config> = {}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await scripted.mountScriptedProvider(ctx, { name: 'mock', ...config })
return ctx
}
@@ -89,7 +89,7 @@ describe('scripted subagent provider fixture', () => {
it('unregisters with its owning fixture fiber', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await scripted.mountScriptedProvider(ctx, { name: 'mock' })
expect(ctx.subagents.list()).toEqual(['mock'])
await fiber.dispose()

View File

@@ -6,17 +6,17 @@ import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as mock from './scripted-provider.ts'
import * as tool from '../src/index.ts'
@@ -26,7 +26,7 @@ const testToolSignal = new AbortController().signal
/**
* Drives the REAL plugin body: mounts `dsh-tool-subagent` on a real
* `ToolRegistry` + `SubagentService`, with a package-local scripted child
* `ToolRuntime` + `SubagentRuntime`, with a package-local scripted child
* boundary, and invokes the registered `subagent` tool through
* `ctx.tools.execute`. Everything downstream of the child boundary is the
* shipping code path.
@@ -40,8 +40,8 @@ function fakeAgent(id = 'parent-1'): Agent {
async function setup(toolConfig: tool.Config, mockConfig: Partial<mock.Config> = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
await mock.mountScriptedProvider(ctx, { name: 'mock', ...mockConfig })
await ctx.plugin(tool, toolConfig)
return ctx
@@ -106,7 +106,7 @@ describe('dsh-tool-subagent', () => {
expect(schema).toBeDefined()
const props = (schema!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['description', 'prompt', 'run_in_background'])
expect(schema!.description).toContain('task_output')
expect(schema!.description).toContain('job_output')
})
it('omits run_in_background entirely when the instance disables it (schema and capability never disagree)', async () => {
@@ -114,7 +114,7 @@ describe('dsh-tool-subagent', () => {
const schema = ctx.tools.schemas().find(s => s.name === 'subagent')
const props = (schema!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['description', 'prompt'])
expect(schema!.description).not.toContain('task_output')
expect(schema!.description).not.toContain('job_output')
})
it('refuses a forced run_in_background at execution time when the instance disables it', async () => {
@@ -191,8 +191,8 @@ describe('dsh-tool-subagent', () => {
// names, so a configurable name is what makes this work.
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
await mock.mountScriptedProvider(ctx, { name: 'spawn', reply: 'from spawn' })
await mock.mountScriptedProvider(ctx, { name: 'acp', reply: 'from acp' })
await ctx.plugin(tool, { provider: 'spawn', toolName: 'subagent' })
@@ -212,8 +212,8 @@ describe('dsh-tool-subagent', () => {
// arm must treat an unrecognized terminal reason as a failure, not success.
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'weird',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -238,8 +238,8 @@ describe('dsh-tool-subagent', () => {
let seen: { agentOptions?: { model?: string } } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -268,8 +268,8 @@ describe('dsh-tool-subagent', () => {
let seen: { agentOptions?: unknown } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'bare',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -303,8 +303,8 @@ describe('dsh-tool-subagent', () => {
it('registers when the provider appears LATER — no load-order requirement (Loader starts siblings concurrently)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
// Tool first: no provider yet — the tool must be absent, not broken.
// Direct apply (schema bypass): also covers the waiting-note's default
// toolName fallback, which validated config pre-fills.
@@ -320,8 +320,8 @@ describe('dsh-tool-subagent', () => {
it('keeps continuable guidance empty while its provider is absent', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
tool.apply(ctx, {
provider: 'later-continuable',
backgroundMode: 'continuable',
@@ -336,8 +336,8 @@ describe('dsh-tool-subagent', () => {
it('mirrors the provider lifecycle: gone on backend dispose, re-derived wording on re-registration', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
const backend = await mock.mountScriptedProvider(ctx, { name: 'mock' }) // fresh conversation (descriptor: false)
await ctx.plugin(tool, { provider: 'mock' })
expect(ctx.tools.schemas().find(s => s.name === 'subagent')!.description).toContain('does not see this conversation')
@@ -355,8 +355,8 @@ describe('dsh-tool-subagent', () => {
it('the tool PLUGIN fiber owns its lifecycle listeners: disposal unmounts, and a disposed fiber never zombie-mounts', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
// Arm 1: a mounted tool and its prompt section die with the plugin fiber;
// the provider survives.
@@ -391,8 +391,8 @@ describe('dsh-tool-subagent', () => {
it('ignores lifecycle events for OTHER providers', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
await mock.mountScriptedProvider(ctx, { name: 'mock' })
await ctx.plugin(tool, { provider: 'mock' })
// An unrelated provider registering (added-event with another name) and
@@ -427,8 +427,8 @@ describe('dsh-tool-subagent', () => {
const disposed = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -450,8 +450,8 @@ describe('dsh-tool-subagent', () => {
const disposed = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -474,8 +474,8 @@ describe('dsh-tool-subagent', () => {
const disposed = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -502,8 +502,8 @@ describe('dsh-tool-subagent', () => {
it('reports a foreground disposal failure after a completed result', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -529,8 +529,8 @@ describe('dsh-tool-subagent', () => {
const cancelled = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -568,8 +568,8 @@ describe('dsh-tool-subagent', () => {
const sawAborted = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -595,8 +595,8 @@ describe('dsh-tool-subagent', () => {
it('tools depend on the service: no `subagent` tool without ctx.subagents', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
// No SubagentService mounted. The tool injects its three required services so its
await ctx.plugin(ToolRuntime)
// No SubagentRuntime mounted. The tool injects its three required services so its
// apply never runs; the tool is absent rather than half-registered.
let booted = true
try {
@@ -632,8 +632,8 @@ describe('dsh-tool-subagent', () => {
let seen: { persona?: string; toolFilter?: unknown; maxDepth?: number } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture2',
capabilities: { outputSchema: false, depthLimit: true, toolFilter: true, persona: true },
@@ -689,8 +689,8 @@ describe('dsh-tool-subagent', () => {
let seen: { toolFilter?: { readonly allow?: readonly string[]; readonly deny?: readonly string[] } } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture3',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: true, persona: false },
@@ -719,8 +719,8 @@ describe('dsh-tool-subagent', () => {
let seen: { agentOptions?: unknown } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture4',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -744,8 +744,8 @@ describe('dsh-tool-subagent', () => {
it('an explicit empty toolFilter fails at plugin load, not at first delegation', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'p',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: true, persona: false },
@@ -776,7 +776,7 @@ describe('dsh-tool-subagent background mode', () => {
async function backgroundSetup(toolConfig: tool.Config, mockConfig: Partial<mock.Config> = {}) {
const ctx = await setup(toolConfig, mockConfig)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalTaskService)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks, {})
return ctx
}
@@ -822,21 +822,21 @@ describe('dsh-tool-subagent background mode', () => {
expect(prepareCalls).toBe(0)
})
it('returns a task id immediately and the answer is collected through task_output', async () => {
it('returns a job id immediately and the answer is collected through job_output', async () => {
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } }, { reply: 'background answer' })
const parent = ownerAgent(ctx, 'sess-parent')
const start = await callSubagent(ctx, { description: 'deep research', prompt: 'dig in', run_in_background: true }, { agent: parent })
expect(start.isError).toBe(false)
if (start.isError) throw new Error('expected background subagent success')
expect(start.value).toEqual({ kind: 'background', taskId: 'subagent-1' })
expect(start.value).toEqual({ kind: 'background', jobId: 'subagent-1' })
expect(text(start)).toBe('started background subagent task subagent-1')
const collected = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('collect-1'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
name: 'job_output',
arguments: { job_id: 'subagent-1', wait: true },
agent: parent,
})
expect(text(collected)).toBe('background answer\n[status: completed]')
@@ -845,8 +845,8 @@ describe('dsh-tool-subagent background mode', () => {
const again = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('collect-2'),
name: 'task_output',
arguments: { task_id: 'subagent-1' },
name: 'job_output',
arguments: { job_id: 'subagent-1' },
agent: parent,
})
expect(text(again)).toBe('background answer\n[status: completed]')
@@ -856,7 +856,7 @@ describe('dsh-tool-subagent background mode', () => {
const ctx = await setup({ provider: 'mock' })
const result = await callSubagent(ctx, { description: 'd', prompt: 'p', run_in_background: true })
expect(result.isError).toBe(true)
expect(text(result)).toContain('background tasks unavailable: load @deepseek-ai/dsh-tasks')
expect(text(result)).toContain('background jobs unavailable: load @deepseek-ai/dsh-jobs')
})
it('skips background startup when the tool signal is already aborted', async () => {
@@ -895,8 +895,8 @@ describe('dsh-tool-subagent background mode', () => {
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-output'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
name: 'job_output',
arguments: { job_id: 'subagent-1', wait: true },
agent: parent,
})
expect(text(output)).toContain('[status: failed, Error: setup failed]')
@@ -925,21 +925,21 @@ describe('dsh-tool-subagent background mode', () => {
await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('pending-kill'),
name: 'task_kill',
arguments: { task_id: 'subagent-1', reason: 'no longer needed' },
name: 'job_kill',
arguments: { job_id: 'subagent-1', reason: 'no longer needed' },
agent: parent,
})
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('pending-output'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
name: 'job_output',
arguments: { job_id: 'subagent-1', wait: true },
agent: parent,
})
expect(text(output)).toBe('(no new output)\n[status: killed]')
})
it('reports startup rollback failure after cancellation as a failed task', async () => {
it('reports startup rollback failure after cancellation as a failed job', async () => {
const ctx = await backgroundSetup({ provider: 'mock' })
const parent = ownerAgent(ctx, 'sess-parent')
ctx.subagents.registerProvider({
@@ -967,21 +967,21 @@ describe('dsh-tool-subagent background mode', () => {
await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-rollback-kill'),
name: 'task_kill',
arguments: { task_id: 'subagent-1' },
name: 'job_kill',
arguments: { job_id: 'subagent-1' },
agent: parent,
})
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-rollback-output'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
name: 'job_output',
arguments: { job_id: 'subagent-1', wait: true },
agent: parent,
})
expect(text(output)).toContain('[status: failed, AggregateError: startup failed and cleanup also failed]')
})
it('forwards task_kill reasons through the run signal (and defaults one when absent)', async () => {
it('forwards job_kill reasons through the run signal (and defaults one when absent)', async () => {
// Use a provider that remains live until its signal is aborted.
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } })
const parent = ownerAgent(ctx, 'sess-parent')
@@ -1015,14 +1015,14 @@ describe('dsh-tool-subagent background mode', () => {
expect(text(startOne)).toBe('started background subagent task subagent-1')
expect(text(startTwo)).toBe('started background subagent task subagent-2')
const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k1'), name: 'task_kill', arguments: { task_id: 'subagent-1', reason: 'superseded' }, agent: parent })
const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k2'), name: 'task_kill', arguments: { task_id: 'subagent-2' }, agent: parent })
expect(text(withReason)).toBe('requested cancellation of task subagent-1')
expect(text(withoutReason)).toBe('requested cancellation of task subagent-2')
const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k1'), name: 'job_kill', arguments: { job_id: 'subagent-1', reason: 'superseded' }, agent: parent })
const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k2'), name: 'job_kill', arguments: { job_id: 'subagent-2' }, agent: parent })
expect(text(withReason)).toBe('requested cancellation of job subagent-1')
expect(text(withoutReason)).toBe('requested cancellation of job subagent-2')
expect(cancels).toEqual(['superseded', 'background subagent task killed'])
// The aborted children settle as killed tasks.
const killed = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('w1'), name: 'task_output', arguments: { task_id: 'subagent-1', wait: true }, agent: parent })
const killed = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('w1'), name: 'job_output', arguments: { job_id: 'subagent-1', wait: true }, agent: parent })
expect(text(killed)).toBe('(no new output)\n[status: killed]')
})
@@ -1042,9 +1042,9 @@ describe('dsh-tool-subagent continuable background mode', () => {
roots.push(root)
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(LocalTaskService)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks, {})
await ctx.plugin(tool, { provider: 'spawn', backgroundMode: 'continuable' })
ctx.llm.registerAdapter(['mock'], new MockAdapter([
@@ -1068,8 +1068,8 @@ describe('dsh-tool-subagent continuable background mode', () => {
const { ctx, parent } = await continuableSetup()
const schema = ctx.tools.schemas().find(s => s.name === 'subagent')!
// Continuable delegation has no Task, so the schema promises no collection.
expect(schema.description).not.toContain('task_output')
expect(schema.description).not.toContain('task_kill')
expect(schema.description).not.toContain('job_output')
expect(schema.description).not.toContain('job_kill')
expect(schema.description).toContain('send_message')
expect(schema.description).toContain('runs in the background by default')
expect(schema.description).not.toContain('never poll or wait on it')
@@ -1092,7 +1092,7 @@ describe('dsh-tool-subagent continuable background mode', () => {
expect(match).not.toBeNull()
const [, childId] = match!
// No Task was created for the continuable child.
expect(ctx.tasks.list(parent)).toEqual([])
expect(ctx.jobs.list(parent)).toEqual([])
await vi.waitFor(() => {
expect(ctx.agents.get(SessionId(childId!))).toBeUndefined()
@@ -1123,7 +1123,7 @@ describe('dsh-tool-subagent continuable background mode', () => {
if (result.isError) throw new Error('expected foreground subagent success')
expect(result.value).toMatchObject({ kind: 'foreground' })
expect(text(result)).toBe('continuable answer')
expect(ctx.tasks.list(parent)).toEqual([])
expect(ctx.jobs.list(parent)).toEqual([])
})
it('isolates a cancelled continuable preparation from a concurrent sibling', async () => {
@@ -1193,10 +1193,10 @@ describe('dsh-tool-subagent continuable background mode', () => {
describe('background preflight failure (no orphaned child, by construction)', () => {
it('never starts the child when tasks.start preflight throws', async () => {
// With no task controller, preflight fails before the provider can spawn.
// With no job controller, preflight fails before the provider can spawn.
const ctx = await setup({ provider: 'mock' })
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalTaskService)
await ctx.plugin(LocalJobRegistry)
const scopeFiber = ctx.plugin(() => {})
const id = SessionId('sess-p')
const parent = {
@@ -1233,7 +1233,7 @@ describe('background preflight failure (no orphaned child, by construction)', ()
agent: parent,
})
expect(result.isError).toBe(true)
expect(text(result)).toContain('no task controller serves this agent')
expect(text(result)).toContain('no job controller serves this agent')
// Declare-then-execute: the failed preflight means no child ever existed.
expect(starts).toBe(0)
})
@@ -1245,8 +1245,8 @@ describe('depth budget configuration', () => {
const requests: SubagentStartRequest[] = []
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true },
@@ -1283,8 +1283,8 @@ describe('depth budget configuration', () => {
it('rejects a numeric maxDepth on a provider without the depthLimit capability at mount', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'no-depth',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -1299,8 +1299,8 @@ describe('depth budget configuration', () => {
const requests: SubagentStartRequest[] = []
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'external',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },

View File

@@ -33,10 +33,10 @@
"path": "../subagent"
},
{
"path": "../../tasks/tasks"
"path": "../../jobs/jobs"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}