@zhushanwen/subagent-core 0.2.0

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 (330) hide show
  1. package/README.md +114 -0
  2. package/dist/chunk-3VOERJPJ.js +22 -0
  3. package/dist/chunk-A4IVZWVX.js +31 -0
  4. package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
  5. package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
  6. package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
  7. package/dist/execution/engine/engines/zcode/constants.js +22 -0
  8. package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
  9. package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
  10. package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
  11. package/dist/execution/engine/engines/zcode/reader.js +239 -0
  12. package/dist/execution/engine/paths.cjs +55 -0
  13. package/dist/execution/engine/paths.d.cts +8 -0
  14. package/dist/execution/engine/paths.d.ts +8 -0
  15. package/dist/execution/engine/paths.js +26 -0
  16. package/dist/execution/relay-env.cjs +62 -0
  17. package/dist/execution/relay-env.d.cts +33 -0
  18. package/dist/execution/relay-env.d.ts +33 -0
  19. package/dist/execution/relay-env.js +20 -0
  20. package/dist/index.cjs +1852 -0
  21. package/dist/index.d.cts +1184 -0
  22. package/dist/index.d.ts +1184 -0
  23. package/dist/index.js +1810 -0
  24. package/dist/types-BxyAidGf.d.cts +875 -0
  25. package/dist/types-BxyAidGf.d.ts +875 -0
  26. package/package.json +99 -0
  27. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
  28. package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
  29. package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
  30. package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
  31. package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
  32. package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
  33. package/src/__tests__/m2-append-content-probe.test.ts +73 -0
  34. package/src/__tests__/manifest-store.test.ts +368 -0
  35. package/src/__tests__/record-store-cache.test.ts +335 -0
  36. package/src/__tests__/record-store-index.test.ts +560 -0
  37. package/src/__tests__/record-store-last-line.test.ts +129 -0
  38. package/src/__tests__/review-fix-loop-script.test.ts +288 -0
  39. package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
  40. package/src/__tests__/robustness-low-batch1.test.ts +286 -0
  41. package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
  42. package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
  43. package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
  44. package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
  45. package/src/__tests__/session-runner.test.ts +69 -0
  46. package/src/__tests__/smoke.test.ts +9 -0
  47. package/src/__tests__/stream-sink.test.ts +203 -0
  48. package/src/core/__tests__/host-services.test.ts +144 -0
  49. package/src/core/__tests__/logger.test.ts +78 -0
  50. package/src/core/__tests__/notify-ports.test.ts +104 -0
  51. package/src/core/host-services.ts +97 -0
  52. package/src/core/logger.ts +47 -0
  53. package/src/core/notify-ports.ts +126 -0
  54. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
  55. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  56. package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
  57. package/src/execution/__tests__/alive-store.test.ts +147 -0
  58. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
  59. package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
  60. package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
  61. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  62. package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
  63. package/src/execution/__tests__/config.test.ts +302 -0
  64. package/src/execution/__tests__/delivery-methods.test.ts +424 -0
  65. package/src/execution/__tests__/dialog-queue.test.ts +299 -0
  66. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  67. package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
  68. package/src/execution/__tests__/execute-nesting.test.ts +323 -0
  69. package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
  70. package/src/execution/__tests__/execution-record.test.ts +1394 -0
  71. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  72. package/src/execution/__tests__/finalize-record.test.ts +367 -0
  73. package/src/execution/__tests__/finalized-marker.test.ts +104 -0
  74. package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
  75. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  76. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  77. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  78. package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
  79. package/src/execution/__tests__/host-mode.test.ts +87 -0
  80. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  83. package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
  84. package/src/execution/__tests__/model-resolver.test.ts +465 -0
  85. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  86. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  87. package/src/execution/__tests__/output-collector.test.ts +358 -0
  88. package/src/execution/__tests__/path-encoding.test.ts +104 -0
  89. package/src/execution/__tests__/pi-invocation.test.ts +134 -0
  90. package/src/execution/__tests__/record-store.test.ts +1057 -0
  91. package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
  92. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
  93. package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
  94. package/src/execution/__tests__/relay-env.test.ts +42 -0
  95. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  96. package/src/execution/__tests__/rpc-mode.test.ts +89 -0
  97. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  98. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  99. package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
  100. package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
  101. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  102. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
  103. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  104. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  105. package/src/execution/__tests__/session-file-gc.test.ts +293 -0
  106. package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
  107. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  108. package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
  109. package/src/execution/__tests__/spawn-args.test.ts +445 -0
  110. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
  111. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  112. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
  113. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  114. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  115. package/src/execution/__tests__/stdin-writer.test.ts +462 -0
  116. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  117. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  118. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  119. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  120. package/src/execution/__tests__/subagent-service.test.ts +788 -0
  121. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  122. package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
  123. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  124. package/src/execution/__tests__/timeout-integration.test.ts +615 -0
  125. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  126. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  127. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  128. package/src/execution/__tests__/ui-channels.test.ts +187 -0
  129. package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
  130. package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
  131. package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
  132. package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
  133. package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
  134. package/src/execution/__tests__/worktree-manager.test.ts +633 -0
  135. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
  136. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  137. package/src/execution/__tests__/worktree-registry.test.ts +199 -0
  138. package/src/execution/agent-registry.ts +199 -0
  139. package/src/execution/agent-result-mapper.ts +90 -0
  140. package/src/execution/alive-store.ts +119 -0
  141. package/src/execution/argv-mirror.ts +113 -0
  142. package/src/execution/best-effort.ts +38 -0
  143. package/src/execution/channel-registry-access.ts +144 -0
  144. package/src/execution/concurrency-pool.ts +116 -0
  145. package/src/execution/config.ts +165 -0
  146. package/src/execution/dialog-queue.ts +330 -0
  147. package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
  148. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  149. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  150. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  151. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  152. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  153. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  154. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  155. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  156. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  157. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  158. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  159. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  160. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  161. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  162. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  163. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  164. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  165. package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
  166. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  167. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  168. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  169. package/src/execution/engine/common/data-dir.ts +66 -0
  170. package/src/execution/engine/common/errors.ts +183 -0
  171. package/src/execution/engine/common/event-journal.ts +254 -0
  172. package/src/execution/engine/common/journal-replay.ts +62 -0
  173. package/src/execution/engine/common/kill-chain.ts +221 -0
  174. package/src/execution/engine/common/nesting-guard.ts +50 -0
  175. package/src/execution/engine/common/persona-router.ts +108 -0
  176. package/src/execution/engine/common/pool-manager.ts +226 -0
  177. package/src/execution/engine/common/schema-emulation.ts +189 -0
  178. package/src/execution/engine/common/session-view-projection.ts +51 -0
  179. package/src/execution/engine/engine-discovery.ts +65 -0
  180. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  181. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  182. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  183. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  184. package/src/execution/engine/engines/pi/reader.ts +48 -0
  185. package/src/execution/engine/engines/pi/registration.ts +35 -0
  186. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  187. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  188. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  189. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  190. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  191. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  192. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
  193. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  194. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  195. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  196. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  197. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  198. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  199. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  200. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  201. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  202. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  203. package/src/execution/engine/host-task-spec.ts +47 -0
  204. package/src/execution/engine/model-prompt.ts +158 -0
  205. package/src/execution/engine/paths.ts +42 -0
  206. package/src/execution/engine/port.ts +153 -0
  207. package/src/execution/engine/registry.ts +133 -0
  208. package/src/execution/engine/routing.ts +218 -0
  209. package/src/execution/engine/types.ts +309 -0
  210. package/src/execution/execute-options-mapper.ts +115 -0
  211. package/src/execution/execution-record.ts +984 -0
  212. package/src/execution/finalize-record.ts +254 -0
  213. package/src/execution/finalized-marker.ts +70 -0
  214. package/src/execution/get-state-handshake.ts +104 -0
  215. package/src/execution/host-mode.ts +56 -0
  216. package/src/execution/idle-gc.ts +47 -0
  217. package/src/execution/lifecycle-manager.ts +513 -0
  218. package/src/execution/lifecycle-predicates.ts +65 -0
  219. package/src/execution/manifest-store.ts +256 -0
  220. package/src/execution/model-config-service.ts +250 -0
  221. package/src/execution/model-resolver.ts +248 -0
  222. package/src/execution/notifier.ts +372 -0
  223. package/src/execution/notify-ledger.ts +580 -0
  224. package/src/execution/output-collector.ts +228 -0
  225. package/src/execution/path-encoding.ts +60 -0
  226. package/src/execution/pi-invocation.ts +120 -0
  227. package/src/execution/record-entry.ts +132 -0
  228. package/src/execution/record-store.ts +1265 -0
  229. package/src/execution/relay-env.ts +37 -0
  230. package/src/execution/session-context-resolver.ts +64 -0
  231. package/src/execution/session-file-gc.ts +120 -0
  232. package/src/execution/session-pending.ts +170 -0
  233. package/src/execution/session-reconstructor.ts +678 -0
  234. package/src/execution/session-runner.ts +1789 -0
  235. package/src/execution/sessions-index.ts +304 -0
  236. package/src/execution/spawn-event-adapter.ts +363 -0
  237. package/src/execution/stdin-writer.ts +198 -0
  238. package/src/execution/stream-sink.ts +126 -0
  239. package/src/execution/subagent-service.ts +2268 -0
  240. package/src/execution/subprocess-agent-runner.ts +317 -0
  241. package/src/execution/temp-prompt.ts +62 -0
  242. package/src/execution/tombstone-store.ts +72 -0
  243. package/src/execution/turn-limiter.ts +102 -0
  244. package/src/execution/types.ts +1023 -0
  245. package/src/execution/ui-channels.ts +216 -0
  246. package/src/execution/ui-interaction-model.ts +48 -0
  247. package/src/execution/ui-request-handler-factory.ts +243 -0
  248. package/src/execution/ui-request-observability.ts +81 -0
  249. package/src/execution/ui-request-queue.ts +184 -0
  250. package/src/execution/worktree-manager.ts +688 -0
  251. package/src/execution/worktree-registry.ts +279 -0
  252. package/src/index.ts +87 -0
  253. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
  254. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
  255. package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  257. package/src/orchestration/__tests__/config-loader.test.ts +503 -0
  258. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
  259. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
  260. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
  261. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  262. package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
  263. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
  264. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  265. package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
  266. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  267. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
  268. package/src/orchestration/__tests__/script-lint.test.ts +831 -0
  269. package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
  270. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  271. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  272. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  273. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  274. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
  275. package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
  276. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
  277. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
  278. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  279. package/src/orchestration/agent-opts-resolver.ts +174 -0
  280. package/src/orchestration/args-validator.ts +127 -0
  281. package/src/orchestration/config-loader.ts +316 -0
  282. package/src/orchestration/error-recovery.ts +1018 -0
  283. package/src/orchestration/execute-agent-call.ts +256 -0
  284. package/src/orchestration/launcher.ts +455 -0
  285. package/src/orchestration/lifecycle.ts +399 -0
  286. package/src/orchestration/models/__tests__/budget.test.ts +307 -0
  287. package/src/orchestration/models/agent-call.ts +83 -0
  288. package/src/orchestration/models/budget.ts +118 -0
  289. package/src/orchestration/models/ports.ts +164 -0
  290. package/src/orchestration/models/run-runtime.ts +104 -0
  291. package/src/orchestration/models/run-spec.ts +83 -0
  292. package/src/orchestration/models/run-state.ts +44 -0
  293. package/src/orchestration/models/trace.ts +183 -0
  294. package/src/orchestration/models/types.ts +301 -0
  295. package/src/orchestration/models/workflow-run.ts +254 -0
  296. package/src/orchestration/models/workflow-script-registry.ts +35 -0
  297. package/src/orchestration/models/workflow-script.ts +117 -0
  298. package/src/orchestration/script-lint.ts +766 -0
  299. package/src/orchestration/skill-discovery.ts +135 -0
  300. package/src/orchestration/worker-handle.ts +115 -0
  301. package/src/orchestration/worker-host.ts +98 -0
  302. package/src/orchestration/worker-script-builder.ts +421 -0
  303. package/src/orchestration/workflow-files.ts +85 -0
  304. package/src/orchestration/workflow-script-registry-impl.ts +133 -0
  305. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  306. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  307. package/src/shared/__tests__/model-ref.test.ts +306 -0
  308. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
  309. package/src/shared/__tests__/resource-discovery.test.ts +749 -0
  310. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  311. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  312. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  313. package/src/shared/agent-event.ts +13 -0
  314. package/src/shared/agent-ref.ts +57 -0
  315. package/src/shared/meta-parser.ts +261 -0
  316. package/src/shared/model-ref.ts +286 -0
  317. package/src/shared/resource-discovery.ts +835 -0
  318. package/src/shared/resource-meta.ts +65 -0
  319. package/src/shared/schema-env.ts +44 -0
  320. package/src/shared/schema-jsonify.ts +58 -0
  321. package/src/shared/timer-delay.ts +54 -0
  322. package/src/shared/xml-injection.ts +35 -0
  323. package/workflows/README.md +81 -0
  324. package/workflows/_shared/agent-refs.cjs +40 -0
  325. package/workflows/chain.js +137 -0
  326. package/workflows/map-reduce.js +180 -0
  327. package/workflows/parallel.js +154 -0
  328. package/workflows/review-fix-loop-utils.cjs +1542 -0
  329. package/workflows/review-fix-loop.js +1605 -0
  330. package/workflows/scatter-gather.js +184 -0
@@ -0,0 +1,984 @@
1
+ // src/core/execution-record.ts
2
+ //
3
+ // 唯一执行状态对象 + 唯一创建/更新/完成/投影入口。
4
+ //
5
+ // 收口设计(2026-06-22 重构):
6
+ // 一次执行的完整内容(text/thinking/toolCalls/usage)按 turn 收口在 record.turns[]。
7
+ // eventLog / currentActivity / result 文本均从 turns[] 派生(getEventLog /
8
+ // getCurrentActivity / getFullText),不再独立存储切片或缓冲。
9
+ //
10
+ // createRecord 唯一创建入口(model 创建时必填,消灭 poll 路径 model 丢失)
11
+ // updateFromEvent 唯一事件更新入口(累积进 turns[],消灭闭包旁路累积器)
12
+ // completeRecord 唯一完成入口(冻结状态)
13
+ // project/snapshot 唯一投影入口(两路径字段一致)
14
+ //
15
+ // Core 层叶子原语:仅依赖 types.ts。零 Pi / Runtime / TUI 依赖。
16
+
17
+ import type {
18
+ AgentEvent,
19
+ AgentEventLogEntry,
20
+ AgentResult,
21
+ AgentUsage,
22
+ AgentUsageTotal,
23
+ ClosedReason,
24
+ DisplayItem,
25
+ ExecutionMode,
26
+ ExecutionOutcome,
27
+ ExecutionRecord,
28
+ ExecutionStatus,
29
+ InternalToolCall,
30
+ ProjectedOutcome,
31
+ RecordSnapshot,
32
+ SubagentToolDetails,
33
+ ToolCall,
34
+ ToolCallResult,
35
+ Turn,
36
+ } from "./types.ts";
37
+
38
+ // ============================================================
39
+ // 常量
40
+ // ============================================================
41
+
42
+ /** currentActivity label 的前缀截断长度(与旧 ACTIVITY_LABEL_MAX 对齐)。 */
43
+ const ACTIVITY_LABEL_MAX = 60;
44
+ /** turn_end 派生条目 label 的最大长度(取本 turn 文本开头)。 */
45
+ const TURN_SUMMARY_MAX = 80;
46
+ /** tool label 的最大长度(command/query/url/basename 截断,保持 TUI 列宽稳定)。 */
47
+ const TOOL_LABEL_MAX = 100;
48
+ /** ms → s 换算。elapsedSeconds 唯一计算点用。 */
49
+ const MS_PER_SECOND = 1000;
50
+
51
+ // ============================================================
52
+ // Label 提取(eventLog 派生的伴生逻辑,co-locate 于 Core)
53
+ // ============================================================
54
+
55
+ /**
56
+ * 从 toolName + args 提取 eventLog label(人类可读)。
57
+ *
58
+ * read/edit/write → "{tool} {basename}"(取 path 参数)
59
+ * bash → "{tool} {command 首行}"(截断)
60
+ * web_search → "{tool} {query}"
61
+ * web_fetch → "{tool} {url}"
62
+ * 其他 / 无 args → 裸 toolName
63
+ *
64
+ * 纯函数(零依赖),由 getEventLog 派生 tool 条目时调用。
65
+ * 所有取自参数的字符串都经 truncateLabel 截断到 TOOL_LABEL_MAX——
66
+ * 保持 TUI 列宽稳定(避免一条 10KB bash 命令撑爆 compact view)。
67
+ */
68
+ export function extractLabelFromArgs(toolName: string, args: unknown): string {
69
+ if (typeof args !== "object" || args === null) return toolName;
70
+ const a = args as Record<string, unknown>;
71
+
72
+ // 读/写/编辑类:取路径 basename(~/.pi/.../foo.ts → foo.ts)
73
+ // 兼容 Pi tool 的多种路径参数名:path / file_path / filePath
74
+ const pathLike = (a.path ?? a.file_path ?? a.filePath) as unknown;
75
+ if (typeof pathLike === "string" && pathLike.length > 0) {
76
+ const base = pathLike.split(/[\\/]/).pop() ?? pathLike;
77
+ return `${toolName} ${truncateLabel(base)}`;
78
+ }
79
+
80
+ // bash:command 首行(截断)
81
+ const cmd = a.command as unknown;
82
+ if (typeof cmd === "string" && cmd.length > 0) {
83
+ const firstLine = cmd.split("\n", 1)[0].trim();
84
+ return `${toolName} ${truncateLabel(firstLine)}`;
85
+ }
86
+
87
+ // web_search:query
88
+ const query = a.query as unknown;
89
+ if (typeof query === "string" && query.length > 0) {
90
+ return `${toolName} ${truncateLabel(query)}`;
91
+ }
92
+
93
+ // web_fetch:url
94
+ const url = a.url as unknown;
95
+ if (typeof url === "string" && url.length > 0) {
96
+ return `${toolName} ${truncateLabel(url)}`;
97
+ }
98
+
99
+ return toolName;
100
+ }
101
+
102
+ /** 截断 label 到 maxLen(非省略号——保持列宽稳定,避免长命令/路径撑爆 TUI 列宽)。 */
103
+ function truncateLabel(label: string): string {
104
+ return label.length > TOOL_LABEL_MAX ? label.slice(0, TOOL_LABEL_MAX) : label;
105
+ }
106
+
107
+ /**
108
+ * 累加两个 AgentUsage(field-wise)。prev 为空时返回 next 的拷贝。
109
+ * 供 message_end 把 usage 增量并入 turn.usageDelta。
110
+ */
111
+ function addUsage(prev: AgentUsage | undefined, next: AgentUsage): AgentUsage {
112
+ if (prev === undefined) {
113
+ return {
114
+ input: next.input ?? 0,
115
+ output: next.output ?? 0,
116
+ cacheRead: next.cacheRead ?? 0,
117
+ cacheWrite: next.cacheWrite ?? 0,
118
+ cost: next.cost,
119
+ };
120
+ }
121
+ return {
122
+ input: (prev.input ?? 0) + (next.input ?? 0),
123
+ output: (prev.output ?? 0) + (next.output ?? 0),
124
+ cacheRead: (prev.cacheRead ?? 0) + (next.cacheRead ?? 0),
125
+ cacheWrite: (prev.cacheWrite ?? 0) + (next.cacheWrite ?? 0),
126
+ cost: (prev.cost ?? 0) + (next.cost ?? 0),
127
+ };
128
+ }
129
+
130
+ // ============================================================
131
+ // 创建(唯一入口)
132
+ // ============================================================
133
+
134
+ /** 创建一个空 turn(text/thinking 空,无 toolCalls,未闭合)。 */
135
+ function emptyTurn(): Turn {
136
+ return { text: "", thinking: "", toolCalls: [], usageDelta: undefined, closed: false };
137
+ }
138
+
139
+ /**
140
+ * 唯一创建入口。identity 字段(agent/model/thinkingLevel/mode/task)一次确定不可变。
141
+ *
142
+ * model 创建时必填——这是 poll 路径 model 丢失的架构修复
143
+ * (旧实现 background record 运行时丢 model,poll 返回缺字段)。
144
+ */
145
+ export function createRecord(
146
+ id: string,
147
+ identity: {
148
+ agent: string;
149
+ model: string;
150
+ thinkingLevel?: string;
151
+ mode: ExecutionMode;
152
+ task: string;
153
+ /** 短标签(≤20 字符),必填。持久化兜底空串。 */
154
+ slug: string;
155
+ startedAt: number;
156
+ /** 根 Pi session ID(session 隔离过滤用)。递归链上所有层同值。 */
157
+ rootSessionId?: string;
158
+ /** 直接父 subagent record ID。顶层为 undefined。 */
159
+ parentRecordId?: string;
160
+ /** subagent 递归深度。顶层=0。 */
161
+ depth?: number;
162
+ /** 对话模式标志(true = 可持续对话,轮次完成进 idle)。默认 undefined/false = 一次性。 */
163
+ chatMode?: boolean;
164
+ /** 空闲超时毫秒数(仅 chatMode 有意义)。覆盖默认 5min。 */
165
+ idleTimeoutMs?: number;
166
+ /** 实际执行引擎 id(P4 路由留痕,D9①)。缺省 = pi 投影(存量零迁移)。 */
167
+ engine?: string;
168
+ /** 引擎 fallback 留痕(probe 失败路由回默认引擎)。GUI 警告条数据源。 */
169
+ engineFallback?: { from: string; reason: string };
170
+ controller?: AbortController;
171
+ },
172
+ ): ExecutionRecord {
173
+ return {
174
+ id,
175
+ agent: identity.agent,
176
+ model: identity.model,
177
+ thinkingLevel: identity.thinkingLevel,
178
+ mode: identity.mode,
179
+ task: identity.task,
180
+ slug: identity.slug,
181
+ startedAt: identity.startedAt,
182
+ rootSessionId: identity.rootSessionId,
183
+ parentRecordId: identity.parentRecordId,
184
+ depth: identity.depth ?? 0,
185
+ chatMode: identity.chatMode,
186
+ idleTimeoutMs: identity.idleTimeoutMs,
187
+ engine: identity.engine,
188
+ engineFallback: identity.engineFallback,
189
+
190
+ // 状态(实时更新)
191
+ status: "running",
192
+ // turns[] 初始化为 [空 turn]——第一个 turn 从创建即存在,
193
+ // updateFromEvent 直接往 turns[last] 累积,无需「无 turn」分支判断。
194
+ turns: [emptyTurn()],
195
+ turnCount: 0,
196
+ totalTokens: 0,
197
+ lastError: undefined,
198
+ // 对话轮次计数(首轮 = 0,每完成一轮 finalizeRoundToIdle +1)。非 chatMode 不自增。
199
+ round: 0,
200
+
201
+ // 完成(completeRecord 唯一写点)
202
+ endedAt: undefined,
203
+ result: undefined,
204
+ error: undefined,
205
+ agentResult: undefined,
206
+
207
+ // 控制(仅 background 持有 controller;sync 为 undefined)
208
+ controller: identity.controller,
209
+ };
210
+ }
211
+
212
+ // ============================================================
213
+ // 事件更新(唯一更新点)
214
+ // ============================================================
215
+
216
+ /**
217
+ * 取当前正在进行(未 closed)的 turn;若全部 closed 则开新 turn。
218
+ * 保证调用后返回的 turn 一定 closed===false,可安全累积内容。
219
+ */
220
+ function currentTurn(record: ExecutionRecord): Turn {
221
+ const last = record.turns[record.turns.length - 1];
222
+ if (last !== undefined && !last.closed) return last;
223
+ const fresh = emptyTurn();
224
+ record.turns.push(fresh);
225
+ return fresh;
226
+ }
227
+
228
+ /**
229
+ * 在 record.turns[] 范围内倒序找最后一个同名且仍 running 的 toolCall。
230
+ *
231
+ * 扫描所有 turn(非仅当前 turn)——SDK 在 turn_end 后仍可能补发滞后的 tool_end,
232
+ * 仅扫当前 turn 会漏配对、误 push 幽灵 ToolCall。跨 turn 扫描兜底滞后事件。
233
+ *
234
+ * 返回 [turn, index];未找到返回 undefined。
235
+ */
236
+ function findRunningToolCall(
237
+ record: ExecutionRecord,
238
+ toolName: string,
239
+ ): readonly [Turn, number] | undefined {
240
+ for (let t = record.turns.length - 1; t >= 0; t--) {
241
+ const turn = record.turns[t];
242
+ if (turn === undefined) continue;
243
+ for (let i = turn.toolCalls.length - 1; i >= 0; i--) {
244
+ const tc = turn.toolCalls[i];
245
+ if (tc?._status === "running" && tc.toolName === toolName) {
246
+ return [turn, i] as const;
247
+ }
248
+ }
249
+ }
250
+ return undefined;
251
+ }
252
+
253
+ /**
254
+ * [perf] running toolCall 倒序索引:tool_start push 位置入索引,tool_end 弹尾定位
255
+ *(尾部 = 最后 push 的同名项,与 findRunningToolCall 倒序全扫的语义等价),把每次
256
+ * tool_end 的 O(所有 turns × toolCalls) 扫描降为 O(1)。WeakMap 按 record 实例隔离
257
+ *(createRecord 新实例从空索引开始,不影响旧实例)。
258
+ * 索引 miss(重建 record 的历史 running toolCall / 外部注入工具无 tool_start)
259
+ * 回退 findRunningToolCall 全扫兜底——正确性不依赖索引完整性。
260
+ */
261
+ const runningToolIndex = new WeakMap<ExecutionRecord, Map<string, Array<{ turn: Turn; idx: number }>>>();
262
+
263
+ function indexToolStart(record: ExecutionRecord, turn: Turn, toolName: string): void {
264
+ let byName = runningToolIndex.get(record);
265
+ if (byName === undefined) {
266
+ byName = new Map();
267
+ runningToolIndex.set(record, byName);
268
+ }
269
+ const arr = byName.get(toolName);
270
+ if (arr === undefined) {
271
+ byName.set(toolName, [{ turn, idx: turn.toolCalls.length - 1 }]);
272
+ } else {
273
+ arr.push({ turn, idx: turn.toolCalls.length - 1 });
274
+ }
275
+ }
276
+
277
+ /**
278
+ * 从 AgentEvent 更新 record。所有数据收口进 record.turns[]。
279
+ * - text/thinking:流式累积进 currentTurn()(完整内容,非切片)
280
+ * - tool_start/end:push 进 currentTurn().toolCalls(含完整 result)
281
+ * tool_end 跨 turn 扫描找 running 同名 toolCall(兜底滞后事件)
282
+ * - turn_end:闭合当前 turn,记 closedTs(真实墙钟,供 getEventLog);
283
+ * 正常闭合清 lastError(瞬态 error 恢复后不应误判 success=false)
284
+ * - message_end:usage 增量存进末 turn.usageDelta(直接写末 turn,不开新 turn);
285
+ * totalTokens 累加
286
+ * - error:存 record.lastError(getEventLog 派生 error 条目用)
287
+ *
288
+ * 唯一写点——session-runner 闭包不再旁路累积,collectResult 从 record 读。
289
+ *
290
+ * 穷尽性:switch 覆盖 AgentEvent 全部 variant;default 的 `never` 断言保证
291
+ * 新增 variant 时编译期报错(而非静默 no-op)。
292
+ */
293
+ export function updateFromEvent(record: ExecutionRecord, event: AgentEvent): void {
294
+ switch (event.type) {
295
+ // ── text / thinking:流式累积进当前 turn ──
296
+ case "text_delta": {
297
+ currentTurn(record).text += event.delta;
298
+ return;
299
+ }
300
+ case "thinking_delta": {
301
+ currentTurn(record).thinking += event.delta;
302
+ return;
303
+ }
304
+
305
+ // ── tool_start:push 一个 running 的 InternalToolCall(带 startedTs)──
306
+ case "tool_start": {
307
+ const tc: InternalToolCall = {
308
+ toolName: event.toolName,
309
+ args: event.args,
310
+ result: undefined,
311
+ isError: false,
312
+ _status: "running",
313
+ startedTs: Date.now(),
314
+ };
315
+ const turn = currentTurn(record);
316
+ turn.toolCalls.push(tc);
317
+ indexToolStart(record, turn, event.toolName);
318
+ return;
319
+ }
320
+
321
+ // ── tool_end:索引弹尾 O(1) 定位 running 同名 toolCall,miss 回退全扫兜底 ──
322
+ case "tool_end": {
323
+ let matched: readonly [Turn, number] | undefined;
324
+ const byName = runningToolIndex.get(record);
325
+ const arr = byName?.get(event.toolName);
326
+ if (arr !== undefined && arr.length > 0) {
327
+ const item = arr[arr.length - 1];
328
+ arr.pop();
329
+ const tc = item.turn.toolCalls[item.idx];
330
+ if (tc !== undefined && tc._status === "running") {
331
+ matched = [item.turn, item.idx] as const;
332
+ }
333
+ }
334
+ if (matched === undefined) {
335
+ // 兜底:重建 record 的历史 running toolCall(索引未覆盖)、索引项被外部路径
336
+ // 置非 running 等场景——保持与旧实现一致的跨 turn 倒序全扫。
337
+ matched = findRunningToolCall(record, event.toolName);
338
+ }
339
+ if (matched !== undefined) {
340
+ const [turn, i] = matched;
341
+ const tc = turn.toolCalls[i]!;
342
+ tc.args = event.args ?? tc.args;
343
+ tc.result = event.result;
344
+ tc.isError = event.isError ?? false;
345
+ tc._status = event.isError ? "failed" : "done";
346
+ return;
347
+ }
348
+ // 匹配失败(SDK 发了 tool_end 但无对应 tool_start,如外部注入的工具):
349
+ // 直接 push 一个已完成的 InternalToolCall,避免数据丢失。
350
+ currentTurn(record).toolCalls.push({
351
+ toolName: event.toolName,
352
+ args: event.args,
353
+ result: event.result,
354
+ isError: event.isError ?? false,
355
+ _status: event.isError ? "failed" : "done",
356
+ startedTs: Date.now(),
357
+ });
358
+ return;
359
+ }
360
+
361
+ // ── turn_end:闭合当前 turn,记 closedTs,turnCount++,清 lastError ──
362
+ case "turn_end": {
363
+ const turn = currentTurn(record);
364
+ turn.closed = true;
365
+ turn.closedTs = Date.now();
366
+ record.turnCount += 1;
367
+ // turn 正常闭合意味着本段执行成功——清掉运行期可能记录的瞬态 error,
368
+ // 避免瞬态 error 恢复后 session-runner 仍据 lastError 误判 success=false。
369
+ // (若 turn_end 后 message_end 报 error,会在 message_end 分支重新写回 lastError。)
370
+ record.lastError = undefined;
371
+ return;
372
+ }
373
+
374
+ // ── message_end:usage 增量累加进 currentTurn().usageDelta,totalTokens 累加 ──
375
+ //
376
+ // usageDelta 按 message_end **累加**(非覆盖)——同一 turn 内若多次 message_end
377
+ // 到达(或 turn_end 后的滞后 message_end 落到 currentTurn 开的新 turn),
378
+ // 累加保证不丢 usage。getTotalUsage 扁平求和所有 turn,归属 turn 的精确性
379
+ // 不影响最终 total(无消费方读单 turn usage)。
380
+ case "message_end": {
381
+ if (event.usage) {
382
+ const turn = currentTurn(record);
383
+ turn.usageDelta = addUsage(turn.usageDelta, event.usage);
384
+ // totalTokens 累加四项之和(保留旧语义,投影直接读)
385
+ record.totalTokens +=
386
+ (event.usage.input ?? 0) + (event.usage.output ?? 0) +
387
+ (event.usage.cacheRead ?? 0) + (event.usage.cacheWrite ?? 0);
388
+ }
389
+ // message_end 的 error(stopReason=error)也记进 lastError
390
+ if (event.error) {
391
+ record.lastError = event.error;
392
+ }
393
+ return;
394
+ }
395
+
396
+ // ── error:存 record.lastError(getEventLog 派生 error 条目)──
397
+ case "error": {
398
+ record.lastError = event.message;
399
+ return;
400
+ }
401
+
402
+ // ── compaction:不产生数据(不变)──
403
+ case "compaction": {
404
+ return;
405
+ }
406
+
407
+ default: {
408
+ // 穷尽性检查:新增 AgentEvent variant 时编译期报错
409
+ const _exhaustive: never = event;
410
+ return _exhaustive;
411
+ }
412
+ }
413
+ }
414
+
415
+ // ============================================================
416
+ // 派生视图(从 turns[] 推导,不存储)
417
+ // ============================================================
418
+
419
+ /**
420
+ * 从 turns[] 派生有序事件序列(eventLog)。
421
+ *
422
+ * 每个 turn 产出:tool_start/tool_end 对(按 toolCalls 顺序)+ turn_end。
423
+ * 若有 lastError,末尾追加 error 条目。
424
+ *
425
+ * [turn1{toolCalls:[A,B]}, turn2{toolCalls:[C]}] + lastError
426
+ * → [tool_start A, tool_end A, tool_start B, tool_end B, turn_end,
427
+ * tool_start C, tool_end C, turn_end, error]
428
+ *
429
+ * ts 为真实墙钟时间戳:tool 条目用 tc.startedTs,turn_end 用 turn.closedTs。
430
+ * (旧实现派生时 ts += 1 是合成值,无法表达真实时序——现已改为存真实时间戳。)
431
+ *
432
+ * 纯函数:每次调用重新生成,不缓存。消费方按需调(投影时用)。
433
+ */
434
+ export function getEventLog(record: ExecutionRecord): AgentEventLogEntry[] {
435
+ const log: AgentEventLogEntry[] = [];
436
+ for (const turn of record.turns) {
437
+ for (const tc of turn.toolCalls) {
438
+ const label = extractLabelFromArgs(tc.toolName, tc.args);
439
+ const ts = tc.startedTs;
440
+ log.push({ type: "tool_start", label, ts, status: "running" });
441
+ if (tc._status !== "running") {
442
+ log.push({ type: "tool_end", label, ts, status: tc._status });
443
+ }
444
+ }
445
+ if (turn.closed) {
446
+ const summary = turn.text.length > 0
447
+ ? (turn.text.length > TURN_SUMMARY_MAX ? turn.text.slice(0, TURN_SUMMARY_MAX) : turn.text)
448
+ : "turn";
449
+ log.push({ type: "turn_end", label: summary, ts: turn.closedTs ?? record.startedAt });
450
+ }
451
+ }
452
+ if (record.lastError) {
453
+ log.push({ type: "error", label: record.lastError, ts: Date.now() });
454
+ }
455
+ return log;
456
+ }
457
+
458
+ /**
459
+ * [STEP3] 从 turns[] 派生 displayItems(对齐 nicobailon getDisplayItems)。
460
+ *
461
+ * 与 getEventLog 的区别:产出可渲染单元(toolCall 含完整 name+args,text 含正文),
462
+ * 而非离散事件。renderResult compact 用此数据 + formatToolCall 生成与 nicobailon
463
+ * 一致的 `→ formatToolCall` 行格式。
464
+ *
465
+ * 派生规则(与 nicobailon getDisplayItems(messages) 等价):
466
+ * - 遍历 turns[],每个 turn:先 text(如果有),再 toolCalls
467
+ * - toolCall:{ type:"toolCall", name, args, status }
468
+ * - text:{ type:"text", text }(取首行,避免撑爆 compact)
469
+ * - 跳过 thinking(与 nicobailon 一致,不在 compact 展示推理)
470
+ *
471
+ * 参数类型放宽为 `{ turns: readonly Turn[] }` 结构子集——ExecutionRecord 和
472
+ * ReconstructedRecord 都满足,磁盘重建路径(record-store)可复用此函数派生
473
+ * displayItems,而非给空数组(否则终态 record 详情看不到 text)。
474
+ */
475
+ export function getDisplayItems(record: { turns: readonly Turn[] }): DisplayItem[] {
476
+ const items: DisplayItem[] = [];
477
+ for (const turn of record.turns) {
478
+ // assistant 正文(与 nicobailon 顺序一致:先 text 后 toolCall)
479
+ if (turn.text.length > 0) {
480
+ items.push({ type: "text", text: turn.text });
481
+ }
482
+ for (const tc of turn.toolCalls) {
483
+ items.push({
484
+ type: "toolCall",
485
+ name: tc.toolName,
486
+ args: (tc.args ?? {}) as Record<string, unknown>,
487
+ status: tc._status,
488
+ });
489
+ }
490
+ }
491
+ return items;
492
+ }
493
+
494
+ /**
495
+ * 从 turns[] 末尾推导当前活动行(running 时)。
496
+ *
497
+ * 优先级:最后一个未闭合 turn 的末尾 running toolCall → thinking → text → undefined
498
+ *
499
+ * 仅 status==="running" 时返回;terminal 态返回 undefined。
500
+ *
501
+ * 注意:返回的 type 联合("tool"|"text"|"thinking")是手写的,未通过类型守卫从
502
+ * AgentEvent 派生——它映射的是累积的 turn 状态(InternalToolCall._status + turn.thinking/text),
503
+ * 而非单个事件。若未来新增 turn 内容模式(如 reasoning_summary),须同步扩展本函数,
504
+ * 否则会静默返回 undefined(活动行运行中途消失)。updateFromEvent 的 switch 有 never 穷尽
505
+ * 检查,但本函数没有,依赖人工同步。
506
+ */
507
+ export function getCurrentActivity(
508
+ record: ExecutionRecord,
509
+ ): { type: "tool" | "text" | "thinking"; label: string } | undefined {
510
+ if (record.status !== "running") return undefined;
511
+ const turn = record.turns[record.turns.length - 1];
512
+ if (turn === undefined || turn.closed) return undefined;
513
+
514
+ // 1. 倒序找最后一个 running 的 toolCall
515
+ for (let i = turn.toolCalls.length - 1; i >= 0; i--) {
516
+ const tc = turn.toolCalls[i];
517
+ if (tc?._status === "running") {
518
+ return { type: "tool", label: extractLabelFromArgs(tc.toolName, tc.args) };
519
+ }
520
+ }
521
+ // 2. 正在 thinking
522
+ if (turn.thinking) {
523
+ return { type: "thinking", label: turn.thinking.slice(0, ACTIVITY_LABEL_MAX) };
524
+ }
525
+ // 3. 正在输出 text
526
+ if (turn.text) {
527
+ return { type: "text", label: turn.text.slice(0, ACTIVITY_LABEL_MAX) };
528
+ }
529
+ return undefined;
530
+ }
531
+
532
+ /**
533
+ * 聚合所有 turn 的 text 为完整文本(替代旧 collectResponseText)。
534
+ *
535
+ * 单一数据源:不再读 session.messages,text 完全来自 record.turns[] 的流式累积。
536
+ * 多 turn 用空行分隔(每个 turn 是一段独立的 assistant 输出)。
537
+ *
538
+ * 语义对齐旧 collectResponseText:后者只取最后一条 assistant message 的 text。
539
+ * turns[] 收口后,每条 assistant message 对应一个 turn,故 join 所有非空 turn 文本
540
+ * 与「拼接所有 assistant message」语义一致。单 turn 场景两者完全等价。
541
+ */
542
+ export function getFullText(record: ExecutionRecord): string {
543
+ return record.turns
544
+ .map((t) => t.text)
545
+ .filter((text) => text.length > 0)
546
+ .join("\n\n");
547
+ }
548
+
549
+ /**
550
+ * [增量通知] 从 fromTurnIndex 起聚合 turn 文本(getFullText 的增量视图)。
551
+ *
552
+ * 语义:record.turns.slice(Math.max(0, fromTurnIndex)) 的 text 数组
553
+ * filter(text => text.length > 0)(判据与 getFullText 逐字对齐——空白串 " " 按非空
554
+ * 处理,不发散)后 join("\n\n")。
555
+ *
556
+ * 不变式(单测锁死):
557
+ * - fromTurnIndex = 0 与 getFullText(record) 逐字节等价(one-shot/首轮回退零成本)
558
+ * - fromTurnIndex >= turns.length 返回 ""(不抛错、不回绕)
559
+ * - fromTurnIndex < 0 规范化为 0(防御,Math.max 不产生 slice 负偏移语义)
560
+ *
561
+ * 纯函数只读;getFullText 本体与既有调用方(collectResult 等)零改动。
562
+ * 唯一调用点 onRoundSettled 的增量派生(record.roundBaseTurnIndex ?? 0)。
563
+ */
564
+ export function getFullTextFrom(record: ExecutionRecord, fromTurnIndex: number): string {
565
+ return record.turns
566
+ .slice(Math.max(0, fromTurnIndex))
567
+ .map((t) => t.text)
568
+ .filter((text) => text.length > 0)
569
+ .join("\n\n");
570
+ }
571
+
572
+ /**
573
+ * [增量通知] 轮次边界推进公式(D1):返回下一轮增量的 turns[] 起始下标。
574
+ *
575
+ * record.turns.length - (末 turn 存在且 !closed 且 text.length === 0 ? 1 : 0)
576
+ *
577
+ * - 末 turn 已闭合(pi 现序常态:带 usage 的 message_end 恒先于 turn_end,settle 时
578
+ * 全闭合)→ 返回 length,下一轮从新 turn 起。
579
+ * - 末 turn 是滞后 message_end 开出的空 turn(防御分支,pi 现序不可达)→ 不计入边界
580
+ * (-1),留在下一轮增量内:新轮首个 text_delta 经 currentTurn 复用该空 turn,复用
581
+ * 累积被 slice(from) 覆盖;若直用 turns.length 会把这段文本挤出 slice 范围静默丢失(D1)。
582
+ * - 末 turn 未闭合且 text 非空(防御形态,pi 现序不可达)→ 计入本轮返回 length(该形态
583
+ * 即 onRoundSettled 推进前观测哨 logger.warn 的触发条件)。
584
+ *
585
+ * 纯函数只读不写 record;唯一调用点 onRoundSettled 第 5 步(notify 之后推进)。
586
+ */
587
+ export function nextRoundBaseTurnIndex(record: ExecutionRecord): number {
588
+ const last = record.turns[record.turns.length - 1];
589
+ const trailingOpenEmpty =
590
+ last !== undefined && !last.closed && last.text.length === 0 ? 1 : 0;
591
+ return record.turns.length - trailingOpenEmpty;
592
+ }
593
+
594
+ /**
595
+ * 聚合所有 turn 的 toolCalls(扁平化),并 strip InternalToolCall 的内部字段。
596
+ * 供 collectResult / schema enforcement 读,替代旧闭包 toolCalls 旁路。
597
+ *
598
+ * 返回 ToolCall[](不含 _status / startedTs)——跨边界导出形状清洁,
599
+ * 避免内部状态机字段泄漏到 AgentResult.toolCalls / 持久化层。
600
+ */
601
+ export function getAllToolCalls(record: ExecutionRecord): ToolCall[] {
602
+ return record.turns.flatMap((t) => t.toolCalls.map(stripInternal));
603
+ }
604
+
605
+ /**
606
+ * 聚合所有 turn 的 toolCalls 总数(免克隆计数)。
607
+ *
608
+ * getAllToolCalls 会 flatMap + strip 克隆出完整数组——只需计数的调用方(渲染签名
609
+ * 每 200ms tick 调用一次)用它是纯浪费;本函数 reduce 累加各 turn 的 length,零分配。
610
+ * 与 getAllToolCalls(...).length 恒等(同一 turns 源)。
611
+ */
612
+ export function countAllToolCalls(record: ExecutionRecord): number {
613
+ return record.turns.reduce((sum, t) => sum + t.toolCalls.length, 0);
614
+ }
615
+
616
+ /** 把 InternalToolCall 映射回纯净的 ToolCall(丢弃 _status / startedTs)。 */
617
+ function stripInternal(tc: InternalToolCall): ToolCall {
618
+ return {
619
+ toolName: tc.toolName,
620
+ args: tc.args,
621
+ result: tc.result,
622
+ isError: tc.isError,
623
+ };
624
+ }
625
+
626
+ /**
627
+ * 聚合所有 turn 的 usageDelta 为完整 usage(含 total + cost)。
628
+ * 全零则返回 undefined(与旧 toUsageTotal 语义一致)。
629
+ *
630
+ * cost 来自 SdkEvent.message.usage.cost.total(message_end 时透传到 usageDelta)。
631
+ * 旧 toUsageTotal/session-runner 累积 cost;本重构保留该行为。
632
+ */
633
+ export function getTotalUsage(record: ExecutionRecord): AgentUsageTotal | undefined {
634
+ let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
635
+ for (const turn of record.turns) {
636
+ const u = turn.usageDelta;
637
+ if (u) {
638
+ input += u.input ?? 0;
639
+ output += u.output ?? 0;
640
+ cacheRead += u.cacheRead ?? 0;
641
+ cacheWrite += u.cacheWrite ?? 0;
642
+ cost += u.cost ?? 0;
643
+ }
644
+ }
645
+ const total = input + output + cacheRead + cacheWrite;
646
+ if (total === 0) return undefined;
647
+ return { input, output, cacheRead, cacheWrite, total, cost };
648
+ }
649
+
650
+ // ============================================================
651
+ // 完成(唯一入口)
652
+ // ============================================================
653
+
654
+ /**
655
+ * status 状态机的 CAS 互斥锁。仅当 `record.status === "running"` 时改为 target
656
+ * 并返回 true,否则返回 false。**status 状态机本身就是互斥锁**——终态
657
+ * (closed/cancelled)不可逆,check-then-set 在 JS 单线程事件循环里天然原子。
658
+ *
659
+ * 用途:executor 的收尾竞争。cancelBackground 与 background detached 完成回调
660
+ * 都调 tryTransition 抢锁:抢到负责完整收尾,没抢到闭嘴不做事。
661
+ *
662
+ * target 仅限正常执行流的两终态(closed + cancelled)。
663
+ * closed 携带 closedReason(L2 原因子枚举),由重建路径或正常执行流写入。
664
+ * 重建路径也可用 markReconstructedStatus 直接赋值(跳过 CAS)。
665
+ *
666
+ * @param closedReason closed 终态的 L2 关闭原因。仅 target="closed" 时有意义;
667
+ * target="cancelled" 时忽略。缺省 "gc"(通用完成/失败)。
668
+ */
669
+ export function tryTransition(
670
+ record: ExecutionRecord,
671
+ target: "closed",
672
+ closedReason?: ClosedReason,
673
+ ): boolean {
674
+ if (record.status !== "running") return false;
675
+ record.status = target;
676
+ record.closedReason = closedReason ?? "gc";
677
+ return true;
678
+ }
679
+
680
+ /**
681
+ * [v8.5 D] 透明重生唯一回边:closed → running,清除终态语义位。
682
+ *
683
+ * 与 tryTransition 单向语义的关系:tryTransition 只负责 running→closed 终化且不可逆;
684
+ * 本函数是刻意豁免的逆向特例——仅服务 message action 对可重连记录(finalizedReason
685
+ * ∈ RECONNECTABLE_FINAL_REASONS)的同 id 重生。调用方必须先通过四守卫(sessionFile
686
+ * 存在 / 非 tombstone / 可重连 reason / worktree+alive 探针),禁止在正常执行流程中
687
+ * 使用。running 入态防御性 no-op:重生是 closed 的专属回边,不是万能改写器。
688
+ */
689
+ export function resurrectClosed(
690
+ record: { status: ExecutionStatus; closedReason?: ClosedReason; endedAt?: number },
691
+ ): boolean {
692
+ if (record.status !== "closed") return false;
693
+ record.status = "running";
694
+ record.closedReason = undefined;
695
+ record.endedAt = undefined;
696
+ return true;
697
+ }
698
+
699
+ /**
700
+ * 重建专用收口:跳过 CAS 直接赋值 status。
701
+ *
702
+ * 仅用于 session-reconstructor 从 session.jsonl 重建终态 record 时——
703
+ * 重建的 record 没有 running 状态需要保护,直接赋值即可。
704
+ * 禁止在正常执行流程中使用此函数(应使用 tryTransition)。
705
+ */
706
+ export function markReconstructedStatus(
707
+ record: { status: ExecutionStatus },
708
+ status: ExecutionStatus,
709
+ ): void {
710
+ record.status = status;
711
+ }
712
+
713
+ /**
714
+ * 唯一完成入口。冻结状态(写 endedAt/agentResult/result/error/outcome)。
715
+ * 不修改 turns/totalTokens——已由 updateFromEvent 累积,completeRecord 只读不重置。
716
+ *
717
+ * ⚠ 前置条件:调用方必须先通过 tryTransition 抢到锁(status 已被 CAS 设为 target)。
718
+ *
719
+ * @param closedReason closed 终态的 L2 关闭原因。仅 status="closed" 时写入;
720
+ * "cancelled" 时忽略。
721
+ */
722
+ export function completeRecord(
723
+ record: ExecutionRecord,
724
+ result: AgentResult,
725
+ status: "closed",
726
+ closedReason?: ClosedReason,
727
+ ): void {
728
+ record.status = status;
729
+ record.closedReason = closedReason ?? "gc";
730
+ // U3 C-outcome:outcome 唯一写入点——终态语义在此一次定形(D6),下游消费方
731
+ // (project/list/notify 文案/渲染器)只读 record.outcome,不再各自推导。
732
+ record.outcome = deriveOutcome(record.closedReason, result.error);
733
+ record.endedAt = Date.now();
734
+ record.agentResult = result;
735
+ record.result = result.text;
736
+ record.error = result.error;
737
+ }
738
+
739
+ // ============================================================
740
+ // 终态 outcome(U3 C-outcome:单一权威派生)
741
+ // ============================================================
742
+
743
+ /**
744
+ * closed 终态 → 三态 outcome 的唯一权威派生(D6 收敛:原 notifier/bg-notify-render/
745
+ * shared deriveClosedDisplay 三处手写同构 switch 的单一实现)。
746
+ *
747
+ * 判定顺序(顺序敏感,勿回退成「error 有值即 failed」的无视取消规则):
748
+ * 1. closedReason === "cancelled" → "cancelled"(取消优先,不参与 error——abort 合成
749
+ * result 可能携带 error,但用户取消语义优先)
750
+ * 2. error 非空(truthy,与旧三处同构的 `record.error &&` 判定逐字对齐——空串 error
751
+ * 不构成失败)→ "failed"
752
+ * 3. 其余 → "completed"
753
+ *
754
+ * [D6 待核项保真] 「failed 优先于 patchFile 提示」:失败轮也会写 patchFile
755
+ * (doFinalizeRecord Step 0 对 worktreeHandle 无条件 collectPatch),消费方必须先按
756
+ * outcome 分流再渲染 patch 提示——failed 分支不展示 patch/result。历史 bug:notifier
757
+ * 的 patchFile 分支曾遮蔽 gc+error 判定,失败终态被 LLM 告知 completed(M1 修复存档)。
758
+ *
759
+ * [D6 显式取舍] parent-shutdown/parent-fork/parent-new 合成关闭(subagent-service
760
+ * disposeAllRecords 合成 result 恒写 error:"closed due to ${reason}")在本映射下落
761
+ * "failed"——语义为「父进程关闭时子 agent 未完成即失败」,选定行为而非疏漏,
762
+ * 勿当 bug 改回 cancelled 造成派生矛盾。
763
+ *
764
+ * 唯一写点 completeRecord 调用本函数冻结 record.outcome;通知 payload(notifier 投影
765
+ * 边界)与无 outcome 字段的存量/重建 record 由 projectOutcome 兜底复用本函数。
766
+ */
767
+ export function deriveOutcome(
768
+ closedReason: ClosedReason | undefined,
769
+ error: string | undefined | null,
770
+ ): ExecutionOutcome {
771
+ if (closedReason === "cancelled") return "cancelled";
772
+ if (error) return "failed";
773
+ return "completed";
774
+ }
775
+
776
+ /**
777
+ * 投影层 outcome 唯一出口:running → undefined(终态语义不适用活跃态);closed →
778
+ * 一等 outcome 字段直读优先,字段缺失(存量/磁盘重建 record——outcome 持久化不在
779
+ * U3 领地内)时回退 deriveOutcome(closedReason, error) 兜底——单一权威函数,
780
+ * 消费方零手写推导。返回值联合含 "closed-legacy" 预留态,消费方必须处理。
781
+ */
782
+ export function projectOutcome(record: {
783
+ status: ExecutionStatus;
784
+ outcome?: ExecutionOutcome;
785
+ closedReason?: ClosedReason;
786
+ error?: string;
787
+ }): ProjectedOutcome | undefined {
788
+ if (record.status !== "closed") return undefined;
789
+ return record.outcome ?? deriveOutcome(record.closedReason, record.error);
790
+ }
791
+
792
+ // ============================================================
793
+ // 投影(唯一 → Details / Snapshot / Persisted)
794
+ // ============================================================
795
+
796
+ /** elapsedSeconds 唯一计算点(共享 helper,消除三处发散)。endedAt 缺失用 Date.now()。 */
797
+ export function computeElapsedSeconds(record: { startedAt: number; endedAt?: number }): number {
798
+ const end = record.endedAt ?? Date.now();
799
+ return Math.floor((end - record.startedAt) / MS_PER_SECOND);
800
+ }
801
+
802
+ /**
803
+ * 投影到 SubagentToolDetails。elapsedSeconds/currentActivity/eventLog 均现算派生。
804
+ */
805
+ export function project(record: ExecutionRecord): SubagentToolDetails {
806
+ return {
807
+ status: record.status,
808
+ outcome: projectOutcome(record),
809
+ mode: record.mode,
810
+ agent: record.agent,
811
+ model: record.model,
812
+ thinkingLevel: record.thinkingLevel,
813
+ slug: record.slug,
814
+ turns: record.turnCount,
815
+ totalTokens: record.totalTokens,
816
+ elapsedSeconds: computeElapsedSeconds(record),
817
+ eventLog: getEventLog(record),
818
+ displayItems: getDisplayItems(record),
819
+ result: record.result,
820
+ error: record.error,
821
+ currentActivity: getCurrentActivity(record),
822
+ parsedOutput: record.agentResult?.parsedOutput,
823
+ sessionFile: record.sessionFile,
824
+ patchFile: record.patchFile,
825
+ };
826
+ }
827
+
828
+ /**
829
+ * 投影到 live 进度快照。elapsedSeconds/currentActivity/eventLog 均现算派生。
830
+ * 供 WorkflowsView 在 agent 运行期间读取实时进度。
831
+ */
832
+ export function projectLiveProgress(record: ExecutionRecord): {
833
+ status: ExecutionRecord["status"];
834
+ turns: number;
835
+ totalTokens: number;
836
+ elapsedSeconds: number;
837
+ eventLog: AgentEventLogEntry[];
838
+ currentActivity: ReturnType<typeof getCurrentActivity>;
839
+ lastError: string | undefined;
840
+ } {
841
+ return {
842
+ status: record.status,
843
+ turns: record.turnCount,
844
+ totalTokens: record.totalTokens,
845
+ elapsedSeconds: computeElapsedSeconds(record),
846
+ eventLog: getEventLog(record),
847
+ currentActivity: getCurrentActivity(record),
848
+ lastError: record.lastError,
849
+ };
850
+ }
851
+
852
+ /**
853
+ * 投影到只读快照(TUI list / poll 消费)。
854
+ * 浅拷贝 turns[],字段标 readonly 阻止 TUI 回写。
855
+ */
856
+ export function snapshot(record: ExecutionRecord): RecordSnapshot {
857
+ return {
858
+ id: record.id,
859
+ agent: record.agent,
860
+ model: record.model,
861
+ thinkingLevel: record.thinkingLevel,
862
+ mode: record.mode,
863
+ task: record.task,
864
+ slug: record.slug,
865
+ status: record.status,
866
+ chatMode: record.chatMode,
867
+ turns: record.turnCount,
868
+ totalTokens: record.totalTokens,
869
+ startedAt: record.startedAt,
870
+ endedAt: record.endedAt,
871
+ result: record.result,
872
+ error: record.error,
873
+ sessionFile: record.sessionFile,
874
+ };
875
+ }
876
+
877
+ // ============================================================
878
+ // JSONL → AgentEvent 翻译(从 live/jsonl-to-agent-event 迁入)
879
+ // ============================================================
880
+
881
+ /** subprocess JSONL 事件(JSON.parse 结果)。duck-typed,对应 SDK SdkEvent。 */
882
+ type JsonlEvent = Record<string, unknown>;
883
+
884
+ /**
885
+ * 把一条 JSONL 事件翻译成 AgentEvent。
886
+ *
887
+ * 返回 undefined 表示该事件不映射到任何 AgentEvent(如 session header、message_start、
888
+ * tool_execution_update),调用方应跳过。
889
+ *
890
+ * 一个 JSONL 事件可能产出**多条** AgentEvent(message_end 的 usage + error 各一条),
891
+ * 故返回数组。绝大多数情况长度为 0 或 1;message_end 最多 2 条。
892
+ */
893
+ export function jsonlToAgentEvent(raw: JsonlEvent): AgentEvent[] {
894
+ const type = raw.type;
895
+
896
+ switch (type) {
897
+ case "session":
898
+ case "message_start":
899
+ case "turn_start":
900
+ case "tool_execution_update":
901
+ return [];
902
+
903
+ case "tool_execution_start": {
904
+ const toolName = typeof raw.toolName === "string" ? raw.toolName : "";
905
+ return [{ type: "tool_start", toolName, args: raw.args }];
906
+ }
907
+
908
+ case "tool_execution_end": {
909
+ const toolName = typeof raw.toolName === "string" ? raw.toolName : "";
910
+ const isError = raw.isError === true;
911
+ return [{
912
+ type: "tool_end",
913
+ toolName,
914
+ args: raw.args,
915
+ result: raw.result as ToolCallResult | undefined,
916
+ isError,
917
+ }];
918
+ }
919
+
920
+ case "message_update": {
921
+ const ame = raw.assistantMessageEvent as Record<string, unknown> | undefined;
922
+ if (ame?.type === "thinking_delta") {
923
+ const delta = typeof ame.delta === "string" ? ame.delta : "";
924
+ return [{ type: "thinking_delta", delta }];
925
+ }
926
+ if (ame !== undefined && ame.delta !== undefined) {
927
+ const delta = typeof ame.delta === "string" ? ame.delta : String(ame.delta);
928
+ return [{ type: "text_delta", delta }];
929
+ }
930
+ return [];
931
+ }
932
+
933
+ case "turn_end": {
934
+ return [{ type: "turn_end" }];
935
+ }
936
+
937
+ case "message_end": {
938
+ return accumulateMessageEndForRecord(raw);
939
+ }
940
+
941
+ case "compaction_start": {
942
+ return [{ type: "compaction" }];
943
+ }
944
+
945
+ default:
946
+ return [];
947
+ }
948
+ }
949
+
950
+ /** message_end 翻译:usage 拍平 + stopReason=error/aborted 额外产 error 事件。 */
951
+ function accumulateMessageEndForRecord(raw: JsonlEvent): AgentEvent[] {
952
+ const events: AgentEvent[] = [];
953
+ const msg = raw.message as Record<string, unknown> | undefined;
954
+ const usageRaw = (typeof msg?.usage === "object" && msg.usage !== null)
955
+ ? msg.usage as Record<string, unknown>
956
+ : undefined;
957
+
958
+ if (usageRaw) {
959
+ const costObj = (typeof usageRaw.cost === "object" && usageRaw.cost !== null)
960
+ ? usageRaw.cost as Record<string, unknown>
961
+ : undefined;
962
+ // MF-3 fix: 显式提取字段 + Number.isFinite 守卫,不使用 spread + as 断言
963
+ const numOrZero = (v: unknown): number =>
964
+ typeof v === "number" && Number.isFinite(v) ? v : 0;
965
+ const usage: AgentUsage = {
966
+ input: numOrZero(usageRaw.input),
967
+ output: numOrZero(usageRaw.output),
968
+ cacheRead: numOrZero(usageRaw.cacheRead),
969
+ cacheWrite: numOrZero(usageRaw.cacheWrite),
970
+ cost: typeof costObj?.total === "number" ? costObj.total : undefined,
971
+ };
972
+ events.push({ type: "message_end", usage });
973
+ }
974
+
975
+ const stopReason = msg?.stopReason;
976
+ if (stopReason === "error" || stopReason === "aborted") {
977
+ const errorMessage = typeof msg?.errorMessage === "string"
978
+ ? msg.errorMessage
979
+ : (typeof raw.reason === "string" ? raw.reason : String(stopReason));
980
+ events.push({ type: "error", message: errorMessage });
981
+ }
982
+
983
+ return events;
984
+ }