@deepseek-ai/dsh-subagent-acp 0.1.2-rc.1 → 0.1.5-alpha.1

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 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: 2d929738de2f761cbc597fae6cb52f7d44d83536
6
- README.zh.md: d67e174a44497148d08ef54d13f17df934db2018
5
+ README.md: 87fb7f3101d2d329300213bed2acb4a479c83b72
6
+ README.zh.md: 399a262b9b6a4bba650552388b210d87f69f5687
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
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.
12
+ Use this package to delegate a task to an ACP-compatible agent running in a fresh subprocess with its own runtime, session, model, and tools. Each run shares only the selected working directory, sends the task over ACP, and returns the child's final answer or a safe error; intermediate messages and tool traffic stay outside the parent conversation. Permission prompts are answered by configured policy without human interaction. Choose it when delegation needs process isolation or a non-Harness ACP agent, and choose an in-process backend when the child must share parent capabilities.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -67,7 +67,7 @@ A successful run returns the child's final streamed assistant text as the result
67
67
 
68
68
  ### Failure and recovery
69
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.
70
+ A spawn, initialization, or new-session failure rejects before publication, ordinarily after the managed range is proven quiescent. If cleanup also fails, the rejection preserves ordered safe startup and teardown facts without claiming whole-range 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
71
 
72
72
  ### Safe diagnostics
73
73
 
@@ -91,7 +91,7 @@ This section explains how the backend drives a child over ACP and where the obse
91
91
 
92
92
  ### Start and ownership flow
93
93
 
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.
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-range exit. Cleanup failures remain observable as ordered safe facts and never claim quiescence.
95
95
 
96
96
  ### Stop-reason mapping
97
97
 
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- `dsh-subagent-acp` 在全新的子进程中运行每个被委派的子 agent,并作为 Agent Client Protocol 客户端驱动它:子 agent(智能体)拥有自己的运行时、会话、模型配置和工具,可以是任何兼容 ACP 的 agent,而不只是 Harness。它是进程内 spawn 与 fork 后端的进程外替代方案,只与子 agent 共享父会话的工作目录。每次运行都会 spawn 全新进程、初始化 ACP 会话、发送任务并收集流式最终答案;权限提示由配置自动应答,因此无需人工参与。父级只收到子 agent 的最终答案或安全错误——中间消息与工具流量不会跨越边界。当子 agent 必须与父 harness 完全隔离且能说 ACP 时,选择它。
12
+ 使用本包可将任务委派给运行在全新子进程中的 ACP 兼容 agent;子 agent 拥有独立的运行时、会话、模型和工具。每次运行只共享选定的工作目录,通过 ACP 发送任务,并返回子 agent 的最终答案或安全错误;中间消息和工具流量不会进入父级对话。权限提示由配置的策略自动应答,无需人工介入。当委派需要进程隔离或需要使用非 Harness ACP agent 时选择本包;当子 agent 必须共享父级能力时,选择进程内后端。
13
13
 
14
14
  ## 目录
15
15
 
@@ -67,7 +67,7 @@ DeepSeek Harness 子进程使用产品启动器和一个显式的绝对路径 `D
67
67
 
68
68
  ### 失败与恢复
69
69
 
70
- spawn、初始化或新建会话失败会在发布前拒绝,通常先等待子进程被回收。如果清理也失败,拒绝会保留有序、安全的启动与拆卸事实,但不会声称整棵进程树已经停稳。非取消错误只暴露固定的提供方、阶段与类别事实;原始失败保留在内部 cause 链与 Host 诊断中。发布后,提示词、传输或进程提前退出会以携带安全诊断的 `error` 结算;本地取消则以不带失败详情的 `aborted` 结算。
70
+ spawn、初始化或新建会话失败会在发布前拒绝,通常先证明 managed range 已经完全停稳。如果清理也失败,拒绝会保留有序、安全的启动与拆卸事实,但不会声称整个 range 已经停稳。非取消错误只暴露固定的提供方、阶段与类别事实;原始失败保留在内部 cause 链与 Host 诊断中。发布后,提示词、传输或进程提前退出会以携带安全诊断的 `error` 结算;本地取消则以不带失败详情的 `aborted` 结算。
71
71
 
72
72
  ### 安全诊断
73
73
 
@@ -91,7 +91,7 @@ spawn、初始化或新建会话失败会在发布前拒绝,通常先等待子
91
91
 
92
92
  ### 启动与所有权流程
93
93
 
94
- 一次启动先解析子 agent 的工作目录(配置的 `cwd` 覆盖值,否则取父会话 cwd),经子进程 seam spawn 命令,完成 ACP `initialize` 与 `newSession` 握手,然后才发布运行。兑现意味着远程会话已就绪、所有权已转移给调用方。dispose(资源释放)是幂等的:先关闭 stdin 并按配置的宽限等待协作式完全停稳,再经 SIGTERM 升级到 SIGKILL,并等待整棵进程树退出。清理失败会作为有序的安全事实保持可观察,且绝不声称已经完全停稳。
94
+ 一次启动先解析子 agent 的工作目录(配置的 `cwd` 覆盖值,否则取父会话 cwd),经子进程 seam spawn 命令,完成 ACP `initialize` 与 `newSession` 握手,然后才发布运行。兑现意味着远程会话已就绪、所有权已转移给调用方。dispose(资源释放)是幂等的:先关闭 stdin 并按配置的宽限等待协作式完全停稳,再经 SIGTERM 升级到 SIGKILL,并等待整个 managed range 退出。清理失败会作为有序的安全事实保持可观察,且绝不声称已经完全停稳。
95
95
 
96
96
  ### 停止原因映射
97
97
 
package/lib/index.js CHANGED
@@ -74,8 +74,8 @@ function permissionRequestKind(kind) {
74
74
  const candidate = kind ?? "unknown";
75
75
  return ACP_TOOL_KINDS.has(candidate) ? candidate : "unknown";
76
76
  }
77
- /** Bounded whole-tree exit wait: polls the handle's tree liveness until it exits or `ms` elapses. */
78
- async function treeExitsWithin(child, ms) {
77
+ /** Bounded managed-range exit wait: observes the handle's range until it is empty or `ms` elapses. */
78
+ async function rangeExitsWithin(child, ms) {
79
79
  const controller = new AbortController();
80
80
  const timer = setTimeout(() => {
81
81
  controller.abort();
@@ -88,22 +88,31 @@ async function treeExitsWithin(child, ms) {
88
88
  }
89
89
  /**
90
90
  * Cooperative teardown ladder for an out-of-process agent, over the seam's
91
- * public verbs; resolves only at whole-tree quiescence: stdin EOF (the child's
91
+ * public verbs; resolves only at whole-range quiescence: stdin EOF (the child's
92
92
  * window to flush persistence and reap its own descendants), then the
93
93
  * terminate() escalation (SIGTERM → spec grace → SIGKILL) and its
94
- * whole-tree exit proof.
94
+ * whole-range exit proof.
95
95
  * @param child - the spawned ACP child's handle.
96
96
  * @param eofGraceMs - tier-1 window after stdin EOF.
97
97
  */
98
98
  async function disposeAcpChild(child, eofGraceMs) {
99
- if (child.pid <= 0) {
100
- await child.done.catch(() => {});
101
- return;
102
- }
99
+ const failures = [];
103
100
  child.stdin?.end();
104
- if (await treeExitsWithin(child, eofGraceMs)) return;
101
+ let exited = false;
102
+ try {
103
+ exited = await rangeExitsWithin(child, eofGraceMs);
104
+ } catch (error) {
105
+ failures.push(toError(error));
106
+ }
107
+ if (exited) return;
105
108
  child.terminate();
106
- await child.waitForExit();
109
+ try {
110
+ await child.waitForExit();
111
+ } catch (error) {
112
+ failures.push(toError(error));
113
+ }
114
+ if (failures.length === 1) throw failures[0];
115
+ if (failures.length > 1) throw new AggregateError(failures, "ACP subprocess teardown failed");
107
116
  }
108
117
  /**
109
118
  * Map an ACP {@link StopReason} to a harness {@link SubagentStopReason}.
@@ -154,11 +163,7 @@ function reportFailure(spec, error) {
154
163
  } catch {}
155
164
  }
156
165
  /** 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);
166
+ function startupFailure(error, stage, outcome) {
162
167
  return new AcpRunFailure(
163
168
  /* v8 ignore next -- Windows anonymous pipes cannot expose a live-child protocol close during startup. */
164
169
  outcome === void 0 ? {
@@ -194,10 +199,10 @@ function terminalFailure(reason, permission) {
194
199
  /**
195
200
  * Start and publish one ACP child after initialization and session creation.
196
201
  * 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
202
+ * safe facts after provider-owned cleanup; successful cleanup proves managed
203
+ * range quiescence. Cleanup failure preserves startup plus teardown facts for an ordinary
199
204
  * failure, or teardown alone after cancellation, without claiming quiescence.
200
- * Disposal cancels, kills, and reaps the child.
205
+ * Disposal cancels, terminates, and settles the child's managed range.
201
206
  * @param request - the start request; its signal is the cancellation channel.
202
207
  * @param spec - the resolved spawn spec: command/args/cwd, env, permission
203
208
  * policy, dispose graces, and the optional error sink.
@@ -230,18 +235,22 @@ async function startAcpRun(request, spec) {
230
235
  if (child.stdin === void 0 || child.stdout === void 0) throw new Error("subagent-acp: subprocess implementation dropped a piped protocol stream");
231
236
  /* v8 ignore stop */
232
237
  let processOutcome;
238
+ let processFailure;
233
239
  const processDone = child.done.then((outcome) => {
234
240
  processOutcome = outcome;
235
241
  return outcome;
242
+ }, (error) => {
243
+ processFailure = toError(error);
244
+ throw processFailure;
236
245
  });
237
- const spawnFailed = processDone.then(
246
+ const processRejected = processDone.then(
238
247
  /* v8 ignore next -- the success arm's never-settling executor is intentionally empty. */
239
248
  () => new Promise(() => {}),
240
249
  (err) => Promise.reject(toError(err))
241
250
  );
242
- spawnFailed.catch(() => {});
251
+ processRejected.catch(() => {});
243
252
  const observeProcessOutcome = async (signal) => {
244
- if (processOutcome !== void 0 || child.pid <= 0) return processOutcome;
253
+ if (processOutcome !== void 0) return processOutcome;
245
254
  const timeout = AbortSignal.timeout(Math.ceil(spec.disposeGraceMs));
246
255
  const bound = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
247
256
  const aborted = Promise.withResolvers();
@@ -255,7 +264,6 @@ async function startAcpRun(request, spec) {
255
264
  try {
256
265
  return await Promise.race([processDone, aborted.promise]);
257
266
  } catch {
258
- /* v8 ignore next -- a published child.done cannot reject; spawn rejection is consumed before publication. */
259
267
  return processOutcome;
260
268
  } finally {
261
269
  bound.removeEventListener("abort", onObservationAbort);
@@ -331,18 +339,20 @@ async function startAcpRun(request, spec) {
331
339
  /* v8 ignore next -- cancelSettled wins the startup race before this post-response guard can settle it. */
332
340
  if (flags.cancelled) throw new Error("subagent cancelled before the ACP session started");
333
341
  })(),
334
- spawnFailed,
342
+ processRejected,
335
343
  cancelSettled.then(() => {
336
344
  throw new Error("subagent cancelled before the ACP session started");
337
345
  })
338
346
  ]);
339
347
  } catch (error) {
340
348
  request.signal.removeEventListener("abort", onAbort);
341
- const startup = flags.cancelled ? { kind: "cancelled" } : {
349
+ const cancelledBeforeCleanup = flags.cancelled;
350
+ const observedOutcome = !cancelledBeforeCleanup && !(error instanceof AcpRunFailure) ? await observeProcessOutcome() : void 0;
351
+ const startup = cancelledBeforeCleanup ? { kind: "cancelled" } : {
342
352
  kind: "failed",
343
- failure: error instanceof AcpRunFailure ? error : startupFailure(error, startupStage, child, await observeProcessOutcome())
353
+ failure: error instanceof AcpRunFailure ? error : startupFailure(error, startupStage, observedOutcome)
344
354
  };
345
- if (startup.kind === "cancelled") {} else reportFailure(spec, error instanceof AcpRunFailure ? error.cause : error);
355
+ if (startup.kind === "cancelled") {} else reportFailure(spec, error instanceof AcpRunFailure ? error.cause : processFailure ?? error);
346
356
  try {
347
357
  await disposeProcess();
348
358
  } catch (cleanupError) {
@@ -392,7 +402,7 @@ async function startAcpRun(request, spec) {
392
402
  outcome
393
403
  }, latestPermission);
394
404
  }
395
- throw error;
405
+ throw processFailure ?? error;
396
406
  }
397
407
  },
398
408
  collectOutput,
@@ -51,7 +51,7 @@ export interface AcpRunSpec {
51
51
  disposeGraceMs: number;
52
52
  /**
53
53
  * Spawn function from the subprocess seam (`ctx.subprocess.spawn`), so the
54
- * child rides the shared scrub, tree-scoped teardown, and service-owned
54
+ * child rides the shared scrub, managed-range teardown, and service-owned
55
55
  * lifetime instead of a package-local child_process path.
56
56
  */
57
57
  spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle;
@@ -74,10 +74,10 @@ export declare const DEFAULT_DISPOSE_GRACE_MS = 3000;
74
74
  export declare function acpConfigurationFailure(cause: unknown): Error;
75
75
  /**
76
76
  * Cooperative teardown ladder for an out-of-process agent, over the seam's
77
- * public verbs; resolves only at whole-tree quiescence: stdin EOF (the child's
77
+ * public verbs; resolves only at whole-range quiescence: stdin EOF (the child's
78
78
  * window to flush persistence and reap its own descendants), then the
79
79
  * terminate() escalation (SIGTERM → spec grace → SIGKILL) and its
80
- * whole-tree exit proof.
80
+ * whole-range exit proof.
81
81
  * @param child - the spawned ACP child's handle.
82
82
  * @param eofGraceMs - tier-1 window after stdin EOF.
83
83
  */
@@ -104,10 +104,10 @@ export declare function toAcpPrompt(prompt: ContentBlock[]): AcpContentBlock[];
104
104
  /**
105
105
  * Start and publish one ACP child after initialization and session creation.
106
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
107
+ * safe facts after provider-owned cleanup; successful cleanup proves managed
108
+ * range quiescence. Cleanup failure preserves startup plus teardown facts for an ordinary
109
109
  * failure, or teardown alone after cancellation, without claiming quiescence.
110
- * Disposal cancels, kills, and reaps the child.
110
+ * Disposal cancels, terminates, and settles the child's managed range.
111
111
  * @param request - the start request; its signal is the cancellation channel.
112
112
  * @param spec - the resolved spawn spec: command/args/cwd, env, permission
113
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.2-rc.1",
4
+ "version": "0.1.5-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -28,33 +28,33 @@
28
28
  "license": "MIT",
29
29
  "peerDependencies": {
30
30
  "@deepseek-ai/cordis": "^4.0.2",
31
- "@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
32
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
33
- "@deepseek-ai/dsh-session": "^0.1.2-rc.1",
34
- "@deepseek-ai/dsh-subagent": "^0.1.2-rc.1",
35
- "@deepseek-ai/dsh-subprocess": "^0.1.2-rc.1",
36
- "@deepseek-ai/dsh-timeout": "^0.1.2-rc.1"
31
+ "@deepseek-ai/dsh-agent": "^0.1.5-alpha.1",
32
+ "@deepseek-ai/dsh-llm": "^0.1.5-alpha.1",
33
+ "@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
34
+ "@deepseek-ai/dsh-subagent": "^0.1.5-alpha.1",
35
+ "@deepseek-ai/dsh-subprocess": "^0.1.5-alpha.1",
36
+ "@deepseek-ai/dsh-timeout": "^0.1.5-alpha.1"
37
37
  },
38
38
  "dependencies": {
39
39
  "@agentclientprotocol/sdk": "1.4.0",
40
- "@deepseek-ai/schemastery": "^3.18.2",
41
- "@deepseek-ai/dsh-brand": "^0.1.2-rc.1"
40
+ "@deepseek-ai/dsh-brand": "^0.1.5-alpha.1",
41
+ "@deepseek-ai/schemastery": "^3.18.2"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/cordis": "^4.0.2",
45
45
  "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
46
- "@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
47
- "@deepseek-ai/dsh-app-boot": "^0.1.2-rc.1",
48
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
49
- "@deepseek-ai/dsh-loader-smoke": "^0.1.2-rc.1",
50
- "@deepseek-ai/dsh-session": "^0.1.2-rc.1",
51
- "@deepseek-ai/dsh-session-checkpoint-policy": "^0.1.2-rc.1",
52
- "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.2-rc.1",
53
- "@deepseek-ai/dsh-session-projection": "^0.1.2-rc.1",
54
- "@deepseek-ai/dsh-subprocess": "^0.1.2-rc.1",
55
- "@deepseek-ai/dsh-timeout": "^0.1.2-rc.1",
56
- "@deepseek-ai/dsh-subagent": "^0.1.2-rc.1",
57
- "@deepseek-ai/dsh-subprocess-local": "^0.1.2-rc.1",
58
- "@deepseek-ai/dsh-tool-subagent": "^0.1.2-rc.1"
46
+ "@deepseek-ai/dsh-agent": "^0.1.5-alpha.1",
47
+ "@deepseek-ai/dsh-app-boot": "^0.1.5-alpha.1",
48
+ "@deepseek-ai/dsh-llm": "^0.1.5-alpha.1",
49
+ "@deepseek-ai/dsh-loader-smoke": "^0.1.5-alpha.1",
50
+ "@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
51
+ "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.5-alpha.1",
52
+ "@deepseek-ai/dsh-session-checkpoint-policy": "^0.1.5-alpha.1",
53
+ "@deepseek-ai/dsh-session-projection": "^0.1.5-alpha.1",
54
+ "@deepseek-ai/dsh-subprocess": "^0.1.5-alpha.1",
55
+ "@deepseek-ai/dsh-subprocess-local": "^0.1.5-alpha.1",
56
+ "@deepseek-ai/dsh-subagent": "^0.1.5-alpha.1",
57
+ "@deepseek-ai/dsh-timeout": "^0.1.5-alpha.1",
58
+ "@deepseek-ai/dsh-tool-subagent": "^0.1.5-alpha.1"
59
59
  }
60
60
  }