@jingyi0605/codingns4dsh 0.1.2 → 0.1.7-rc.3

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.
Files changed (171) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +14 -5
  3. package/data/build/dist/client/account-bar.js +57 -3
  4. package/data/build/dist/client/bundle.js +36246 -268
  5. package/data/build/dist/client/cli-slots.js +60 -18
  6. package/data/build/dist/client/dsh-h5-bootstrap.d.ts +2 -0
  7. package/data/build/dist/client/dsh-h5-bootstrap.js +79 -13
  8. package/data/build/dist/client/features/file-management-panel.d.ts +5 -0
  9. package/data/build/dist/client/features/file-management-panel.js +48 -0
  10. package/data/build/dist/client/features/file-management.d.ts +4 -0
  11. package/data/build/dist/client/features/file-management.js +50 -0
  12. package/data/build/dist/client/features/index.d.ts +10 -1
  13. package/data/build/dist/client/features/index.js +12 -1
  14. package/data/build/dist/client/features/login-protection-panel.js +1 -1
  15. package/data/build/dist/client/features/login-protection-session.d.ts +2 -0
  16. package/data/build/dist/client/features/login-protection-session.js +18 -0
  17. package/data/build/dist/client/features/peer-host.d.ts +4 -0
  18. package/data/build/dist/client/features/peer-host.js +89 -0
  19. package/data/build/dist/client/features/reverse-proxy-panel.js +23 -7
  20. package/data/build/dist/client/features/reverse-proxy.d.ts +1 -2
  21. package/data/build/dist/client/features/reverse-proxy.js +75 -5
  22. package/data/build/dist/client/features/types.d.ts +9 -0
  23. package/data/build/dist/client/features/workspace-session-enhancement-panel.js +10 -0
  24. package/data/build/dist/client/features/workspace-session-enhancement.js +18 -1
  25. package/data/build/dist/client/file-management-dom.d.ts +12 -0
  26. package/data/build/dist/client/file-management-dom.js +527 -0
  27. package/data/build/dist/client/git-management.js +24 -5
  28. package/data/build/dist/client/host-navigation.d.ts +29 -0
  29. package/data/build/dist/client/host-navigation.js +29 -0
  30. package/data/build/dist/client/host-router.d.ts +3 -0
  31. package/data/build/dist/client/host-router.js +2 -0
  32. package/data/build/dist/client/index.d.ts +6 -1
  33. package/data/build/dist/client/index.js +30 -10
  34. package/data/build/dist/client/locale.js +36 -7
  35. package/data/build/dist/client/peer-host-connection-button.d.ts +13 -0
  36. package/data/build/dist/client/peer-host-connection-button.js +69 -0
  37. package/data/build/dist/client/peer-host-management-api.d.ts +40 -0
  38. package/data/build/dist/client/peer-host-management-api.js +33 -0
  39. package/data/build/dist/client/peer-host-management-panel.d.ts +13 -0
  40. package/data/build/dist/client/peer-host-management-panel.js +181 -0
  41. package/data/build/dist/client/peer-host-native-session-ui.d.ts +60 -0
  42. package/data/build/dist/client/peer-host-native-session-ui.js +372 -0
  43. package/data/build/dist/client/peer-host-scoped-client.d.ts +67 -0
  44. package/data/build/dist/client/peer-host-scoped-client.js +316 -0
  45. package/data/build/dist/client/peer-host-session-controller.d.ts +30 -0
  46. package/data/build/dist/client/peer-host-session-controller.js +95 -0
  47. package/data/build/dist/client/quick-phrase-slot.js +14 -0
  48. package/data/build/dist/client/remote-web-context.js +26 -4
  49. package/data/build/dist/client/session-changed-files-view.d.ts +15 -0
  50. package/data/build/dist/client/session-changed-files-view.js +337 -0
  51. package/data/build/dist/client/settings-bridge.d.ts +14 -4
  52. package/data/build/dist/client/settings-bridge.js +59 -12
  53. package/data/build/dist/client/settings-section.js +17 -4
  54. package/data/build/dist/client/subscription-slot.js +21 -8
  55. package/data/build/dist/client/workspace-session-rightbar-dom.d.ts +28 -0
  56. package/data/build/dist/client/workspace-session-rightbar-dom.js +291 -0
  57. package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +25 -33
  58. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +7 -1
  59. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +34 -11
  60. package/data/build/dist/dsh-capabilities/matrix.js +9 -0
  61. package/data/build/dist/dsh-capabilities/routes.js +35 -0
  62. package/data/build/dist/dsh-capabilities/settings-store.d.ts +12 -0
  63. package/data/build/dist/dsh-capabilities/settings-store.js +34 -0
  64. package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
  65. package/data/build/dist/features/host-router.d.ts +26 -0
  66. package/data/build/dist/features/host-router.js +110 -0
  67. package/data/build/dist/features/registry.js +3 -0
  68. package/data/build/dist/host/cli-adapters/codex-driver.d.ts +13 -1
  69. package/data/build/dist/host/cli-adapters/codex-driver.js +528 -72
  70. package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +21 -2
  71. package/data/build/dist/host/cli-adapters/command-code-driver.js +408 -105
  72. package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
  73. package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +2 -0
  74. package/data/build/dist/host/cli-adapters/dsh-message-projector.js +47 -28
  75. package/data/build/dist/host/cli-adapters/dsh-tool-history.js +37 -0
  76. package/data/build/dist/host/cli-adapters/feature.js +94 -10
  77. package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +6 -0
  78. package/data/build/dist/host/cli-adapters/json-rpc-process.js +14 -3
  79. package/data/build/dist/host/cli-adapters/kimi-driver.js +3 -3
  80. package/data/build/dist/host/cli-adapters/model-catalog.d.ts +6 -0
  81. package/data/build/dist/host/cli-adapters/model-catalog.js +8 -0
  82. package/data/build/dist/host/cli-adapters/opencode-driver.js +14 -2
  83. package/data/build/dist/host/cli-adapters/pi-driver.js +2 -2
  84. package/data/build/dist/host/cli-adapters/process-utils.d.ts +8 -0
  85. package/data/build/dist/host/cli-adapters/process-utils.js +48 -0
  86. package/data/build/dist/host/cli-adapters/registry.d.ts +6 -1
  87. package/data/build/dist/host/cli-adapters/registry.js +41 -7
  88. package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +23 -4
  89. package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
  90. package/data/build/dist/host/cli-adapters/standard-stream-driver.js +36 -15
  91. package/data/build/dist/host/dsh-device-runtime.d.ts +2 -0
  92. package/data/build/dist/host/dsh-device-runtime.js +55 -29
  93. package/data/build/dist/host/dsh-runtime-version.d.ts +3 -2
  94. package/data/build/dist/host/dsh-runtime-version.js +71 -2
  95. package/data/build/dist/host/features/auth.js +10 -2
  96. package/data/build/dist/host/features/file-management.d.ts +5 -0
  97. package/data/build/dist/host/features/file-management.js +267 -0
  98. package/data/build/dist/host/features/git-management.js +24 -2
  99. package/data/build/dist/host/features/index.d.ts +2 -0
  100. package/data/build/dist/host/features/index.js +6 -0
  101. package/data/build/dist/host/features/peer-host.d.ts +22 -0
  102. package/data/build/dist/host/features/peer-host.js +322 -0
  103. package/data/build/dist/host/features/types.d.ts +4 -0
  104. package/data/build/dist/host/host-router.d.ts +3 -0
  105. package/data/build/dist/host/host-router.js +2 -0
  106. package/data/build/dist/host/index-injection.d.ts +17 -0
  107. package/data/build/dist/host/index-injection.js +24 -0
  108. package/data/build/dist/host/index.d.ts +4 -1
  109. package/data/build/dist/host/index.js +41 -7
  110. package/data/build/dist/host/lan-access-dsh.d.ts +34 -1
  111. package/data/build/dist/host/lan-access-dsh.js +145 -26
  112. package/data/build/dist/host/modules/peer-host/host-api-proxy-service.d.ts +52 -0
  113. package/data/build/dist/host/modules/peer-host/host-api-proxy-service.js +166 -0
  114. package/data/build/dist/host/modules/peer-host/host-ws-connector.d.ts +31 -0
  115. package/data/build/dist/host/modules/peer-host/host-ws-connector.js +108 -0
  116. package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.d.ts +26 -0
  117. package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.js +159 -0
  118. package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.d.ts +50 -0
  119. package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.js +97 -0
  120. package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.d.ts +19 -0
  121. package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.js +54 -0
  122. package/data/build/dist/host/modules/peer-host/peer-host-handshake.d.ts +44 -0
  123. package/data/build/dist/host/modules/peer-host/peer-host-handshake.js +133 -0
  124. package/data/build/dist/host/modules/peer-host/peer-host-relay.d.ts +65 -0
  125. package/data/build/dist/host/modules/peer-host/peer-host-relay.js +196 -0
  126. package/data/build/dist/host/modules/peer-host/peer-host-session.d.ts +42 -0
  127. package/data/build/dist/host/modules/peer-host/peer-host-session.js +152 -0
  128. package/data/build/dist/host/modules/peer-host/peer-host-store.d.ts +92 -0
  129. package/data/build/dist/host/modules/peer-host/peer-host-store.js +323 -0
  130. package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.d.ts +34 -0
  131. package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.js +156 -0
  132. package/data/build/dist/host/native-session-bridge.d.ts +18 -2
  133. package/data/build/dist/host/native-session-bridge.js +236 -8
  134. package/data/build/dist/host/remote-web-runtime.js +26 -1
  135. package/data/build/dist/host/rpc.js +8 -3
  136. package/data/build/dist/host/session-changes.d.ts +5 -0
  137. package/data/build/dist/host/session-changes.js +163 -0
  138. package/data/build/dist/host/settings.js +6 -0
  139. package/data/build/dist/index.d.ts +1 -1
  140. package/data/build/dist/index.js +1 -1
  141. package/data/build/dist/shared/contracts/cli-adapter.d.ts +23 -2
  142. package/data/build/dist/shared/contracts/config.d.ts +15 -0
  143. package/data/build/dist/shared/contracts/config.js +10 -0
  144. package/data/build/dist/shared/contracts/dsh-device.d.ts +2 -0
  145. package/data/build/dist/shared/contracts/errors.d.ts +14 -0
  146. package/data/build/dist/shared/contracts/errors.js +15 -0
  147. package/data/build/dist/shared/contracts/feature.d.ts +2 -0
  148. package/data/build/dist/shared/contracts/file-management.d.ts +6 -0
  149. package/data/build/dist/shared/contracts/file-management.js +3 -0
  150. package/data/build/dist/shared/contracts/peer-host.d.ts +116 -15
  151. package/data/build/dist/shared/contracts/peer-host.js +19 -0
  152. package/data/build/dist/shared/contracts/version.d.ts +1 -1
  153. package/data/build/dist/shared/contracts/version.js +1 -1
  154. package/data/build/dist/shared/index.d.ts +4 -3
  155. package/data/build/dist/shared/index.js +2 -2
  156. package/data/build/dist/transport/debug.d.ts +5 -0
  157. package/data/build/dist/transport/debug.js +23 -1
  158. package/data/build/dist/transport/dsh-transport.d.ts +2 -0
  159. package/data/build/dist/transport/dsh-transport.js +11 -0
  160. package/data/build/dist/transport/webrtc-client.d.ts +2 -0
  161. package/data/build/dist/transport/webrtc-client.js +2 -1
  162. package/data/build/dist/transport/webrtc-host.d.ts +2 -0
  163. package/data/build/dist/transport/webrtc-host.js +6 -3
  164. package/data/build/dist/typert.host.d.ts +1 -1
  165. package/data/build/dist/typert.host.js +2 -1
  166. package/dsh.bundle.patch +10 -0
  167. package/package.json +19 -11
  168. package/profile/README.md +2 -2
  169. package/profile/package.json +2 -2
  170. package/profile/version.json +1 -1
  171. package/version.json +1 -1
@@ -5,7 +5,7 @@ import { join } from 'node:path';
5
5
  import readline from 'node:readline';
6
6
  import { firstToolText, serializeToolValue } from './tool-observation.js';
7
7
  import { usageChunk } from './rpc-driver-utils.js';
8
- import { terminateChildProcess } from './process-utils.js';
8
+ import { commandEnvironment, resolveCommandPath, terminateChildProcess } from './process-utils.js';
9
9
  const WINDOWS = process.platform === 'win32';
10
10
  const COMMAND_CODE_BINARIES = WINDOWS
11
11
  ? ['command-code', 'commandcode', 'cmdc']
@@ -91,8 +91,12 @@ const CATALOG_EFFORTS = new Map([
91
91
  ['xai/grok-4.7', ['low', 'medium', 'high', 'xhigh']],
92
92
  ]);
93
93
  /**
94
- * Command Code 驱动:沿用附件中的 `--session + -p + --output-format json` 方式。
95
- * 它只负责 CLI 进程和事件转换,不把 Command Code 私有事件泄漏给上层。
94
+ * Command Code 驱动:沿用 `--session + -p + --output-format json` 的 NDJSON 事件流。
95
+ *
96
+ * 与 Codex 驱动保持同一套消息优化:正文/推理增量携带 assistant 消息身份,工具完成后
97
+ * 的下一条 assistant 消息之前结束当前 DSH step,因此一个 Provider 运行会被切成多个
98
+ * step,而不是把整轮正文堆积到最后一条结算消息里。驱动只输出公共事件契约,工具历史、
99
+ * usage 和原生组件映射全部交给公共消息投影层。
96
100
  */
97
101
  export class CommandCodeDriver {
98
102
  descriptor = {
@@ -101,12 +105,16 @@ export class CommandCodeDriver {
101
105
  protocol: 'command',
102
106
  capabilities: ['models', 'stream', 'resume', 'interrupt', 'tool-events', 'reasoning', 'usage'],
103
107
  };
108
+ /** 驱动自己维护 Provider turn 边界,Host 可以把工具边界映射为 DSH step。 */
109
+ supportsSegmentedTurns = true;
104
110
  homeDirectory;
105
111
  binaries;
106
112
  runSpawnSync;
107
113
  runSpawn;
108
114
  cachedBinary = null;
115
+ cachedEnvironment;
109
116
  processes = new Set();
117
+ turns = new Map();
110
118
  constructor(options = {}) {
111
119
  this.homeDirectory = options.homeDirectory ?? join(homedir(), '.commandcode');
112
120
  this.binaries = options.binaries ?? COMMAND_CODE_BINARIES;
@@ -115,28 +123,47 @@ export class CommandCodeDriver {
115
123
  }
116
124
  async detect() {
117
125
  for (const command of this.binaries) {
118
- try {
119
- const result = this.runSpawnSync(command, ['--version'], { encoding: 'utf8', timeout: 3_000, windowsHide: true, shell: WINDOWS });
120
- const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
121
- const version = output.match(/\d+\.\d+\.\d+/u)?.[0] ?? null;
122
- if (result.status === 0 && version !== null) {
123
- this.cachedBinary = command;
124
- return { installed: true, version, command };
125
- }
126
- }
127
- catch {
128
- // PATH 中不存在候选命令属于正常的未安装状态。
129
- }
126
+ const direct = this.detectCommand(command);
127
+ if (direct !== null)
128
+ return direct;
129
+ if (!this.lookupAfterDetectionFailure)
130
+ continue;
131
+ const resolved = resolveCommandPath(command, this.runSpawnSync);
132
+ if (resolved === null)
133
+ continue;
134
+ const fallback = this.detectCommand(resolved);
135
+ if (fallback !== null)
136
+ return fallback;
130
137
  }
131
138
  return { installed: false, version: null, command: null };
132
139
  }
140
+ lookupAfterDetectionFailure = false;
141
+ detectCommand(command) {
142
+ this.lookupAfterDetectionFailure = false;
143
+ try {
144
+ const result = this.runSpawnSync(command, ['--version'], { encoding: 'utf8', timeout: 3_000, windowsHide: true, shell: WINDOWS, env: commandEnvironment(command) });
145
+ const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
146
+ const version = output.match(/\d+\.\d+\.\d+/u)?.[0] ?? null;
147
+ if (result.status === 0 && version !== null) {
148
+ this.cachedBinary = command;
149
+ this.cachedEnvironment = commandEnvironment(command);
150
+ return { installed: true, version, command };
151
+ }
152
+ this.lookupAfterDetectionFailure = result.status === null;
153
+ }
154
+ catch {
155
+ // PATH 中不存在候选命令属于正常的未安装状态。
156
+ this.lookupAfterDetectionFailure = true;
157
+ }
158
+ return null;
159
+ }
133
160
  async listModels() {
134
161
  const detection = await this.detect();
135
162
  if (!detection.installed || detection.command === null)
136
163
  return emptyCatalog();
137
164
  let stdout = '';
138
165
  try {
139
- const result = this.runSpawnSync(detection.command, ['--list-models'], { encoding: 'utf8', timeout: 12_000, windowsHide: true, shell: WINDOWS });
166
+ const result = this.runSpawnSync(detection.command, ['--list-models'], { encoding: 'utf8', timeout: 12_000, windowsHide: true, shell: WINDOWS, ...(this.cachedEnvironment === undefined ? {} : { env: this.cachedEnvironment }) });
140
167
  stdout = result.stdout ?? '';
141
168
  }
142
169
  catch {
@@ -177,6 +204,52 @@ export class CommandCodeDriver {
177
204
  const binary = this.cachedBinary ?? (await this.detect()).command;
178
205
  if (binary === null)
179
206
  throw new Error('Command Code 未安装');
207
+ const segmented = input.splitToolSteps === true;
208
+ const turn = segmented ? this.acquireTurn(input, binary) : this.startTurn(input, binary);
209
+ let suspended = false;
210
+ const onAbort = () => { turn.aborted = true; this.disposeTurn(turn); };
211
+ input.signal?.addEventListener('abort', onAbort, { once: true });
212
+ if (input.signal?.aborted)
213
+ onAbort();
214
+ try {
215
+ yield* this.consumeTurn(turn, input, segmented, () => { suspended = true; });
216
+ }
217
+ finally {
218
+ input.signal?.removeEventListener('abort', onAbort);
219
+ // 只有驱动自己为下一个 DSH step 挂起时才保留进程;正常结束、取消和调用方
220
+ // 提前关闭流都必须回收 CLI 进程与临时 transcript。
221
+ if (!suspended)
222
+ this.disposeTurn(turn);
223
+ }
224
+ }
225
+ dispose() {
226
+ for (const turn of [...this.turns.values()])
227
+ this.disposeTurn(turn);
228
+ this.turns.clear();
229
+ for (const child of this.processes)
230
+ terminateChildProcess(child);
231
+ this.processes.clear();
232
+ this.cachedBinary = null;
233
+ }
234
+ /** 丢弃等待下一个 DSH step 的 `-p` 运行,避免旧进程继续占用新一轮输出。 */
235
+ discardSegmentedTurn(sessionId) {
236
+ const turn = this.turns.get(sessionId);
237
+ if (turn !== undefined)
238
+ this.disposeTurn(turn);
239
+ }
240
+ /** 只有 Host 显式声明续段时才复用常驻进程;新的用户回合必须重新启动。 */
241
+ acquireTurn(input, binary) {
242
+ const existing = this.turns.get(input.sessionId);
243
+ if (existing !== undefined) {
244
+ if (input.resumeSegmentedTurn === true && !existing.disposed && !existing.finished)
245
+ return existing;
246
+ this.disposeTurn(existing);
247
+ }
248
+ const turn = this.startTurn(input, binary);
249
+ this.turns.set(input.sessionId, turn);
250
+ return turn;
251
+ }
252
+ startTurn(input, binary) {
180
253
  const transcriptPath = join(tmpdir(), `codingns4dsh-cc-${safeId(input.sessionId)}.jsonl`);
181
254
  writeTranscript(transcriptPath, input);
182
255
  const args = ['--session', transcriptPath, '-p', input.prompt, '--output-format', 'json', '--tools-all', '--yolo'];
@@ -184,15 +257,39 @@ export class CommandCodeDriver {
184
257
  args.push('-m', input.modelId);
185
258
  if (input.effortId && input.effortId !== 'default' && input.effortId !== 'Default')
186
259
  args.push('--effort', input.effortId);
187
- const child = this.runSpawn(binary, args, { cwd: input.cwd ?? process.cwd(), env: { ...process.env }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: WINDOWS });
260
+ const child = this.runSpawn(binary, args, { cwd: input.cwd ?? process.cwd(), env: this.cachedEnvironment ?? commandEnvironment(binary), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: WINDOWS });
188
261
  this.processes.add(child);
189
- let finished = false;
190
- const onAbort = () => { terminateChildProcess(child); };
191
- input.signal?.addEventListener('abort', onAbort, { once: true });
192
262
  // 必须消费 stderr,错误内容不能回传给 DSH,避免泄露命令参数或文件片段。
193
- child.stderr.on('data', () => undefined);
263
+ child.stderr?.on('data', () => undefined);
264
+ const turn = {
265
+ sessionId: input.sessionId,
266
+ child,
267
+ transcriptPath,
268
+ queue: createEventQueue(),
269
+ currentMessageId: undefined,
270
+ sawCompletedTool: false,
271
+ pendingChunk: undefined,
272
+ terminal: false,
273
+ finished: false,
274
+ aborted: false,
275
+ failure: null,
276
+ disposed: false,
277
+ };
278
+ // 未监听 error 的 ChildProcess 会把 spawn 失败升级成宿主进程异常。
279
+ if (typeof child.on === 'function') {
280
+ child.on('error', (error) => {
281
+ turn.failure ??= error;
282
+ turn.queue.close();
283
+ });
284
+ }
285
+ void this.pumpTurn(turn);
286
+ return turn;
287
+ }
288
+ /** 常驻读取 stdout:NDJSON 转成公共事件,进程结束后关闭队列。 */
289
+ async pumpTurn(turn) {
290
+ const state = createStreamState(() => turn.aborted);
194
291
  try {
195
- const lines = readline.createInterface({ input: child.stdout });
292
+ const lines = readline.createInterface({ input: turn.child.stdout });
196
293
  try {
197
294
  for await (const line of lines) {
198
295
  if (!line.trim())
@@ -201,62 +298,234 @@ export class CommandCodeDriver {
201
298
  if (item === null)
202
299
  continue;
203
300
  const event = item.type === 'event' && isRecord(item.event) ? item.event : item;
204
- const eventType = textValue(event.type).toLowerCase();
205
- const chunks = commandCodeEventChunks(event, input.signal?.aborted ?? false);
206
- for (const chunk of chunks) {
301
+ for (const chunk of commandCodeEventChunks(event, state)) {
207
302
  if (chunk.type === 'finish')
208
- finished = true;
209
- yield chunk;
303
+ turn.terminal = true;
304
+ turn.queue.push(chunk);
210
305
  }
211
- if (eventType === 'result' && !finished)
212
- finished = true;
213
306
  }
214
307
  }
215
308
  finally {
216
309
  lines.close();
217
310
  }
218
- if (!finished) {
219
- if (input.signal?.aborted)
220
- yield { type: 'finish', reason: 'cancel' };
221
- else
222
- throw new Error('Command Code 执行失败');
223
- }
311
+ }
312
+ catch (error) {
313
+ turn.failure ??= error instanceof Error ? error : new Error(String(error));
224
314
  }
225
315
  finally {
226
- input.signal?.removeEventListener('abort', onAbort);
227
- this.processes.delete(child);
228
- terminateChildProcess(child);
229
- try {
230
- rmSync(transcriptPath, { force: true });
316
+ turn.queue.close();
317
+ this.processes.delete(turn.child);
318
+ }
319
+ }
320
+ async *consumeTurn(turn, input, segmented, suspend) {
321
+ while (true) {
322
+ let chunk;
323
+ if (turn.pendingChunk !== undefined) {
324
+ chunk = turn.pendingChunk;
325
+ turn.pendingChunk = undefined;
326
+ }
327
+ else {
328
+ const next = await turn.queue.next();
329
+ if (next.done) {
330
+ if (turn.failure !== null)
331
+ throw turn.failure;
332
+ if (turn.terminal)
333
+ return;
334
+ if (turn.aborted || input.signal?.aborted === true) {
335
+ yield { type: 'finish', reason: 'cancel' };
336
+ return;
337
+ }
338
+ throw new Error('Command Code 执行失败');
339
+ }
340
+ chunk = next.value;
341
+ }
342
+ // 一个 `-p` 运行会在同一进程里连续跑多个 agent turn。把新 assistant 消息的
343
+ // 首个正文留给下一次 llm/stream,当前流只返回边界,确保 DSH 先创建新 step。
344
+ if (segmented
345
+ && (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta')
346
+ && chunk.messageId !== undefined) {
347
+ const previousMessageId = turn.currentMessageId;
348
+ if (previousMessageId !== undefined
349
+ && previousMessageId !== chunk.messageId
350
+ && turn.sawCompletedTool) {
351
+ turn.pendingChunk = chunk;
352
+ turn.currentMessageId = chunk.messageId;
353
+ turn.sawCompletedTool = false;
354
+ suspend();
355
+ yield { type: 'step-boundary' };
356
+ return;
357
+ }
358
+ turn.currentMessageId = chunk.messageId;
359
+ }
360
+ if (chunk.type === 'tool-event' && (chunk.status === 'completed' || chunk.status === 'failed')) {
361
+ turn.sawCompletedTool = true;
231
362
  }
232
- catch { /* 临时文件清理尽力而为 */ }
363
+ if (chunk.type === 'finish')
364
+ turn.finished = true;
365
+ yield chunk;
233
366
  }
234
367
  }
235
- dispose() {
236
- for (const child of this.processes)
237
- terminateChildProcess(child);
238
- this.processes.clear();
239
- this.cachedBinary = null;
368
+ disposeTurn(turn) {
369
+ if (turn.disposed)
370
+ return;
371
+ turn.disposed = true;
372
+ turn.queue.close();
373
+ if (this.turns.get(turn.sessionId) === turn)
374
+ this.turns.delete(turn.sessionId);
375
+ this.processes.delete(turn.child);
376
+ terminateChildProcess(turn.child);
377
+ try {
378
+ rmSync(turn.transcriptPath, { force: true });
379
+ }
380
+ catch { /* 临时文件清理尽力而为 */ }
240
381
  }
241
382
  }
242
- function commandCodeEventChunks(event, cancelled) {
383
+ function createStreamState(aborted) {
384
+ return {
385
+ messageSequence: 0,
386
+ messageId: null,
387
+ emittedText: '',
388
+ emittedReasoning: '',
389
+ sawText: false,
390
+ perRequestUsageSeen: false,
391
+ turnUsageSeen: false,
392
+ sessionId: null,
393
+ aborted,
394
+ };
395
+ }
396
+ function createEventQueue() {
397
+ const values = [];
398
+ const waiters = [];
399
+ let closed = false;
400
+ const next = () => {
401
+ const value = values.shift();
402
+ if (value !== undefined)
403
+ return Promise.resolve({ done: false, value });
404
+ if (closed)
405
+ return Promise.resolve({ done: true, value: undefined });
406
+ return new Promise((resolve) => waiters.push(resolve));
407
+ };
408
+ return {
409
+ next,
410
+ push(event) {
411
+ if (closed)
412
+ return;
413
+ const waiter = waiters.shift();
414
+ if (waiter !== undefined)
415
+ waiter({ done: false, value: event });
416
+ else
417
+ values.push(event);
418
+ },
419
+ close() {
420
+ if (closed)
421
+ return;
422
+ closed = true;
423
+ while (waiters.length > 0)
424
+ waiters.shift()?.({ done: true, value: undefined });
425
+ },
426
+ };
427
+ }
428
+ /**
429
+ * Command Code NDJSON → 公共 Agent 事件。
430
+ *
431
+ * 关键约定(对照本机 1.66.0 真实事件流):
432
+ * - `turn_start` / `message_start` 标识新的 assistant 消息,正文和推理增量都带上它;
433
+ * - `tool_*` 使用 `toolCallId` 作为稳定调用 ID,同一调用的 queued/running/completed
434
+ * 只能投影成一个 DSH 工具节点;
435
+ * - `model_request_end` 的 usage 是当前请求的上下文占用;`run_end`/最终 `result`
436
+ * 的 usage 是整个运行的累计值,只在完全没有单请求 usage 时兜底。
437
+ */
438
+ function commandCodeEventChunks(event, state) {
243
439
  const chunks = [];
244
440
  const type = textValue(event.type).trim().toLowerCase();
245
- const sessionId = textValue(event.sessionId ?? event.session_id ?? recordValue(event.session)?.id ?? recordValue(event.result)?.sessionId).trim();
246
- if (sessionId)
441
+ const sessionId = readSessionId(event);
442
+ if (sessionId !== null && sessionId !== state.sessionId) {
443
+ state.sessionId = sessionId;
247
444
  chunks.push({ type: 'session-binding', providerSessionId: sessionId });
248
- if (type === 'thinking_delta' || type === 'thinking-delta') {
249
- const text = textValue(event.delta ?? event.thinking ?? event.content);
250
- if (text)
251
- chunks.push({ type: 'reasoning-delta', text });
252
- }
253
- else if (type === 'text_delta' || type === 'text-delta') {
254
- const text = textValue(event.delta ?? event.text ?? event.content);
255
- if (text)
256
- chunks.push({ type: 'text-delta', text });
257
445
  }
258
- else if (type === 'message' || type === 'message_update' || type === 'message-update') {
259
- appendMessageSnapshots(chunks, event);
446
+ switch (type) {
447
+ case 'turn_start':
448
+ case 'turn-start':
449
+ beginMessage(state);
450
+ break;
451
+ case 'message_start':
452
+ case 'message-start':
453
+ // 正常一轮只有一个模型请求;没有 turn_start 的旧版本由这里补消息身份。
454
+ if (state.messageId === null)
455
+ beginMessage(state);
456
+ break;
457
+ case 'text_delta':
458
+ case 'text-delta': {
459
+ const text = textValue(event.delta ?? event.text ?? event.content);
460
+ if (text) {
461
+ ensureMessage(state);
462
+ state.emittedText += text;
463
+ state.sawText = true;
464
+ chunks.push(textDelta(text, state));
465
+ }
466
+ break;
467
+ }
468
+ case 'thinking_delta':
469
+ case 'thinking-delta': {
470
+ const text = textValue(event.delta ?? event.thinking ?? event.content);
471
+ if (text) {
472
+ ensureMessage(state);
473
+ state.emittedReasoning += text;
474
+ chunks.push(reasoningDelta(text, state));
475
+ }
476
+ break;
477
+ }
478
+ case 'message':
479
+ case 'message_update':
480
+ case 'message-update':
481
+ case 'message_end':
482
+ case 'message-end':
483
+ // 只补齐增量流没有覆盖到的正文,避免累计快照被重复追加。
484
+ appendContentFallback(chunks, event.content ?? recordValue(event.message)?.content, state);
485
+ break;
486
+ case 'model_request_end':
487
+ case 'model-request-end': {
488
+ const usage = usageChunk(recordValue(event.usage));
489
+ if (usage !== null) {
490
+ state.perRequestUsageSeen = true;
491
+ state.turnUsageSeen = true;
492
+ chunks.push(usage);
493
+ }
494
+ break;
495
+ }
496
+ case 'turn_end':
497
+ case 'turn-end': {
498
+ // 旧版本可能只在 turn_end 带 usage;与 model_request_end 去重,不重复结算。
499
+ if (!state.turnUsageSeen) {
500
+ const usage = usageChunk(recordValue(event.usage));
501
+ if (usage !== null) {
502
+ state.perRequestUsageSeen = true;
503
+ state.turnUsageSeen = true;
504
+ chunks.push(usage);
505
+ }
506
+ }
507
+ break;
508
+ }
509
+ case 'result': {
510
+ if (!state.perRequestUsageSeen) {
511
+ const usage = usageChunk(recordValue(event.usage));
512
+ if (usage !== null)
513
+ chunks.push(usage);
514
+ }
515
+ if (!state.sawText) {
516
+ const finalText = textValue(event.finalText ?? recordValue(event.result)?.finalText ?? (typeof event.result === 'string' ? event.result : undefined));
517
+ if (finalText) {
518
+ ensureMessage(state);
519
+ state.sawText = true;
520
+ state.emittedText += finalText;
521
+ chunks.push(textDelta(finalText, state));
522
+ }
523
+ }
524
+ chunks.push({ type: 'finish', reason: state.aborted() ? 'cancel' : resultReason(event) });
525
+ break;
526
+ }
527
+ default:
528
+ break;
260
529
  }
261
530
  if (isToolStart(type)) {
262
531
  const tool = readToolChunk(event, type === 'tool_queued' || type === 'tool_started' ? 'started' : 'running');
@@ -264,62 +533,82 @@ function commandCodeEventChunks(event, cancelled) {
264
533
  chunks.push(tool);
265
534
  }
266
535
  else if (isToolResult(type)) {
267
- const tool = readToolChunk(event, type.includes('error') || type.includes('fail') || type.includes('denied') ? 'failed' : 'completed');
268
- if (tool !== null)
536
+ const failed = type.includes('error') || type.includes('fail') || type.includes('denied') || type.includes('blocked');
537
+ const tool = readToolChunk(event, failed ? 'failed' : 'completed');
538
+ if (tool !== null) {
269
539
  chunks.push(tool);
270
- }
271
- const resultRecord = recordValue(event.result);
272
- const usage = recordValue(event.usage) ?? recordValue(resultRecord?.usage);
273
- const usageEvent = usageChunk(usage);
274
- if (usageEvent !== null)
275
- chunks.push(usageEvent);
276
- if (type === 'result') {
277
- const finalText = textValue(event.finalText ?? resultRecord?.finalText ?? (typeof event.result === 'string' ? event.result : undefined) ?? event.output ?? event.text);
278
- if (finalText)
279
- chunks.push({ type: 'text-snapshot', text: finalText });
280
- chunks.push({ type: 'finish', reason: cancelled ? 'cancel' : 'stop' });
540
+ // 工具完成后的正文属于下一条 assistant 消息。即使 CLI 没有发送
541
+ // turn_start/message_start,也要让公共投影层看到消息身份切换。
542
+ if (tool.status === 'completed' || tool.status === 'failed')
543
+ resetMessageIdentity(state);
544
+ }
281
545
  }
282
546
  return chunks;
283
547
  }
284
- function appendMessageSnapshots(chunks, event) {
285
- const payload = recordValue(event.message ?? event.data) ?? event;
286
- if (Array.isArray(payload.content)) {
287
- const snapshots = new Map();
288
- for (const block of payload.content) {
289
- const value = recordValue(block);
290
- if (value === null)
291
- continue;
292
- const blockType = textValue(value.type).toLowerCase();
293
- const channel = blockType.includes('thinking') || blockType.includes('reasoning') ? 'reasoning' : 'text';
294
- const text = channel === 'reasoning'
295
- ? textValue(value.thinking ?? value.reasoning ?? value.text ?? value.content)
296
- : textValue(value.text ?? value.content);
297
- if (!text)
298
- continue;
299
- const values = snapshots.get(channel) ?? [];
300
- values.push(text);
301
- snapshots.set(channel, values);
302
- }
303
- for (const [channel, values] of snapshots)
304
- chunks.push({ type: `${channel}-snapshot`, text: values.join('') });
548
+ function beginMessage(state) {
549
+ state.messageSequence += 1;
550
+ state.messageId = `command-code-message-${state.messageSequence}`;
551
+ state.emittedText = '';
552
+ state.emittedReasoning = '';
553
+ state.turnUsageSeen = false;
554
+ }
555
+ /** 丢弃当前消息身份;下一条正文增量会懒加载新的身份。 */
556
+ function resetMessageIdentity(state) {
557
+ state.messageId = null;
558
+ state.emittedText = '';
559
+ state.emittedReasoning = '';
560
+ }
561
+ function ensureMessage(state) {
562
+ if (state.messageId === null)
563
+ beginMessage(state);
564
+ }
565
+ function textDelta(text, state) {
566
+ return { type: 'text-delta', text, ...(state.messageId === null ? {} : { messageId: state.messageId }) };
567
+ }
568
+ function reasoningDelta(text, state) {
569
+ return { type: 'reasoning-delta', text, ...(state.messageId === null ? {} : { messageId: state.messageId }) };
570
+ }
571
+ /** 用完整的 assistant content 补齐缺失的正文/推理增量(只发送尚未发送的尾部)。 */
572
+ function appendContentFallback(chunks, content, state) {
573
+ if (!Array.isArray(content))
305
574
  return;
575
+ let text = '';
576
+ let reasoning = '';
577
+ for (const block of content) {
578
+ const value = recordValue(block);
579
+ if (value === null)
580
+ continue;
581
+ const blockType = textValue(value.type).toLowerCase();
582
+ const isReasoning = blockType.includes('thinking') || blockType.includes('reasoning')
583
+ || typeof value.thinking === 'string' || typeof value.reasoning === 'string';
584
+ if (isReasoning)
585
+ reasoning += textValue(value.thinking ?? value.reasoning ?? value.text ?? value.content);
586
+ else
587
+ text += textValue(value.text ?? value.content);
588
+ }
589
+ if (text.length > state.emittedText.length) {
590
+ const delta = text.slice(state.emittedText.length);
591
+ ensureMessage(state);
592
+ state.emittedText = text;
593
+ state.sawText = true;
594
+ chunks.push(textDelta(delta, state));
595
+ }
596
+ if (reasoning.length > state.emittedReasoning.length) {
597
+ const delta = reasoning.slice(state.emittedReasoning.length);
598
+ ensureMessage(state);
599
+ state.emittedReasoning = reasoning;
600
+ chunks.push(reasoningDelta(delta, state));
306
601
  }
307
- const reasoning = textValue(payload.thinking ?? payload.reasoning);
308
- if (reasoning)
309
- chunks.push({ type: 'reasoning-snapshot', text: reasoning });
310
- const text = textValue(payload.text ?? payload.content ?? event.text ?? event.content);
311
- if (text)
312
- chunks.push({ type: 'text-snapshot', text });
313
602
  }
314
603
  function readToolChunk(event, status) {
315
- const callId = textValue(event.callId ?? event.call_id ?? event.toolUseId ?? event.tool_use_id ?? event.id);
604
+ const callId = firstToolText(event.callId, event.call_id, event.toolCallId, event.tool_call_id, event.toolUseId, event.tool_use_id, event.id);
316
605
  const fn = recordValue(event.function);
317
- const toolName = textValue(event.name ?? event.toolName ?? event.tool ?? fn?.name) || 'tool';
318
- const error = textValue(event.error ?? event.reason);
606
+ const toolName = textValue(event.name ?? event.toolName ?? event.tool_name ?? event.tool ?? fn?.name) || 'tool';
607
+ const error = textValue(event.error ?? event.reason ?? event.message);
319
608
  const output = textValue(event.output ?? event.result ?? event.content);
320
609
  const input = event.input ?? fn?.arguments ?? event.arguments;
321
610
  const agentId = firstToolText(event.agentId, event.agent_id);
322
- const detail = serializeToolValue(event.detail ?? event.metadata);
611
+ const detail = serializeToolValue(event.detail ?? event.metadata ?? event.description);
323
612
  if (!callId && !toolName)
324
613
  return null;
325
614
  return {
@@ -339,7 +628,21 @@ function isToolStart(type) {
339
628
  return ['tool_queued', 'tool_started', 'tool_running', 'tool_use', 'tool_call', 'function_call'].includes(type);
340
629
  }
341
630
  function isToolResult(type) {
342
- return ['tool_completed', 'tool_result', 'tool_return', 'tool_failed', 'tool_error', 'tool_denied', 'function_result'].includes(type);
631
+ return ['tool_completed', 'tool_result', 'tool_return', 'tool_failed', 'tool_error', 'tool_denied', 'tool_hook_blocked', 'function_result'].includes(type);
632
+ }
633
+ function resultReason(event) {
634
+ const resultRecord = recordValue(event.result);
635
+ const stopReason = textValue(event.stopReason ?? resultRecord?.stopReason).toLowerCase();
636
+ const subtype = textValue(event.subtype).toLowerCase();
637
+ if (event.error !== undefined || subtype === 'error' || stopReason.includes('error') || stopReason.includes('fail'))
638
+ return 'error';
639
+ if (stopReason.includes('interrupt') || stopReason.includes('cancel') || stopReason === 'aborted')
640
+ return 'cancel';
641
+ return 'stop';
642
+ }
643
+ function readSessionId(event) {
644
+ const value = textValue(event.sessionId ?? event.session_id ?? recordValue(event.session)?.id ?? recordValue(event.result)?.sessionId).trim();
645
+ return value === '' ? null : value;
343
646
  }
344
647
  function textValue(value) {
345
648
  return typeof value === 'string' ? value : value === undefined || value === null ? '' : structuredText(value);
@@ -29,6 +29,8 @@ export interface CodingNsCliDriver {
29
29
  readonly descriptor: Omit<CodingNsCliAdapterDescriptor, 'installed' | 'enabled' | 'version' | 'command'>;
30
30
  /** 驱动是否已经自行维护 Provider turn 的 step 边界。 */
31
31
  readonly supportsSegmentedTurns?: boolean;
32
+ /** 丢弃等待下一个 DSH step 的 Provider 运行;只有自行分段的驱动需要实现。 */
33
+ discardSegmentedTurn?(sessionId: string): void;
32
34
  detect(): Promise<Pick<CodingNsCliAdapterDescriptor, 'installed' | 'version' | 'command'>>;
33
35
  listModels(): Promise<CodingNsCliModelCatalog>;
34
36
  probeSession?(input: CodingNsCliSessionProbeInput): Promise<CodingNsCliSessionProbeResult>;
@@ -27,6 +27,8 @@ export declare class CodingNsDshMessageProjector {
27
27
  private reasoningText;
28
28
  private textText;
29
29
  private finished;
30
+ /** 只要收到正文、工具、usage 或交互事件,就说明 Provider 确实产出了结果。 */
31
+ private receivedProviderEvent;
30
32
  constructor(options: CodingNsDshMessageProjectorOptions);
31
33
  get isFinished(): boolean;
32
34
  push(event: CodingNsAgentEvent): Promise<readonly CodingNsDshStreamChunk[]>;