@deepseek-ai/dsh-subagent-acp 0.1.1-rc.2 → 0.1.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +114 -37
- package/README.zh.md +120 -43
- package/lib/index.js +276 -89
- package/lib/types/index.d.ts +1 -1
- package/lib/types/run.d.ts +19 -14
- package/package.json +29 -22
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/subagent/subagent-acp/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 185a06a4003386a18414998e9cbaa314b9599921
|
|
6
|
+
README.zh.md: 1594677f2ecdacd4ca389337fb29bd01ff18a108
|
package/README.md
CHANGED
|
@@ -1,73 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The out-of-process ACP subagent backend for users and maintainers choosing a delegation provider, configuring a child ACP agent command, or debugging remote child runs."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-subagent-acp
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
`dsh-subagent-acp` runs each delegated child in a fresh subprocess and drives it as an Agent Client Protocol client: the child gets its own runtime, session, model configuration, and tools, and it can be any ACP-compatible agent, not just Harness. It is the out-of-process alternative to the in-process spawn and fork backends, sharing only the parent session's working directory with the child. Each run spawns a fresh process, initializes an ACP session, sends the task, and collects the streamed final answer; permission prompts are auto-answered by configuration, so no human is needed. The parent receives only the child's final answer or a safe error — no intermediate messages or tool traffic crosses the boundary. Choose it when the child must be fully isolated from the parent harness and can speak ACP.
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
## Table of Contents
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
-----
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
16
27
|
|
|
17
|
-
|
|
28
|
+
Mount this provider when a composition needs a fully isolated, out-of-process child that speaks the Agent Client Protocol. The common path is explicit: mount the seam, mount this provider, and give it a command that starts an ACP agent.
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
### When to choose it
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
Choose this backend when the child must run with its own runtime, model, and tools in a separate process — for example an ACP agent from another project — or when you want delegation that cannot touch the parent harness. Choose an in-process backend when the child must share the parent's composition or honor parent-enforced capabilities: this provider advertises no optional start-time capabilities, so the seam rejects requests for `agentOptions`, structured output, depth caps, tool filters, or personas rather than silently omitting them.
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
### Configuration
|
|
24
35
|
|
|
25
|
-
|
|
|
36
|
+
| Field | Default | Meaning |
|
|
26
37
|
|---|---|---|
|
|
27
|
-
| `providerName` | `acp` | Registry name on `ctx.subagents
|
|
28
|
-
| `command` | required | Executable spawned for each run
|
|
29
|
-
| `args` | `[]` | Command arguments
|
|
30
|
-
| `cwd` | parent session cwd | Working-directory override for the child process and its ACP session
|
|
31
|
-
| `permission` | `reject` | Auto-answer permission requests by rejecting or choosing the first `allow_once` or `allow_always` option
|
|
32
|
-
| `env` | `{}` | Explicit child environment layered over
|
|
33
|
-
| `disposeEofGraceMs` | `6000` |
|
|
34
|
-
| `disposeGraceMs` | `3000` |
|
|
38
|
+
| `providerName` | `acp` | Registry name on `ctx.subagents` |
|
|
39
|
+
| `command` | required | Executable spawned for each run (the child ACP agent) |
|
|
40
|
+
| `args` | `[]` | Command arguments |
|
|
41
|
+
| `cwd` | parent session cwd | Working-directory override for the child process and its ACP session |
|
|
42
|
+
| `permission` | `reject` | Auto-answer permission requests by rejecting, or choosing the first `allow_once` or `allow_always` option (`allow`) |
|
|
43
|
+
| `env` | `{}` | Explicit child environment layered over the credential-scrubbed parent environment |
|
|
44
|
+
| `disposeEofGraceMs` | `6000` | Grace after stdin EOF before platform termination |
|
|
45
|
+
| `disposeGraceMs` | `3000` | Bound for observing structured process facts after failure and, on POSIX, the SIGTERM-to-SIGKILL grace |
|
|
46
|
+
|
|
47
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-subagent-acp) is the exhaustive source for every accepted field and its JSDoc.
|
|
48
|
+
|
|
49
|
+
A DeepSeek Harness child uses the product launcher and an explicit absolute `DSH_HOME`. The isolated home prevents a nested runtime from discovering the launching person's profiles or credentials; the generic ACP provider does not impose this requirement on non-DSH agents.
|
|
35
50
|
|
|
36
51
|
```yaml
|
|
37
52
|
- id: subagent-acp
|
|
38
53
|
name: '@deepseek-ai/dsh-subagent-acp'
|
|
39
54
|
config:
|
|
40
55
|
providerName: acp
|
|
41
|
-
command:
|
|
42
|
-
args: ['--
|
|
56
|
+
command: dsh
|
|
57
|
+
args: ['--profile', 'acp', '--patch', '/absolute/path/to/acp.patch.yml']
|
|
43
58
|
permission: reject
|
|
44
59
|
env:
|
|
60
|
+
DSH_HOME: /absolute/path/to/isolated-child-home
|
|
45
61
|
DEEPSEEK_API_KEY: !!js process.env.DEEPSEEK_API_KEY
|
|
46
62
|
```
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
### What you get
|
|
65
|
+
|
|
66
|
+
A successful run returns the child's final streamed assistant text as the result output. The child's session, model, and tools come from the child process itself — the parent supplies only the task and the working directory. The stop reason maps `end_turn` to `completed`, `max_tokens` to `max-tokens`, `refusal` to `refusal`, `cancelled` to `aborted`, and every other value to `error`. A failed published run preserves partial assistant text in `output` and returns safe structured detail separately in `diagnostic`.
|
|
67
|
+
|
|
68
|
+
### Failure and recovery
|
|
69
|
+
|
|
70
|
+
A spawn, initialization, or new-session failure rejects before publication, ordinarily after the child process is reaped. If cleanup also fails, the rejection preserves ordered safe startup and teardown facts without claiming whole-tree quiescence. Non-cancellation errors expose only fixed provider, stage, and category facts; the original failure stays on the internal cause chain and in Host diagnostics. After publication, a prompt, transport, or early-process failure resolves as `error` with a safe diagnostic, while local cancellation resolves as `aborted` without failure detail.
|
|
71
|
+
|
|
72
|
+
### Safe diagnostics
|
|
73
|
+
|
|
74
|
+
A generic diagnostic uses one fixed line: `Subagent failure (provider: ACP; stage: <stage>; category: <category>; ...)`. Optional stop reason, exit code, and signal come only from closed protocol or managed-process facts. Stderr, exception text, task content, tool input, paths, environment values, credentials, and protocol payloads never enter the diagnostic; the shared result boundary limits it to 4096 UTF-8 bytes. A non-completed run that requested permission can add one fixed policy, tool-kind, and decision line. Successful runs and local cancellation omit it.
|
|
75
|
+
|
|
76
|
+
-----
|
|
77
|
+
|
|
78
|
+
<a id="understand-the-implementation"></a>
|
|
79
|
+
## Understand the implementation
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary>Implementation internals — click to expand</summary>
|
|
83
|
+
|
|
84
|
+
This section explains how the backend drives a child over ACP and where the observable behavior comes from; the full contract lives in [Use this package](#use-this-package).
|
|
85
|
+
|
|
86
|
+
### Design concept
|
|
87
|
+
|
|
88
|
+
- **Full process isolation.** Each child runs in a fresh subprocess with its own session, model, and tools; only the resolved working directory crosses from the parent.
|
|
89
|
+
- **One process per run.** Every run spawns a new process; there is no process pooling.
|
|
90
|
+
- **The ACP wire is the serialization boundary.** Same-process subagent values are not defensively cloned; the protocol is where hostile input is validated.
|
|
91
|
+
|
|
92
|
+
### Start and ownership flow
|
|
49
93
|
|
|
50
|
-
|
|
51
|
-
|---|---|
|
|
52
|
-
| `end_turn` | `completed` |
|
|
53
|
-
| `max_tokens` | `max-tokens` |
|
|
54
|
-
| `refusal` | `refusal` |
|
|
55
|
-
| `cancelled` | `aborted` |
|
|
56
|
-
| `max_turn_requests` or unknown | `error` |
|
|
94
|
+
A start resolves the child's working directory (the configured `cwd` override, else the parent session's cwd), spawns the command through the subprocess seam, performs the ACP `initialize` and `newSession` handshake, and only then publishes the run. Fulfillment means a remote session is ready and ownership has transferred to the caller. Disposal is idempotent: it closes stdin and waits a configured grace for cooperative quiescence, then escalates through SIGTERM to SIGKILL and awaits whole-tree exit. Cleanup failures remain observable as ordered safe facts and never claim quiescence.
|
|
57
95
|
|
|
58
|
-
|
|
96
|
+
### Stop-reason mapping
|
|
59
97
|
|
|
60
|
-
The
|
|
98
|
+
The run outcome maps the ACP terminal into the shared stop-reason vocabulary (`completed`, `max-tokens`, `refusal`, `aborted`, or `error`) in [`src/run.ts`](src/run.ts).
|
|
61
99
|
|
|
62
|
-
|
|
100
|
+
### Process boundary
|
|
63
101
|
|
|
102
|
+
The child spawns through the subprocess seam: credential-shaped ambient variables are scrubbed, then explicit `config.env` values merge after the scrub. Stderr is inherited to the parent's stream, and disposal applies this provider's EOF window before the shared termination escalation.
|
|
103
|
+
|
|
104
|
+
</details>
|
|
105
|
+
|
|
106
|
+
-----
|
|
107
|
+
|
|
108
|
+
<a id="further-exploration"></a>
|
|
109
|
+
## Further Exploration
|
|
110
|
+
|
|
111
|
+
Read these pages when the package-level contract is not enough. They move from this backend to the seam it plugs into and the protocol it drives.
|
|
112
|
+
|
|
113
|
+
- [Subagent subsystem](../../../docs/subsystems/subagent.md) — the service contract, provider contract, and terminal result semantics.
|
|
114
|
+
- [dsh-subagent seam](../subagent/README.md) — the registry and start API this provider registers on.
|
|
115
|
+
- [Agent Client Protocol automation server](../../acp/acp/README.md) — the automation-only server this provider drives as a client.
|
|
116
|
+
- [dsh-subprocess seam](../../subprocess/subprocess/README.md) — the process-spawn and teardown machinery behind each run.
|
|
117
|
+
- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-subagent-acp) — every accepted config field and its source declaration.
|
|
118
|
+
|
|
119
|
+
-----
|
|
120
|
+
|
|
121
|
+
<a id="model-experience"></a>
|
|
64
122
|
## Model Experience
|
|
65
123
|
|
|
66
124
|
### Child-agent request
|
|
67
125
|
|
|
68
126
|
#### What the model sees
|
|
69
127
|
|
|
70
|
-
The remote child receives the standalone task content through ACP plus its own process's configured system prompt, tools, and fresh session. It receives no parent conversation. This provider advertises no optional start-time capabilities, so the local service rejects requests for persona, tool filtering, depth enforcement, or structured output instead of silently omitting them.
|
|
128
|
+
The remote child receives the standalone task content through ACP plus its own process's configured system prompt, tools, and fresh session. It receives no parent conversation. This provider advertises no optional start-time capabilities, so the local service rejects requests for `agentOptions`, persona, tool filtering, depth enforcement, or structured output instead of silently omitting them.
|
|
71
129
|
|
|
72
130
|
#### Token effect
|
|
73
131
|
|
|
@@ -81,7 +139,7 @@ Independent of the parent request cache. Each ACP child can reuse only prefixes
|
|
|
81
139
|
|
|
82
140
|
#### What the model sees
|
|
83
141
|
|
|
84
|
-
Through `dsh-tool-subagent`, the parent receives only the child's final streamed assistant text or that consumer's exact stop-reason error, not intermediate messages or tool traffic. A request already cancelled before publication becomes exactly `Error: subagent request was aborted before the ACP child started`;
|
|
142
|
+
Through `dsh-tool-subagent`, the parent receives only the child's final streamed assistant text or that consumer's exact stop-reason error, not intermediate messages or tool traffic. Non-completed results present the safe diagnostic before separately preserved partial assistant output. A request already cancelled before publication becomes exactly `Error: subagent request was aborted before the ACP child started`; another start failure contains only the fixed `Subagent failure (...)` line.
|
|
85
143
|
|
|
86
144
|
#### Token effect
|
|
87
145
|
|
|
@@ -93,8 +151,27 @@ Append-only; newly visible content follows the reusable request prefix and does
|
|
|
93
151
|
|
|
94
152
|
## Known Limitations and Deferred Work
|
|
95
153
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
154
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
These limits define when this backend is a poor fit or needs special operational care. They are current package constraints, not a general ACP comparison or a task backlog.
|
|
158
|
+
|
|
159
|
+
- **A fresh process per run** — there is no process pooling; each delegation pays the full spawn and ACP handshake cost.
|
|
160
|
+
- **Local workspaces only** — the resolved working directory is a local path handed to a child on the same machine; remote workspace mapping is not designed.
|
|
161
|
+
- **No optional start-time capabilities** — this provider cannot apply `agentOptions`, `outputSchema`, a depth cap, a tool filter, or a persona inside the remote process, so the seam rejects requests that require them.
|
|
99
162
|
- **Only committed `agent_message_chunk` text is collected** — the automation server keeps reasoning, tool activity, plans, and other trace data in the child session log rather than emitting them on ACP.
|
|
100
163
|
- **Permission prompts are auto-answered** (`permission: allow | reject`) — no human is surfaced a child's `session/request_permission`.
|
|
164
|
+
|
|
165
|
+
<a id="dev-note"></a>
|
|
166
|
+
### Dev Note
|
|
167
|
+
|
|
168
|
+
<details>
|
|
169
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
170
|
+
|
|
171
|
+
This Dev Note is working context for maintainers: open questions and undecided directions. It is explicitly non-authoritative — shipped behavior and limits live in the sections above and in the package code.
|
|
172
|
+
|
|
173
|
+
- **Process pooling** — persistent-process reuse is a possible future optimization but changes the per-run isolation model.
|
|
174
|
+
- **Remote workspaces** — mapping a remote ACP agent's workspace would need its own backend capability.
|
|
175
|
+
- **Continuable ACP children** — would require persisting the remote session id and a per-child continuation advertisement.
|
|
176
|
+
|
|
177
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,73 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向用户与维护者的进程外 ACP subagent 后端,用于选择委派提供方、配置子 ACP agent 命令或排查远程子 agent 运行问题。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-subagent-acp
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
`dsh-subagent-acp` 在全新的子进程中运行每个被委派的子 agent,并作为 Agent Client Protocol 客户端驱动它:子 agent(智能体)拥有自己的运行时、会话、模型配置和工具,可以是任何兼容 ACP 的 agent,而不只是 Harness。它是进程内 spawn 与 fork 后端的进程外替代方案,只与子 agent 共享父会话的工作目录。每次运行都会 spawn 全新进程、初始化 ACP 会话、发送任务并收集流式最终答案;权限提示由配置自动应答,因此无需人工参与。父级只收到子 agent 的最终答案或安全错误——中间消息与工具流量不会跨越边界。当子 agent 必须与父 harness 完全隔离且能说 ACP 时,选择它。
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
## 目录
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
-----
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
16
27
|
|
|
17
|
-
|
|
28
|
+
当组合需要一个说 Agent Client Protocol 的完全隔离进程外子 agent 时,挂载本提供方。常用路径是显式的:挂载 seam、挂载本提供方,并给出一个启动 ACP agent 的命令。
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
### 何时选择
|
|
20
31
|
|
|
21
|
-
ACP
|
|
32
|
+
当子 agent 必须在独立进程中运行、拥有自己的运行时、模型和工具时选择此后端——例如来自其他项目的 ACP agent——或者你希望委派完全无法触及父 harness 时。当子 agent 必须共享父级组合或遵守父级强制的能力时,请选择进程内后端:本提供方不声明任何可选启动时能力,因此 seam 会拒绝要求 `agentOptions`、结构化输出、深度上限、工具过滤或 persona 的请求,而不是静默省略。
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
### 配置
|
|
24
35
|
|
|
25
|
-
|
|
|
36
|
+
| 字段 | 默认值 | 含义 |
|
|
26
37
|
|---|---|---|
|
|
27
|
-
| `providerName` | `acp` | `ctx.subagents`
|
|
28
|
-
| `command` | 必填 | 每次运行时 spawn
|
|
29
|
-
| `args` | `[]` |
|
|
30
|
-
| `cwd` | 父会话 cwd | 子进程及其 ACP
|
|
31
|
-
| `permission` | `reject` |
|
|
32
|
-
| `env` | `{}` |
|
|
33
|
-
| `disposeEofGraceMs` | `6000` | stdin EOF
|
|
34
|
-
| `disposeGraceMs` | `3000` | POSIX
|
|
38
|
+
| `providerName` | `acp` | `ctx.subagents` 上的注册表名称 |
|
|
39
|
+
| `command` | 必填 | 每次运行时 spawn 的可执行文件(子 ACP agent) |
|
|
40
|
+
| `args` | `[]` | 命令参数 |
|
|
41
|
+
| `cwd` | 父会话 cwd | 子进程及其 ACP 会话的工作目录覆盖值 |
|
|
42
|
+
| `permission` | `reject` | 自动应答权限请求:拒绝,或选择第一个 `allow_once` 或 `allow_always` 选项(`allow`) |
|
|
43
|
+
| `env` | `{}` | 叠加在已清理凭据的父环境之上的显式子环境 |
|
|
44
|
+
| `disposeEofGraceMs` | `6000` | stdin EOF 之后、平台终止之前的宽限 |
|
|
45
|
+
| `disposeGraceMs` | `3000` | 失败后观察结构化进程事实的时限;在 POSIX 上也是 SIGTERM 到 SIGKILL 的宽限 |
|
|
46
|
+
|
|
47
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-subagent-acp)是每个受支持字段及其 JSDoc 的穷尽式真源。
|
|
48
|
+
|
|
49
|
+
DeepSeek Harness 子进程使用产品启动器和一个显式的绝对路径 `DSH_HOME`。隔离 home 可防止嵌套 runtime 发现启动者个人的 profile 或凭据;通用 ACP provider 不会把这一要求强加给非 DSH agent。
|
|
35
50
|
|
|
36
51
|
```yaml
|
|
37
52
|
- id: subagent-acp
|
|
38
53
|
name: '@deepseek-ai/dsh-subagent-acp'
|
|
39
54
|
config:
|
|
40
55
|
providerName: acp
|
|
41
|
-
command:
|
|
42
|
-
args: ['--
|
|
56
|
+
command: dsh
|
|
57
|
+
args: ['--profile', 'acp', '--patch', '/absolute/path/to/acp.patch.yml']
|
|
43
58
|
permission: reject
|
|
44
59
|
env:
|
|
60
|
+
DSH_HOME: /absolute/path/to/isolated-child-home
|
|
45
61
|
DEEPSEEK_API_KEY: !!js process.env.DEEPSEEK_API_KEY
|
|
46
62
|
```
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
### 你会得到什么
|
|
65
|
+
|
|
66
|
+
成功的运行会把子 agent 最终的流式 assistant 文本作为结果输出返回。子 agent 的会话、模型与工具来自子进程自身——父级只提供任务与工作目录。停止原因把 `end_turn` 映射为 `completed`、`max_tokens` 映射为 `max-tokens`、`refusal` 映射为 `refusal`、`cancelled` 映射为 `aborted`,其余值映射为 `error`。已发布运行失败时,部分 assistant 文本保留在 `output`,安全的结构化详情则单独放在 `diagnostic`。
|
|
67
|
+
|
|
68
|
+
### 失败与恢复
|
|
69
|
+
|
|
70
|
+
spawn、初始化或新建会话失败会在发布前拒绝,通常先等待子进程被回收。如果清理也失败,拒绝会保留有序、安全的启动与拆卸事实,但不会声称整棵进程树已经停稳。非取消错误只暴露固定的提供方、阶段与类别事实;原始失败保留在内部 cause 链与 Host 诊断中。发布后,提示词、传输或进程提前退出会以携带安全诊断的 `error` 结算;本地取消则以不带失败详情的 `aborted` 结算。
|
|
71
|
+
|
|
72
|
+
### 安全诊断
|
|
73
|
+
|
|
74
|
+
通用诊断使用固定的一行:`Subagent failure (provider: ACP; stage: <stage>; category: <category>; ...)`。可选的停止原因、退出码与信号只来自封闭协议或受管进程事实。stderr、异常文本、任务内容、工具输入、路径、环境值、凭据与协议载荷绝不会进入诊断;共享结果边界把诊断限制在 4096 个 UTF-8 字节内。请求过权限且未完成的运行可以增加一行固定的策略、工具种类与决定。成功运行和本地取消不包含该行。
|
|
75
|
+
|
|
76
|
+
-----
|
|
77
|
+
|
|
78
|
+
<a id="understand-the-implementation"></a>
|
|
79
|
+
## 理解实现
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary>实现细节——点击展开</summary>
|
|
83
|
+
|
|
84
|
+
本节解释后端如何经 ACP 驱动子 agent,以及可观察行为从何而来;完整约定见[使用本包](#use-this-package)。
|
|
85
|
+
|
|
86
|
+
### 设计理念
|
|
87
|
+
|
|
88
|
+
- **完全进程隔离。** 每个子 agent 在全新子进程中运行,拥有自己的会话、模型与工具;只有解析后的工作目录从父级跨越。
|
|
89
|
+
- **每次运行一个进程。** 每次运行都 spawn 新进程;没有进程池。
|
|
90
|
+
- **ACP 协议格式(wire format)是序列化边界。** 同进程 subagent 值不会为防御目的克隆;协议才是校验不可信输入的地方。
|
|
91
|
+
|
|
92
|
+
### 启动与所有权流程
|
|
49
93
|
|
|
50
|
-
|
|
51
|
-
|---|---|
|
|
52
|
-
| `end_turn` | `completed` |
|
|
53
|
-
| `max_tokens` | `max-tokens` |
|
|
54
|
-
| `refusal` | `refusal` |
|
|
55
|
-
| `cancelled` | `aborted` |
|
|
56
|
-
| `max_turn_requests` 或未知值 | `error` |
|
|
94
|
+
一次启动先解析子 agent 的工作目录(配置的 `cwd` 覆盖值,否则取父会话 cwd),经子进程 seam spawn 命令,完成 ACP `initialize` 与 `newSession` 握手,然后才发布运行。兑现意味着远程会话已就绪、所有权已转移给调用方。dispose(资源释放)是幂等的:先关闭 stdin 并按配置的宽限等待协作式完全停稳,再经 SIGTERM 升级到 SIGKILL,并等待整棵进程树退出。清理失败会作为有序的安全事实保持可观察,且绝不声称已经完全停稳。
|
|
57
95
|
|
|
58
|
-
|
|
96
|
+
### 停止原因映射
|
|
59
97
|
|
|
60
|
-
|
|
98
|
+
运行结果会把 ACP 终态映射进共享的停止原因词汇(`completed`、`max-tokens`、`refusal`、`aborted` 或 `error`),实现见 [`src/run.ts`](src/run.ts)。
|
|
61
99
|
|
|
62
|
-
|
|
100
|
+
### 进程边界
|
|
63
101
|
|
|
102
|
+
子进程经子进程 seam spawn:先清除疑似凭据的环境变量,再合并显式 `config.env` 值。stderr 继承到父级流,dispose 先应用本提供方的 EOF 窗口,再执行共享的逐级终止。
|
|
103
|
+
|
|
104
|
+
</details>
|
|
105
|
+
|
|
106
|
+
-----
|
|
107
|
+
|
|
108
|
+
<a id="further-exploration"></a>
|
|
109
|
+
## 进一步探索
|
|
110
|
+
|
|
111
|
+
当包级约定不够用时阅读以下页面。它们从本后端逐步进入它接入的 seam 与它驱动的协议。
|
|
112
|
+
|
|
113
|
+
- [Subagent 子系统](../../../docs/subsystems/subagent.zh.md)——服务约定、提供方约定与终态结果语义。
|
|
114
|
+
- [dsh-subagent seam](../subagent/README.zh.md)——本提供方注册于其上的注册表与启动 API。
|
|
115
|
+
- [Agent Client Protocol 自动化服务器](../../acp/acp/README.zh.md)——本提供方作为客户端驱动的仅自动化服务器。
|
|
116
|
+
- [dsh-subprocess seam](../../subprocess/subprocess/README.zh.md)——每次运行背后的进程 spawn 与清理机制。
|
|
117
|
+
- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-subagent-acp)——每个受支持配置字段及其源声明。
|
|
118
|
+
|
|
119
|
+
-----
|
|
120
|
+
|
|
121
|
+
<a id="model-experience"></a>
|
|
64
122
|
## 模型体验
|
|
65
123
|
|
|
66
124
|
### 子 agent 请求
|
|
67
125
|
|
|
68
|
-
####
|
|
126
|
+
#### 模型看到什么
|
|
69
127
|
|
|
70
|
-
远程子 agent 通过 ACP
|
|
128
|
+
远程子 agent 通过 ACP 接收独立任务内容,并使用其自身进程配置的系统提示词、工具和全新会话。它不接收父级对话。本提供方不声明可选启动时能力,因此本地服务会拒绝要求 `agentOptions`、persona、工具过滤、深度强制或结构化输出的请求,而不是静默省略。
|
|
71
129
|
|
|
72
130
|
#### Token 影响
|
|
73
131
|
|
|
@@ -79,22 +137,41 @@ ACP 不声明任何启动时能力,因为当前进程无法强制执行远程
|
|
|
79
137
|
|
|
80
138
|
### 父级工具结果(间接)
|
|
81
139
|
|
|
82
|
-
####
|
|
140
|
+
#### 模型看到什么
|
|
83
141
|
|
|
84
|
-
通过 `dsh-tool-subagent`,父级只接收子 agent 最终的流式 assistant
|
|
142
|
+
通过 `dsh-tool-subagent`,父级只接收子 agent 最终的流式 assistant 文本或该消费方给出的精确停止原因错误,不接收中间消息或工具流量。未完成的结果会先呈现安全诊断,再单独保留部分 assistant 输出。发布前已经取消的请求会精确变为 `Error: subagent request was aborted before the ACP child started`;其他启动失败只包含固定的 `Subagent failure (...)` 行。
|
|
85
143
|
|
|
86
144
|
#### Token 影响
|
|
87
145
|
|
|
88
|
-
父级输入只增加最终结果或错误,其内容依赖数据,并保留到压缩(compaction
|
|
146
|
+
父级输入只增加最终结果或错误,其内容依赖数据,并保留到压缩(compaction)为止。本提供方自身不会添加父级 schema。
|
|
89
147
|
|
|
90
148
|
#### KV Cache 影响
|
|
91
149
|
|
|
92
150
|
仅追加;新增可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
93
151
|
|
|
94
|
-
##
|
|
152
|
+
## 已知限制与延期工作
|
|
153
|
+
|
|
154
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
这些限制说明本后端何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用 ACP 对比或任务积压。
|
|
158
|
+
|
|
159
|
+
- **每次运行使用全新进程**——没有进程池;每次委派都要付出完整的 spawn 与 ACP 握手成本。
|
|
160
|
+
- **仅支持本地工作区**——解析后的工作目录是交给同一台机器上子进程的本地路径;远程工作区映射尚未设计。
|
|
161
|
+
- **不支持可选启动时能力**——本提供方无法在远程进程内应用 `agentOptions`、`outputSchema`、深度上限、工具过滤器或 persona,因此 seam 会拒绝需要它们的请求。
|
|
162
|
+
- **只收集已提交的 `agent_message_chunk` 文本**——自动化服务器把推理(reasoning)、工具活动、计划和其他 trace 数据保留在子 agent 会话日志中,不通过 ACP 发出。
|
|
163
|
+
- **权限提示自动应答**(`permission: allow | reject`)——不会把子 agent 的 `session/request_permission` 呈现给人。
|
|
164
|
+
|
|
165
|
+
<a id="dev-note"></a>
|
|
166
|
+
### 开发备注
|
|
167
|
+
|
|
168
|
+
<details>
|
|
169
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
170
|
+
|
|
171
|
+
本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为与限制以上文和包代码为准。
|
|
172
|
+
|
|
173
|
+
- **进程池**——持久进程复用是可能的未来优化,但会改变每次运行的隔离模型。
|
|
174
|
+
- **远程工作区**——映射远程 ACP agent 的工作区需要独立的后端能力。
|
|
175
|
+
- **可继续 ACP 子级**——需要持久化远程会话 id 与逐子级的继续执行能力声明。
|
|
95
176
|
|
|
96
|
-
|
|
97
|
-
- **仅支持本地工作区**:解析后的 cwd 是交给同一台机器上子进程的本地路径;远程 ACP agent 的工作区映射需要独立的后端能力,此处尚未设计这种能力。
|
|
98
|
-
- **不支持可选启动时能力**:该提供方无法在远程进程内应用本地 harness 的 `outputSchema`、深度上限、工具过滤器或 persona,因此不会声明这些能力;服务会拒绝需要它们的请求。
|
|
99
|
-
- **只收集已提交的 `agent_message_chunk` 文本**:自动化服务器把推理(reasoning)、工具活动、计划和其他 trace 数据保留在子 agent 会话日志中,不通过 ACP 发出。
|
|
100
|
-
- **权限提示自动回答**(`permission: allow | reject`):不会把子 agent 的 `session/request_permission` 呈现给人。
|
|
177
|
+
</details>
|
package/lib/index.js
CHANGED
|
@@ -4,23 +4,76 @@ import z from "@deepseek-ai/schemastery";
|
|
|
4
4
|
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { Readable, Writable } from "node:stream";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { AssistantOutputFold } from "@deepseek-ai/dsh-subagent";
|
|
7
|
+
import { PROTOCOL_VERSION, client, methods, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
8
|
+
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
9
|
+
import { AssistantOutputFold, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
|
|
10
10
|
//#region lib/types/run.js
|
|
11
11
|
/**
|
|
12
12
|
* Fresh-process ACP subagent client. Drives one child session and owns cancellation and
|
|
13
13
|
* quiescent disposal.
|
|
14
14
|
*
|
|
15
|
-
* TODO(acp-subagent-replay): add snapshot-tier coverage with a separate replay fixture and
|
|
16
|
-
* sessions root inside each child process. Current keyless coverage uses a scripted ACP child;
|
|
17
|
-
* with-key coverage drives the real ACP example.
|
|
18
15
|
* @module @deepseek-ai/dsh-subagent-acp/run
|
|
19
16
|
*/
|
|
20
17
|
/** EOF grace for child flush and nested-process teardown; wider than the signal grace below. */
|
|
21
18
|
const DEFAULT_DISPOSE_EOF_GRACE_MS = 6e3;
|
|
22
19
|
/** Default POSIX grace between SIGTERM and SIGKILL on dispose (the `disposeGraceMs` config). */
|
|
23
20
|
const DEFAULT_DISPOSE_GRACE_MS = 3e3;
|
|
21
|
+
const ACP_TOOL_KINDS = new Set([
|
|
22
|
+
"read",
|
|
23
|
+
"edit",
|
|
24
|
+
"delete",
|
|
25
|
+
"move",
|
|
26
|
+
"search",
|
|
27
|
+
"execute",
|
|
28
|
+
"think",
|
|
29
|
+
"fetch",
|
|
30
|
+
"switch_mode",
|
|
31
|
+
"other"
|
|
32
|
+
]);
|
|
33
|
+
/** Fixed safe failure text derived only from provider-owned structured facts. */
|
|
34
|
+
function failureDiagnostic(facts) {
|
|
35
|
+
const fields = [
|
|
36
|
+
"provider: ACP",
|
|
37
|
+
`stage: ${facts.stage}`,
|
|
38
|
+
`category: ${facts.category}`
|
|
39
|
+
];
|
|
40
|
+
if (facts.stopReason !== void 0) fields.push(`stop reason: ${facts.stopReason}`);
|
|
41
|
+
if (facts.outcome?.exitCode !== null && facts.outcome?.exitCode !== void 0) fields.push(`exit code: ${facts.outcome.exitCode}`);
|
|
42
|
+
/* v8 ignore next -- Windows does not report POSIX child signals in SubprocessOutcome. */
|
|
43
|
+
if (facts.outcome?.signal !== null && facts.outcome?.signal !== void 0) fields.push(`signal: ${facts.outcome.signal}`);
|
|
44
|
+
return `Subagent failure (${fields.join("; ")})`;
|
|
45
|
+
}
|
|
46
|
+
/** Fixed permission fact; ACP tool titles and option text never enter it. */
|
|
47
|
+
function permissionDiagnostic(permission) {
|
|
48
|
+
return `ACP unattended decision (policy: ${permission.policy}; request: ${permission.request}; decision: ${permission.decision})`;
|
|
49
|
+
}
|
|
50
|
+
/** Put the operation failure first, followed by the latest permission decision. */
|
|
51
|
+
function diagnosticText(facts, permission) {
|
|
52
|
+
const failure = failureDiagnostic(facts);
|
|
53
|
+
return permission === void 0 ? failure : `${failure}\n${permissionDiagnostic(permission)}`;
|
|
54
|
+
}
|
|
55
|
+
var AcpRunFailure = class extends Error {
|
|
56
|
+
constructor(facts, cause) {
|
|
57
|
+
super(`subagent-acp: ${failureDiagnostic(facts)}`, { cause });
|
|
58
|
+
this.name = "AcpRunFailure";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Hide a pre-spawn workspace/configuration failure behind fixed safe facts.
|
|
63
|
+
* @param cause - original Host failure retained on the Error cause chain.
|
|
64
|
+
* @returns an Error whose message contains only the fixed ACP failure line.
|
|
65
|
+
*/
|
|
66
|
+
function acpConfigurationFailure(cause) {
|
|
67
|
+
return new AcpRunFailure({
|
|
68
|
+
stage: "initialize",
|
|
69
|
+
category: "configuration"
|
|
70
|
+
}, cause);
|
|
71
|
+
}
|
|
72
|
+
/** Keep only the closed ACP tool-kind vocabulary; future values use a fixed fallback. */
|
|
73
|
+
function permissionRequestKind(kind) {
|
|
74
|
+
const candidate = kind ?? "unknown";
|
|
75
|
+
return ACP_TOOL_KINDS.has(candidate) ? candidate : "unknown";
|
|
76
|
+
}
|
|
24
77
|
/** Bounded whole-tree exit wait: polls the handle's tree liveness until it exits or `ms` elapses. */
|
|
25
78
|
async function treeExitsWithin(child, ms) {
|
|
26
79
|
const controller = new AbortController();
|
|
@@ -94,10 +147,57 @@ function toError(value) {
|
|
|
94
147
|
/* v8 ignore next */
|
|
95
148
|
return value instanceof Error ? value : new Error(String(value));
|
|
96
149
|
}
|
|
150
|
+
/** Report an original Host failure without letting the observation sink replace it. */
|
|
151
|
+
function reportFailure(spec, error) {
|
|
152
|
+
try {
|
|
153
|
+
spec.onError?.(toError(error), "error");
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
/** Classify an unpublished failure from the active protocol operation and observed process facts. */
|
|
157
|
+
function startupFailure(error, stage, child, outcome) {
|
|
158
|
+
if (child.pid <= 0) return new AcpRunFailure({
|
|
159
|
+
stage: "process",
|
|
160
|
+
category: "process-start"
|
|
161
|
+
}, error);
|
|
162
|
+
return new AcpRunFailure(
|
|
163
|
+
/* v8 ignore next -- Windows anonymous pipes cannot expose a live-child protocol close during startup. */
|
|
164
|
+
outcome === void 0 ? {
|
|
165
|
+
stage,
|
|
166
|
+
category: "transport"
|
|
167
|
+
} : {
|
|
168
|
+
stage,
|
|
169
|
+
category: "process-exit",
|
|
170
|
+
outcome
|
|
171
|
+
},
|
|
172
|
+
error
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
/** Map one remote terminal reason to the optional safe failure line it needs. */
|
|
176
|
+
function terminalFailure(reason, permission) {
|
|
177
|
+
switch (reason) {
|
|
178
|
+
case "end_turn": return;
|
|
179
|
+
case "max_turn_requests": return diagnosticText({
|
|
180
|
+
stage: "prompt",
|
|
181
|
+
category: "remote-limit",
|
|
182
|
+
stopReason: "max_turn_requests"
|
|
183
|
+
}, permission);
|
|
184
|
+
case "max_tokens":
|
|
185
|
+
case "refusal":
|
|
186
|
+
case "cancelled": return permission === void 0 ? void 0 : permissionDiagnostic(permission);
|
|
187
|
+
default: return diagnosticText({
|
|
188
|
+
stage: "prompt",
|
|
189
|
+
category: "unknown",
|
|
190
|
+
stopReason: "unknown"
|
|
191
|
+
}, permission);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
97
194
|
/**
|
|
98
195
|
* Start and publish one ACP child after initialization and session creation.
|
|
99
|
-
* Child failures resolve through the run result
|
|
100
|
-
*
|
|
196
|
+
* Child failures resolve through the run result. Startup rejects with fixed
|
|
197
|
+
* safe facts after provider-owned cleanup; successful cleanup proves process
|
|
198
|
+
* reap. Cleanup failure preserves startup plus teardown facts for an ordinary
|
|
199
|
+
* failure, or teardown alone after cancellation, without claiming quiescence.
|
|
200
|
+
* Disposal cancels, kills, and reaps the child.
|
|
101
201
|
* @param request - the start request; its signal is the cancellation channel.
|
|
102
202
|
* @param spec - the resolved spawn spec: command/args/cwd, env, permission
|
|
103
203
|
* policy, dispose graces, and the optional error sink.
|
|
@@ -105,50 +205,95 @@ function toError(value) {
|
|
|
105
205
|
*/
|
|
106
206
|
async function startAcpRun(request, spec) {
|
|
107
207
|
if (request.signal.aborted) throw new Error("subagent request was aborted before the ACP child started");
|
|
108
|
-
const id =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
208
|
+
const id = brandString(randomUUID());
|
|
209
|
+
let child;
|
|
210
|
+
try {
|
|
211
|
+
child = spec.spawn({
|
|
212
|
+
argv: [spec.command, ...spec.args],
|
|
213
|
+
cwd: spec.cwd,
|
|
214
|
+
stdio: {
|
|
215
|
+
stdin: "pipe",
|
|
216
|
+
stdout: "pipe",
|
|
217
|
+
stderr: "inherit"
|
|
218
|
+
},
|
|
219
|
+
graceMs: spec.disposeGraceMs,
|
|
220
|
+
env: spec.env
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
reportFailure(spec, error);
|
|
224
|
+
throw new AcpRunFailure({
|
|
225
|
+
stage: "process",
|
|
226
|
+
category: "process-start"
|
|
227
|
+
}, error);
|
|
228
|
+
}
|
|
120
229
|
/* v8 ignore start -- 'pipe' dispositions expose both streams by the seam contract; defensive. */
|
|
121
230
|
if (child.stdin === void 0 || child.stdout === void 0) throw new Error("subagent-acp: subprocess implementation dropped a piped protocol stream");
|
|
122
231
|
/* v8 ignore stop */
|
|
123
|
-
|
|
232
|
+
let processOutcome;
|
|
233
|
+
const processDone = child.done.then((outcome) => {
|
|
234
|
+
processOutcome = outcome;
|
|
235
|
+
return outcome;
|
|
236
|
+
});
|
|
237
|
+
const spawnFailed = processDone.then(
|
|
124
238
|
/* v8 ignore next -- the success arm's never-settling executor is intentionally empty. */
|
|
125
239
|
() => new Promise(() => {}),
|
|
126
240
|
(err) => Promise.reject(toError(err))
|
|
127
241
|
);
|
|
128
242
|
spawnFailed.catch(() => {});
|
|
243
|
+
const observeProcessOutcome = async (signal) => {
|
|
244
|
+
if (processOutcome !== void 0 || child.pid <= 0) return processOutcome;
|
|
245
|
+
const timeout = AbortSignal.timeout(Math.ceil(spec.disposeGraceMs));
|
|
246
|
+
const bound = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
247
|
+
const aborted = Promise.withResolvers();
|
|
248
|
+
/* v8 ignore next -- Windows cannot expose the live-child protocol close needed to await this abort. */
|
|
249
|
+
const onObservationAbort = () => {
|
|
250
|
+
aborted.resolve(void 0);
|
|
251
|
+
};
|
|
252
|
+
bound.addEventListener("abort", onObservationAbort, { once: true });
|
|
253
|
+
/* v8 ignore next -- closes the event-loop race between listener registration and the preceding derived-signal check. */
|
|
254
|
+
if (bound.aborted) onObservationAbort();
|
|
255
|
+
try {
|
|
256
|
+
return await Promise.race([processDone, aborted.promise]);
|
|
257
|
+
} catch {
|
|
258
|
+
/* v8 ignore next -- a published child.done cannot reject; spawn rejection is consumed before publication. */
|
|
259
|
+
return processOutcome;
|
|
260
|
+
} finally {
|
|
261
|
+
bound.removeEventListener("abort", onObservationAbort);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
129
264
|
let processDisposal;
|
|
130
265
|
const disposeProcess = () => processDisposal ??= disposeAcpChild(child, spec.disposeEofGraceMs);
|
|
131
266
|
const fold = new AssistantOutputFold();
|
|
132
267
|
const flags = { cancelled: false };
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
268
|
+
let latestPermission;
|
|
269
|
+
const agent = client({ name: "deepseek-harness-subagent-acp" }).onNotification(methods.client.session.update, ({ params }) => {
|
|
270
|
+
const update = params.update;
|
|
271
|
+
if (update.sessionUpdate === "agent_message_chunk") fold.pushText(acpContentText(update.content));
|
|
272
|
+
return Promise.resolve();
|
|
273
|
+
}).onRequest(methods.client.session.requestPermission, ({ params }) => {
|
|
274
|
+
if (spec.permission === "allow") {
|
|
275
|
+
const allow = params.options.find((o) => o.kind === "allow_once" || o.kind === "allow_always");
|
|
276
|
+
if (allow !== void 0) {
|
|
277
|
+
latestPermission = {
|
|
278
|
+
policy: "allow",
|
|
279
|
+
request: permissionRequestKind(params.toolCall.kind),
|
|
280
|
+
decision: "allowed"
|
|
281
|
+
};
|
|
282
|
+
return Promise.resolve({ outcome: {
|
|
143
283
|
outcome: "selected",
|
|
144
284
|
optionId: allow.optionId
|
|
145
285
|
} });
|
|
146
286
|
}
|
|
147
|
-
return Promise.resolve({ outcome: { outcome: "cancelled" } });
|
|
148
287
|
}
|
|
149
|
-
|
|
150
|
-
|
|
288
|
+
latestPermission = {
|
|
289
|
+
policy: spec.permission,
|
|
290
|
+
request: permissionRequestKind(params.toolCall.kind),
|
|
291
|
+
decision: "denied"
|
|
292
|
+
};
|
|
293
|
+
return Promise.resolve({ outcome: { outcome: "cancelled" } });
|
|
294
|
+
}).connect(ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout))).agent;
|
|
151
295
|
let sessionId;
|
|
296
|
+
let startupStage = "initialize";
|
|
152
297
|
let signalCancelSettled;
|
|
153
298
|
const cancelSettled = new Promise((resolve) => {
|
|
154
299
|
signalCancelSettled = resolve;
|
|
@@ -158,7 +303,7 @@ async function startAcpRun(request, spec) {
|
|
|
158
303
|
flags.cancelled = true;
|
|
159
304
|
signalCancelSettled();
|
|
160
305
|
/* v8 ignore next */
|
|
161
|
-
if (sessionId !== void 0)
|
|
306
|
+
if (sessionId !== void 0) agent.notify(methods.agent.session.cancel, { sessionId }).catch(() => {});
|
|
162
307
|
};
|
|
163
308
|
const onAbort = () => {
|
|
164
309
|
requestCancel();
|
|
@@ -168,17 +313,22 @@ async function startAcpRun(request, spec) {
|
|
|
168
313
|
try {
|
|
169
314
|
await Promise.race([
|
|
170
315
|
(async () => {
|
|
171
|
-
await
|
|
316
|
+
await agent.request(methods.agent.initialize, {
|
|
172
317
|
protocolVersion: PROTOCOL_VERSION,
|
|
173
318
|
clientCapabilities: {}
|
|
174
319
|
});
|
|
175
|
-
|
|
320
|
+
startupStage = "new-session";
|
|
321
|
+
const session = await agent.request(methods.agent.session.new, {
|
|
176
322
|
cwd: spec.cwd,
|
|
177
323
|
mcpServers: []
|
|
178
324
|
});
|
|
179
325
|
const returnedSessionId = Reflect.get(session, "sessionId");
|
|
180
|
-
if (typeof returnedSessionId !== "string") throw new
|
|
326
|
+
if (typeof returnedSessionId !== "string") throw new AcpRunFailure({
|
|
327
|
+
stage: "new-session",
|
|
328
|
+
category: "protocol"
|
|
329
|
+
}, /* @__PURE__ */ new Error("ACP child published without a session id"));
|
|
181
330
|
sessionId = returnedSessionId;
|
|
331
|
+
/* v8 ignore next -- cancelSettled wins the startup race before this post-response guard can settle it. */
|
|
182
332
|
if (flags.cancelled) throw new Error("subagent cancelled before the ACP session started");
|
|
183
333
|
})(),
|
|
184
334
|
spawnFailed,
|
|
@@ -188,59 +338,86 @@ async function startAcpRun(request, spec) {
|
|
|
188
338
|
]);
|
|
189
339
|
} catch (error) {
|
|
190
340
|
request.signal.removeEventListener("abort", onAbort);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
341
|
+
const startup = flags.cancelled ? { kind: "cancelled" } : {
|
|
342
|
+
kind: "failed",
|
|
343
|
+
failure: error instanceof AcpRunFailure ? error : startupFailure(error, startupStage, child, await observeProcessOutcome())
|
|
344
|
+
};
|
|
345
|
+
if (startup.kind === "cancelled") {} else reportFailure(spec, error instanceof AcpRunFailure ? error.cause : error);
|
|
346
|
+
try {
|
|
347
|
+
await disposeProcess();
|
|
348
|
+
} catch (cleanupError) {
|
|
349
|
+
reportFailure(spec, cleanupError);
|
|
350
|
+
const cleanupFailure = new AcpRunFailure({
|
|
351
|
+
stage: "teardown",
|
|
352
|
+
category: processOutcome === void 0 ? "unknown" : "process-exit",
|
|
353
|
+
...processOutcome === void 0 ? {} : { outcome: processOutcome }
|
|
354
|
+
}, cleanupError);
|
|
355
|
+
if (startup.kind === "cancelled") throw new AggregateError([cleanupFailure], cleanupFailure.message);
|
|
356
|
+
throw new AggregateError([startup.failure, cleanupFailure], `${startup.failure.message}; ${cleanupFailure.message}`);
|
|
357
|
+
}
|
|
358
|
+
if (startup.kind === "cancelled") throw new Error("subagent request was aborted before the ACP child started");
|
|
359
|
+
throw startup.failure;
|
|
194
360
|
}
|
|
195
361
|
/* v8 ignore next */
|
|
196
362
|
if (sessionId === void 0) throw new Error("unreachable: ACP startup fulfilled without a session id");
|
|
197
363
|
const remoteSessionId = sessionId;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const prompt = async () => {
|
|
201
|
-
const promptResult = await conn.prompt({
|
|
202
|
-
sessionId: remoteSessionId,
|
|
203
|
-
prompt: toAcpPrompt(request.prompt)
|
|
204
|
-
});
|
|
205
|
-
return {
|
|
206
|
-
output: collectOutput(),
|
|
207
|
-
stopReason: acpStopReason(promptResult.stopReason)
|
|
208
|
-
};
|
|
209
|
-
};
|
|
210
|
-
return await Promise.race([prompt(), cancelSettled.then(() => ({
|
|
211
|
-
output: collectOutput(),
|
|
212
|
-
stopReason: "aborted"
|
|
213
|
-
}))]);
|
|
214
|
-
} catch (error) {
|
|
215
|
-
/* v8 ignore next */
|
|
216
|
-
if (flags.cancelled) return {
|
|
217
|
-
output: collectOutput(),
|
|
218
|
-
stopReason: "aborted"
|
|
219
|
-
};
|
|
220
|
-
try {
|
|
221
|
-
spec.onError?.(toError(error), "error");
|
|
222
|
-
} catch {}
|
|
223
|
-
return {
|
|
224
|
-
output: collectOutput(),
|
|
225
|
-
stopReason: "error"
|
|
226
|
-
};
|
|
227
|
-
} finally {
|
|
228
|
-
request.signal.removeEventListener("abort", onAbort);
|
|
229
|
-
}
|
|
230
|
-
})();
|
|
231
|
-
let disposal;
|
|
232
|
-
return {
|
|
364
|
+
let diagnostic;
|
|
365
|
+
return subprocessRunHandle({
|
|
233
366
|
id,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
367
|
+
result: settleRunResult({
|
|
368
|
+
attempt: async () => {
|
|
369
|
+
try {
|
|
370
|
+
const promptResult = await Promise.race([agent.request(methods.agent.session.prompt, {
|
|
371
|
+
sessionId: remoteSessionId,
|
|
372
|
+
prompt: toAcpPrompt(request.prompt)
|
|
373
|
+
}), cancelSettled.then(() => {
|
|
374
|
+
throw new Error("subagent cancelled while the ACP prompt was running");
|
|
375
|
+
})]);
|
|
376
|
+
const stopReason = acpStopReason(promptResult.stopReason);
|
|
377
|
+
diagnostic = terminalFailure(promptResult.stopReason, latestPermission);
|
|
378
|
+
return {
|
|
379
|
+
output: collectOutput(),
|
|
380
|
+
...diagnostic === void 0 ? {} : { diagnostic },
|
|
381
|
+
stopReason
|
|
382
|
+
};
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (!flags.cancelled) {
|
|
385
|
+
const outcome = await observeProcessOutcome(request.signal);
|
|
386
|
+
diagnostic = diagnosticText(outcome === void 0 ? {
|
|
387
|
+
stage: "prompt",
|
|
388
|
+
category: "transport"
|
|
389
|
+
} : {
|
|
390
|
+
stage: "process",
|
|
391
|
+
category: "process-exit",
|
|
392
|
+
outcome
|
|
393
|
+
}, latestPermission);
|
|
394
|
+
}
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
collectOutput,
|
|
399
|
+
collectDiagnostic: () => diagnostic,
|
|
400
|
+
cancelled: () => flags.cancelled,
|
|
401
|
+
onError: spec.onError,
|
|
402
|
+
signal: request.signal,
|
|
403
|
+
onAbort
|
|
404
|
+
}),
|
|
405
|
+
signal: request.signal,
|
|
406
|
+
onAbort,
|
|
407
|
+
requestCancel,
|
|
408
|
+
teardown: async () => {
|
|
409
|
+
try {
|
|
410
|
+
await disposeProcess();
|
|
411
|
+
} catch (error) {
|
|
412
|
+
reportFailure(spec, error);
|
|
413
|
+
throw new AcpRunFailure({
|
|
414
|
+
stage: "teardown",
|
|
415
|
+
category: processOutcome === void 0 ? "unknown" : "process-exit",
|
|
416
|
+
...processOutcome === void 0 ? {} : { outcome: processOutcome }
|
|
417
|
+
}, error);
|
|
418
|
+
}
|
|
242
419
|
}
|
|
243
|
-
};
|
|
420
|
+
});
|
|
244
421
|
}
|
|
245
422
|
//#endregion
|
|
246
423
|
//#region lib/types/index.js
|
|
@@ -264,7 +441,7 @@ const Config = z.object({
|
|
|
264
441
|
disposeEofGraceMs: z.number().default(DEFAULT_DISPOSE_EOF_GRACE_MS),
|
|
265
442
|
disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS)
|
|
266
443
|
});
|
|
267
|
-
/** A
|
|
444
|
+
/** A process grace must fit every Node timer that observes or terminates the child. */
|
|
268
445
|
function assertPositiveFinite(name, value) {
|
|
269
446
|
if (!Number.isFinite(value) || value <= 0 || value > MAX_TIMER_DELAY_MS) throw new Error(`subagent-acp: ${name} must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
270
447
|
}
|
|
@@ -312,14 +489,15 @@ function resolveCwd(configured, request) {
|
|
|
312
489
|
}
|
|
313
490
|
/**
|
|
314
491
|
* The ACP provider. Advertises NO start-time capabilities: an out-of-process
|
|
315
|
-
* child cannot honor `outputSchema`/`maxDepth`/`toolFilter
|
|
316
|
-
* a request needing any
|
|
492
|
+
* child cannot honor `agentOptions`/`outputSchema`/`maxDepth`/`toolFilter`/
|
|
493
|
+
* `persona` (the service rejects a request needing any before `start` runs).
|
|
317
494
|
*/
|
|
318
495
|
var AcpProvider = class {
|
|
319
496
|
name;
|
|
320
497
|
ctx;
|
|
321
498
|
config;
|
|
322
499
|
capabilities = {
|
|
500
|
+
agentOptions: false,
|
|
323
501
|
outputSchema: false,
|
|
324
502
|
depthLimit: false,
|
|
325
503
|
toolFilter: false,
|
|
@@ -332,10 +510,19 @@ var AcpProvider = class {
|
|
|
332
510
|
this.config = config;
|
|
333
511
|
}
|
|
334
512
|
start(request) {
|
|
513
|
+
if (request.signal.aborted) throw new Error("subagent request was aborted before the ACP child started");
|
|
514
|
+
let cwd;
|
|
515
|
+
try {
|
|
516
|
+
cwd = resolveCwd(this.config.cwd, request);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
const failure = acpConfigurationFailure(error);
|
|
519
|
+
this.ctx.logger.warn(`subagent-acp "${this.name}": child start failed: %o`, error);
|
|
520
|
+
throw failure;
|
|
521
|
+
}
|
|
335
522
|
return startAcpRun(request, {
|
|
336
523
|
command: this.config.command,
|
|
337
524
|
args: this.config.args,
|
|
338
|
-
cwd
|
|
525
|
+
cwd,
|
|
339
526
|
permission: this.config.permission,
|
|
340
527
|
env: this.config.env,
|
|
341
528
|
disposeEofGraceMs: this.config.disposeEofGraceMs,
|
package/lib/types/index.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface Config {
|
|
|
47
47
|
* `MAX_TIMER_DELAY_MS`.
|
|
48
48
|
*/
|
|
49
49
|
disposeEofGraceMs?: number;
|
|
50
|
-
/**
|
|
50
|
+
/** Failure-observation and termination-escalation grace (ms); must not exceed `MAX_TIMER_DELAY_MS`. */
|
|
51
51
|
disposeGraceMs?: number;
|
|
52
52
|
}
|
|
53
53
|
export declare const Config: z<Config>;
|
package/lib/types/run.d.ts
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
* Fresh-process ACP subagent client. Drives one child session and owns cancellation and
|
|
3
3
|
* quiescent disposal.
|
|
4
4
|
*
|
|
5
|
-
* TODO(acp-subagent-replay): add snapshot-tier coverage with a separate replay fixture and
|
|
6
|
-
* sessions root inside each child process. Current keyless coverage uses a scripted ACP child;
|
|
7
|
-
* with-key coverage drives the real ACP example.
|
|
8
5
|
* @module @deepseek-ai/dsh-subagent-acp/run
|
|
9
6
|
*/
|
|
10
7
|
import { type ContentBlock as AcpContentBlock, type StopReason } from '@agentclientprotocol/sdk';
|
|
@@ -45,9 +42,11 @@ export interface AcpRunSpec {
|
|
|
45
42
|
*/
|
|
46
43
|
disposeEofGraceMs: number;
|
|
47
44
|
/**
|
|
48
|
-
*
|
|
49
|
-
* waits this long
|
|
50
|
-
*
|
|
45
|
+
* Process-observation and termination-escalation grace (ms). Failure
|
|
46
|
+
* classification waits at most this long for structured exit facts; POSIX
|
|
47
|
+
* dispose also waits this long after `SIGTERM` before `SIGKILL`, while
|
|
48
|
+
* Windows force-terminates directly. The plugin fills it from
|
|
49
|
+
* `disposeGraceMs`.
|
|
51
50
|
*/
|
|
52
51
|
disposeGraceMs: number;
|
|
53
52
|
/**
|
|
@@ -57,12 +56,9 @@ export interface AcpRunSpec {
|
|
|
57
56
|
*/
|
|
58
57
|
spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle;
|
|
59
58
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* rather than silently lost; the provider wires it to `ctx.logger.warn`.
|
|
64
|
-
* A throw from the sink itself is contained — it cannot reject `result`.
|
|
65
|
-
* Optional — omitted in a unit test that asserts the stop reason directly.
|
|
59
|
+
* Host sink for startup, published-run, or teardown failures. Model-visible
|
|
60
|
+
* text uses fixed safe facts, while this callback retains the original Error
|
|
61
|
+
* when one exists. A throw from the sink itself is contained.
|
|
66
62
|
*/
|
|
67
63
|
onError?: (error: Error, stopReason: SubagentStopReason) => void;
|
|
68
64
|
}
|
|
@@ -70,6 +66,12 @@ export interface AcpRunSpec {
|
|
|
70
66
|
export declare const DEFAULT_DISPOSE_EOF_GRACE_MS = 6000;
|
|
71
67
|
/** Default POSIX grace between SIGTERM and SIGKILL on dispose (the `disposeGraceMs` config). */
|
|
72
68
|
export declare const DEFAULT_DISPOSE_GRACE_MS = 3000;
|
|
69
|
+
/**
|
|
70
|
+
* Hide a pre-spawn workspace/configuration failure behind fixed safe facts.
|
|
71
|
+
* @param cause - original Host failure retained on the Error cause chain.
|
|
72
|
+
* @returns an Error whose message contains only the fixed ACP failure line.
|
|
73
|
+
*/
|
|
74
|
+
export declare function acpConfigurationFailure(cause: unknown): Error;
|
|
73
75
|
/**
|
|
74
76
|
* Cooperative teardown ladder for an out-of-process agent, over the seam's
|
|
75
77
|
* public verbs; resolves only at whole-tree quiescence: stdin EOF (the child's
|
|
@@ -101,8 +103,11 @@ export declare function acpContentText(content: AcpContentBlock): string;
|
|
|
101
103
|
export declare function toAcpPrompt(prompt: ContentBlock[]): AcpContentBlock[];
|
|
102
104
|
/**
|
|
103
105
|
* Start and publish one ACP child after initialization and session creation.
|
|
104
|
-
* Child failures resolve through the run result
|
|
105
|
-
*
|
|
106
|
+
* Child failures resolve through the run result. Startup rejects with fixed
|
|
107
|
+
* safe facts after provider-owned cleanup; successful cleanup proves process
|
|
108
|
+
* reap. Cleanup failure preserves startup plus teardown facts for an ordinary
|
|
109
|
+
* failure, or teardown alone after cancellation, without claiming quiescence.
|
|
110
|
+
* Disposal cancels, kills, and reaps the child.
|
|
106
111
|
* @param request - the start request; its signal is the cancellation channel.
|
|
107
112
|
* @param spec - the resolved spawn spec: command/args/cwd, env, permission
|
|
108
113
|
* policy, dispose graces, and the optional error sink.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-subagent-acp",
|
|
3
3
|
"description": "Out-of-process ACP subagent backend: drives a child agent in a spawned subprocess over the Agent Client Protocol",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -32,30 +32,37 @@
|
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/
|
|
36
|
-
"@deepseek-ai/dsh-
|
|
37
|
-
"@deepseek-ai/dsh-
|
|
38
|
-
"@deepseek-ai/dsh-
|
|
39
|
-
"@deepseek-ai/dsh-
|
|
40
|
-
"@deepseek-ai/dsh-
|
|
41
|
-
"@deepseek-ai/dsh-
|
|
42
|
-
"@deepseek-ai/
|
|
35
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
36
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
37
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
38
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
|
|
39
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
40
|
+
"@deepseek-ai/dsh-subagent": "^0.1.2-alpha.2",
|
|
41
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-timeout": "^0.1.2-alpha.2"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@agentclientprotocol/sdk": "
|
|
46
|
-
"@deepseek-ai/
|
|
45
|
+
"@agentclientprotocol/sdk": "1.4.0",
|
|
46
|
+
"@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
|
|
47
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
|
-
"@deepseek-ai/cordis
|
|
50
|
-
"@deepseek-ai/
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
51
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.3",
|
|
52
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-agent-spine-demo": "^0.1.2-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-app-boot": "^0.1.2-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-loader-smoke": "^0.1.2-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
59
|
+
"@deepseek-ai/dsh-session-checkpoint-policy": "^0.1.2-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.2-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-subagent": "^0.1.2-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.1.2-alpha.2",
|
|
65
|
+
"@deepseek-ai/dsh-timeout": "^0.1.2-alpha.2",
|
|
66
|
+
"@deepseek-ai/dsh-tool-subagent": "^0.1.2-alpha.2"
|
|
60
67
|
}
|
|
61
68
|
}
|