@zhushanwen/pi-subagent-workflow 8.5.0 → 8.7.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 (336) hide show
  1. package/package.json +13 -16
  2. package/relay/relay.mjs +1 -1
  3. package/src/host/__tests__/pi-host.test.ts +214 -0
  4. package/src/host/pi-host.ts +109 -0
  5. package/src/index.ts +142 -32
  6. package/src/injectors/__tests__/engine-awareness.test.ts +292 -0
  7. package/src/injectors/__tests__/engine-section-stability.test.ts +341 -0
  8. package/src/injectors/__tests__/subagent-list-injector.test.ts +5 -5
  9. package/src/injectors/__tests__/workflow-list-injector.test.ts +5 -5
  10. package/src/injectors/engine-awareness.ts +123 -0
  11. package/src/injectors/model-list-injector.ts +1 -1
  12. package/src/injectors/subagent-list-injector.ts +8 -8
  13. package/src/injectors/workflow-list-injector.ts +8 -8
  14. package/src/interface/__tests__/detectors.test.ts +3 -3
  15. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
  16. package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
  17. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +2 -2
  18. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
  19. package/src/interface/bg-notify-render.ts +34 -13
  20. package/src/interface/commands.ts +3 -3
  21. package/src/interface/format.ts +2 -2
  22. package/src/interface/helpers.ts +3 -3
  23. package/src/interface/list-component.ts +4 -4
  24. package/src/interface/list-shared.ts +2 -2
  25. package/src/interface/list-view.ts +2 -2
  26. package/src/interface/subagent-actions.ts +288 -19
  27. package/src/interface/subagent-tool-schema.ts +163 -0
  28. package/src/interface/subagent-tool.ts +65 -129
  29. package/src/interface/subagents.ts +5 -5
  30. package/src/interface/tool-render.ts +34 -2
  31. package/src/interface/tool-workflow-script.ts +4 -4
  32. package/src/interface/tool-workflow.ts +6 -6
  33. package/src/interface/views/WorkflowsView.ts +5 -5
  34. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  35. package/src/interface/views/__tests__/detail-content-session-file.test.ts +2 -2
  36. package/src/interface/views/detail-content.ts +4 -4
  37. package/src/interface/views/format.ts +4 -4
  38. package/src/{orchestration/jsonl-run-store.ts → jsonl-run-store.ts} +132 -18
  39. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +0 -53
  40. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +0 -1
  41. package/src/execution/__tests__/agent-registry.test.ts +0 -363
  42. package/src/execution/__tests__/agent-result-mapper.test.ts +0 -150
  43. package/src/execution/__tests__/alive-store.test.ts +0 -147
  44. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  45. package/src/execution/__tests__/before-agent-start-injection.test.ts +0 -132
  46. package/src/execution/__tests__/bg-notify-render.test.ts +0 -256
  47. package/src/execution/__tests__/channel-registry-handshake.test.ts +0 -243
  48. package/src/execution/__tests__/chat-engine-routing.test.ts +0 -597
  49. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +0 -365
  50. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -190
  51. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +0 -215
  52. package/src/execution/__tests__/concurrency-pool.test.ts +0 -250
  53. package/src/execution/__tests__/config.test.ts +0 -110
  54. package/src/execution/__tests__/conversation-wiring.test.ts +0 -198
  55. package/src/execution/__tests__/crash-recovery.test.ts +0 -320
  56. package/src/execution/__tests__/delivery-methods.test.ts +0 -385
  57. package/src/execution/__tests__/dialog-queue.test.ts +0 -299
  58. package/src/execution/__tests__/epipe-fallback.test.ts +0 -241
  59. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -243
  60. package/src/execution/__tests__/execute-nesting.test.ts +0 -323
  61. package/src/execution/__tests__/execute-options-mapper.test.ts +0 -189
  62. package/src/execution/__tests__/execution-record.test.ts +0 -1284
  63. package/src/execution/__tests__/finalize-record.test.ts +0 -368
  64. package/src/execution/__tests__/finalized-marker.test.ts +0 -82
  65. package/src/execution/__tests__/format-schema-instruction.test.ts +0 -138
  66. package/src/execution/__tests__/format.test.ts +0 -458
  67. package/src/execution/__tests__/gc-timer.test.ts +0 -184
  68. package/src/execution/__tests__/get-record-for-action-restart.test.ts +0 -254
  69. package/src/execution/__tests__/gui-mode-dispatch.test.ts +0 -59
  70. package/src/execution/__tests__/helpers/mock-extension-api.ts +0 -30
  71. package/src/execution/__tests__/helpers/spawn-mock.ts +0 -238
  72. package/src/execution/__tests__/host-mode.test.ts +0 -87
  73. package/src/execution/__tests__/index-session-start-identity.test.ts +0 -371
  74. package/src/execution/__tests__/index-session-start.test.ts +0 -593
  75. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  76. package/src/execution/__tests__/lifecycle-manager.test.ts +0 -337
  77. package/src/execution/__tests__/lifecycle-predicates.test.ts +0 -116
  78. package/src/execution/__tests__/list-component.test.ts +0 -401
  79. package/src/execution/__tests__/list-fields.test.ts +0 -109
  80. package/src/execution/__tests__/manifest-parentid.test.ts +0 -117
  81. package/src/execution/__tests__/model-resolver.test.ts +0 -413
  82. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +0 -287
  83. package/src/execution/__tests__/nested-visibility.test.ts +0 -325
  84. package/src/execution/__tests__/notifier-flush.test.ts +0 -574
  85. package/src/execution/__tests__/notifier-golden-snapshot.test.ts +0 -261
  86. package/src/execution/__tests__/one-shot-upgrade.test.ts +0 -205
  87. package/src/execution/__tests__/output-collector.test.ts +0 -61
  88. package/src/execution/__tests__/parent-child-matrix.test.ts +0 -336
  89. package/src/execution/__tests__/path-encoding.test.ts +0 -104
  90. package/src/execution/__tests__/pi-invocation.test.ts +0 -134
  91. package/src/execution/__tests__/record-store.test.ts +0 -1057
  92. package/src/execution/__tests__/records-cwd-isolation.test.ts +0 -91
  93. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +0 -339
  94. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -273
  95. package/src/execution/__tests__/relay-agent.test.ts +0 -448
  96. package/src/execution/__tests__/relay-env.test.ts +0 -42
  97. package/src/execution/__tests__/resource-policy.test.ts +0 -109
  98. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  99. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -267
  100. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -253
  101. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -644
  102. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  103. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  104. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  105. package/src/execution/__tests__/sdk-contract.test.ts +0 -273
  106. package/src/execution/__tests__/session-context-resolver.test.ts +0 -167
  107. package/src/execution/__tests__/session-file-gc.test.ts +0 -293
  108. package/src/execution/__tests__/session-pending.test.ts +0 -197
  109. package/src/execution/__tests__/session-reconstructor.test.ts +0 -379
  110. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -178
  111. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -311
  112. package/src/execution/__tests__/session-start-reaper.test.ts +0 -233
  113. package/src/execution/__tests__/spawn-args.test.ts +0 -434
  114. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -189
  115. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  116. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -207
  117. package/src/execution/__tests__/spawned-children.test.ts +0 -92
  118. package/src/execution/__tests__/startup-config-declaration.test.ts +0 -35
  119. package/src/execution/__tests__/status-refactor.test.ts +0 -345
  120. package/src/execution/__tests__/stdin-writer.test.ts +0 -463
  121. package/src/execution/__tests__/stream-sink-retirement.test.ts +0 -261
  122. package/src/execution/__tests__/subagent-service-abort.test.ts +0 -60
  123. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -629
  124. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +0 -180
  125. package/src/execution/__tests__/subagent-service.test.ts +0 -788
  126. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -310
  127. package/src/execution/__tests__/subprocess-agent-runner.test.ts +0 -506
  128. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  129. package/src/execution/__tests__/timeout-integration.test.ts +0 -397
  130. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  131. package/src/execution/__tests__/tool-action.test.ts +0 -385
  132. package/src/execution/__tests__/truncline-snapshot.test.ts +0 -81
  133. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  134. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  135. package/src/execution/__tests__/ui-channels.test.ts +0 -187
  136. package/src/execution/__tests__/ui-interaction-model.test.ts +0 -67
  137. package/src/execution/__tests__/ui-request-handler-factory.test.ts +0 -370
  138. package/src/execution/__tests__/ui-request-handler.test.ts +0 -204
  139. package/src/execution/__tests__/ui-request-observability.test.ts +0 -114
  140. package/src/execution/__tests__/ui-request-queue.test.ts +0 -133
  141. package/src/execution/__tests__/worktree-manager.test.ts +0 -633
  142. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -229
  143. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +0 -181
  144. package/src/execution/__tests__/worktree-registry.test.ts +0 -199
  145. package/src/execution/agent-registry.ts +0 -193
  146. package/src/execution/agent-result-mapper.ts +0 -90
  147. package/src/execution/alive-store.ts +0 -92
  148. package/src/execution/argv-mirror.ts +0 -109
  149. package/src/execution/best-effort.ts +0 -38
  150. package/src/execution/channel-registry-access.ts +0 -144
  151. package/src/execution/concurrency-pool.ts +0 -116
  152. package/src/execution/config.ts +0 -96
  153. package/src/execution/dialog-queue.ts +0 -330
  154. package/src/execution/engine/__tests__/common/data-dir.test.ts +0 -53
  155. package/src/execution/engine/__tests__/common/errors.test.ts +0 -132
  156. package/src/execution/engine/__tests__/common/event-journal.test.ts +0 -177
  157. package/src/execution/engine/__tests__/common/kill-chain.test.ts +0 -192
  158. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +0 -81
  159. package/src/execution/engine/__tests__/common/persona-router.test.ts +0 -123
  160. package/src/execution/engine/__tests__/common/pool-manager.test.ts +0 -154
  161. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  162. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  163. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +0 -141
  164. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +0 -109
  165. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +0 -101
  166. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +0 -77
  167. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +0 -104
  168. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +0 -342
  169. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +0 -201
  170. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +0 -76
  171. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +0 -79
  172. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -87
  173. package/src/execution/engine/__tests__/engines-declaration.test.ts +0 -36
  174. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -85
  175. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  176. package/src/execution/engine/__tests__/registry.test.ts +0 -120
  177. package/src/execution/engine/__tests__/routing.test.ts +0 -231
  178. package/src/execution/engine/common/data-dir.ts +0 -62
  179. package/src/execution/engine/common/errors.ts +0 -183
  180. package/src/execution/engine/common/event-journal.ts +0 -254
  181. package/src/execution/engine/common/journal-replay.ts +0 -62
  182. package/src/execution/engine/common/kill-chain.ts +0 -221
  183. package/src/execution/engine/common/nesting-guard.ts +0 -50
  184. package/src/execution/engine/common/persona-router.ts +0 -108
  185. package/src/execution/engine/common/pool-manager.ts +0 -226
  186. package/src/execution/engine/common/schema-emulation.ts +0 -189
  187. package/src/execution/engine/common/session-view-projection.ts +0 -51
  188. package/src/execution/engine/engine-discovery.ts +0 -65
  189. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -469
  190. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  191. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
  192. package/src/execution/engine/engines/pi/pi-engine.ts +0 -415
  193. package/src/execution/engine/engines/pi/reader.ts +0 -48
  194. package/src/execution/engine/engines/pi/registration.ts +0 -35
  195. package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
  196. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
  197. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
  198. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -246
  199. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -228
  200. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  201. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -64
  202. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -127
  203. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -567
  204. package/src/execution/engine/engines/zcode/constants.ts +0 -43
  205. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -39
  206. package/src/execution/engine/engines/zcode/launcher.ts +0 -161
  207. package/src/execution/engine/engines/zcode/parser.ts +0 -436
  208. package/src/execution/engine/engines/zcode/preparer.ts +0 -363
  209. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  210. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  211. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -648
  212. package/src/execution/engine/host-task-spec.ts +0 -47
  213. package/src/execution/engine/model-prompt.ts +0 -59
  214. package/src/execution/engine/paths.ts +0 -42
  215. package/src/execution/engine/port.ts +0 -153
  216. package/src/execution/engine/registry.ts +0 -123
  217. package/src/execution/engine/routing.ts +0 -218
  218. package/src/execution/engine/types.ts +0 -304
  219. package/src/execution/execute-options-mapper.ts +0 -111
  220. package/src/execution/execution-record.ts +0 -906
  221. package/src/execution/finalize-record.ts +0 -251
  222. package/src/execution/finalized-marker.ts +0 -51
  223. package/src/execution/get-state-handshake.ts +0 -104
  224. package/src/execution/host-mode.ts +0 -56
  225. package/src/execution/idle-gc.ts +0 -47
  226. package/src/execution/lifecycle-manager.ts +0 -491
  227. package/src/execution/lifecycle-predicates.ts +0 -65
  228. package/src/execution/manifest-store.ts +0 -256
  229. package/src/execution/model-config-service.ts +0 -204
  230. package/src/execution/model-resolver.ts +0 -276
  231. package/src/execution/notifier.ts +0 -260
  232. package/src/execution/output-collector.ts +0 -88
  233. package/src/execution/path-encoding.ts +0 -60
  234. package/src/execution/pi-invocation.ts +0 -120
  235. package/src/execution/record-entry.ts +0 -132
  236. package/src/execution/record-store.ts +0 -1234
  237. package/src/execution/relay-env.ts +0 -37
  238. package/src/execution/session-context-resolver.ts +0 -64
  239. package/src/execution/session-file-gc.ts +0 -120
  240. package/src/execution/session-pending.ts +0 -164
  241. package/src/execution/session-reconstructor.ts +0 -678
  242. package/src/execution/session-runner.ts +0 -1548
  243. package/src/execution/sessions-index.ts +0 -304
  244. package/src/execution/spawn-event-adapter.ts +0 -363
  245. package/src/execution/stdin-writer.ts +0 -198
  246. package/src/execution/stream-sink.ts +0 -126
  247. package/src/execution/subagent-service.ts +0 -2119
  248. package/src/execution/subprocess-agent-runner.ts +0 -303
  249. package/src/execution/temp-prompt.ts +0 -62
  250. package/src/execution/tombstone-store.ts +0 -72
  251. package/src/execution/turn-limiter.ts +0 -102
  252. package/src/execution/types.ts +0 -910
  253. package/src/execution/ui-channels.ts +0 -216
  254. package/src/execution/ui-interaction-model.ts +0 -48
  255. package/src/execution/ui-request-handler-factory.ts +0 -212
  256. package/src/execution/ui-request-observability.ts +0 -81
  257. package/src/execution/ui-request-queue.ts +0 -174
  258. package/src/execution/worktree-manager.ts +0 -688
  259. package/src/execution/worktree-registry.ts +0 -222
  260. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +0 -336
  261. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -208
  262. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -157
  263. package/src/orchestration/__tests__/args-validator.test.ts +0 -143
  264. package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
  265. package/src/orchestration/__tests__/config-loader.test.ts +0 -503
  266. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +0 -724
  267. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +0 -405
  268. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +0 -56
  269. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +0 -166
  270. package/src/orchestration/__tests__/execute-agent-call.test.ts +0 -314
  271. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +0 -171
  272. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +0 -1030
  273. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -277
  274. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +0 -96
  275. package/src/orchestration/__tests__/lifecycle.test.ts +0 -625
  276. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +0 -1971
  277. package/src/orchestration/__tests__/script-lint.test.ts +0 -831
  278. package/src/orchestration/__tests__/skill-discovery.test.ts +0 -157
  279. package/src/orchestration/__tests__/test-mocks.ts +0 -197
  280. package/src/orchestration/__tests__/worker-host.test.ts +0 -120
  281. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +0 -164
  282. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +0 -520
  283. package/src/orchestration/__tests__/worker-script-builder.test.ts +0 -309
  284. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +0 -110
  285. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -317
  286. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +0 -110
  287. package/src/orchestration/__tests__/workflows-e2e.test.ts +0 -537
  288. package/src/orchestration/agent-opts-resolver.ts +0 -93
  289. package/src/orchestration/args-validator.ts +0 -127
  290. package/src/orchestration/concurrency-gate.ts +0 -69
  291. package/src/orchestration/config-loader.ts +0 -315
  292. package/src/orchestration/error-recovery.ts +0 -862
  293. package/src/orchestration/execute-agent-call.ts +0 -217
  294. package/src/orchestration/launcher.ts +0 -410
  295. package/src/orchestration/lifecycle.ts +0 -396
  296. package/src/orchestration/models/__tests__/budget.test.ts +0 -367
  297. package/src/orchestration/models/__tests__/trace.test.ts +0 -408
  298. package/src/orchestration/models/agent-call.ts +0 -83
  299. package/src/orchestration/models/budget.ts +0 -148
  300. package/src/orchestration/models/ports.ts +0 -164
  301. package/src/orchestration/models/run-runtime.ts +0 -89
  302. package/src/orchestration/models/run-spec.ts +0 -83
  303. package/src/orchestration/models/run-state.ts +0 -44
  304. package/src/orchestration/models/trace.ts +0 -183
  305. package/src/orchestration/models/types.ts +0 -292
  306. package/src/orchestration/models/workflow-run.ts +0 -254
  307. package/src/orchestration/models/workflow-script-registry.ts +0 -35
  308. package/src/orchestration/models/workflow-script.ts +0 -117
  309. package/src/orchestration/script-lint.ts +0 -766
  310. package/src/orchestration/skill-discovery.ts +0 -92
  311. package/src/orchestration/worker-handle.ts +0 -115
  312. package/src/orchestration/worker-host.ts +0 -98
  313. package/src/orchestration/worker-script-builder.ts +0 -408
  314. package/src/orchestration/workflow-files.ts +0 -85
  315. package/src/orchestration/workflow-script-registry-impl.ts +0 -133
  316. package/src/shared/__tests__/agent-ref.test.ts +0 -34
  317. package/src/shared/__tests__/meta-parser.test.ts +0 -304
  318. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -280
  319. package/src/shared/__tests__/resource-discovery.test.ts +0 -736
  320. package/src/shared/__tests__/resource-meta.test.ts +0 -51
  321. package/src/shared/__tests__/schema-jsonify.test.ts +0 -81
  322. package/src/shared/agent-event.ts +0 -13
  323. package/src/shared/agent-ref.ts +0 -57
  324. package/src/shared/meta-parser.ts +0 -261
  325. package/src/shared/resource-discovery.ts +0 -811
  326. package/src/shared/resource-meta.ts +0 -65
  327. package/src/shared/schema-jsonify.ts +0 -56
  328. package/src/shared/xml-injection.ts +0 -35
  329. package/workflows/README.md +0 -81
  330. package/workflows/_shared/agent-refs.cjs +0 -40
  331. package/workflows/chain.js +0 -131
  332. package/workflows/map-reduce.js +0 -174
  333. package/workflows/parallel.js +0 -148
  334. package/workflows/review-fix-loop-utils.cjs +0 -1307
  335. package/workflows/review-fix-loop.js +0 -1396
  336. package/workflows/scatter-gather.js +0 -178
@@ -1,1284 +0,0 @@
1
- // src/__tests__/execution-record.test.ts
2
- import { describe, expect, it } from "vitest";
3
-
4
- import {
5
- completeRecord,
6
- computeElapsedSeconds,
7
- createRecord,
8
- extractLabelFromArgs,
9
- getAllToolCalls,
10
- getCurrentActivity,
11
- getEventLog,
12
- getFullText,
13
- getFullTextFrom,
14
- getTotalUsage,
15
- markReconstructedStatus,
16
- nextRoundBaseTurnIndex,
17
- project,
18
- projectLiveProgress,
19
- snapshot,
20
- tryTransition,
21
- updateFromEvent,
22
- } from "../execution-record.ts";
23
- import type { AgentResult, ExecutionRecord, SubagentRecord, Turn } from "../types.ts";
24
- import { toSubagentRecordEntry } from "../record-entry.ts";
25
-
26
- // ── 常量(与源码 module-private 值对齐,测试用字面量)──
27
- const TURN_SUMMARY_MAX = 80;
28
-
29
- // ── 工厂 ──
30
- function emptyTurn(): Turn {
31
- return { text: "", thinking: "", toolCalls: [], usageDelta: undefined, closed: false, closedTs: undefined };
32
- }
33
-
34
- function makeRecord(over: Partial<ExecutionRecord> = {}): ExecutionRecord {
35
- return {
36
- id: "test-1",
37
- agent: "worker",
38
- model: "test-model",
39
- thinkingLevel: undefined,
40
- mode: "sync",
41
- task: "test task",
42
- startedAt: 1000,
43
- status: "running",
44
- turns: [emptyTurn()],
45
- turnCount: 0,
46
- totalTokens: 0,
47
- lastError: undefined,
48
- endedAt: undefined,
49
- result: undefined,
50
- error: undefined,
51
- agentResult: undefined,
52
- controller: undefined,
53
- ...over,
54
- };
55
- }
56
-
57
- const SAMPLE_RESULT: AgentResult = {
58
- text: "done",
59
- turns: 1,
60
- durationMs: 500,
61
- success: true,
62
- sessionId: "sess-1",
63
- toolCalls: [],
64
- };
65
-
66
- // ============================================================
67
- // createRecord
68
- // ============================================================
69
- describe("createRecord", () => {
70
- it("creates a record with identity fields frozen and defaults", () => {
71
- const r = createRecord("r1", {
72
- agent: "reviewer",
73
- model: "m1",
74
- thinkingLevel: "high",
75
- mode: "background",
76
- task: "review PR",
77
- startedAt: 2000,
78
- });
79
- expect(r.id).toBe("r1");
80
- expect(r.agent).toBe("reviewer");
81
- expect(r.model).toBe("m1");
82
- expect(r.thinkingLevel).toBe("high");
83
- expect(r.mode).toBe("background");
84
- expect(r.task).toBe("review PR");
85
- expect(r.startedAt).toBe(2000);
86
-
87
- // defaults——turns[] 初始化为 [空 turn],turnCount=0
88
- expect(r.status).toBe("running");
89
- expect(r.turns).toHaveLength(1);
90
- expect(r.turns[0]).toMatchObject({ text: "", thinking: "", toolCalls: [], closed: false });
91
- expect(r.turnCount).toBe(0);
92
- expect(r.totalTokens).toBe(0);
93
- expect(r.lastError).toBeUndefined();
94
- expect(r.endedAt).toBeUndefined();
95
- expect(r.result).toBeUndefined();
96
- expect(r.error).toBeUndefined();
97
- expect(r.agentResult).toBeUndefined();
98
- });
99
-
100
- it("stores controller when provided (background)", () => {
101
- const controller = new AbortController();
102
- const r = createRecord("r1", {
103
- agent: "w", model: "m", mode: "background", task: "t", startedAt: 0, controller,
104
- });
105
- expect(r.controller).toBe(controller);
106
- });
107
-
108
- it("stores rootSessionId when provided", () => {
109
- const r = createRecord("r1", {
110
- agent: "w", model: "m", mode: "sync", task: "t", startedAt: 0, rootSessionId: "sess-A",
111
- });
112
- expect(r.rootSessionId).toBe("sess-A");
113
- });
114
-
115
- it("defaults rootSessionId to undefined when omitted", () => {
116
- const r = createRecord("r1", {
117
- agent: "w", model: "m", mode: "sync", task: "t", startedAt: 0,
118
- });
119
- expect(r.rootSessionId).toBeUndefined();
120
- });
121
- });
122
-
123
- // ============================================================
124
- // updateFromEvent — turns accumulation
125
- // ============================================================
126
- describe("updateFromEvent", () => {
127
- describe("turnCount accumulation", () => {
128
- it("increments turnCount on turn_end", () => {
129
- const r = makeRecord();
130
- updateFromEvent(r, { type: "turn_end", summary: "done" });
131
- expect(r.turnCount).toBe(1);
132
- updateFromEvent(r, { type: "turn_end" });
133
- expect(r.turnCount).toBe(2);
134
- });
135
-
136
- it("does not increment turnCount on other events", () => {
137
- const r = makeRecord();
138
- updateFromEvent(r, { type: "text_delta", delta: "hi" });
139
- updateFromEvent(r, { type: "tool_start", toolName: "read" });
140
- updateFromEvent(r, { type: "message_end" });
141
- expect(r.turnCount).toBe(0);
142
- });
143
- });
144
-
145
- describe("totalTokens accumulation", () => {
146
- it("sums all usage fields on message_end", () => {
147
- const r = makeRecord();
148
- updateFromEvent(r, {
149
- type: "message_end",
150
- usage: { input: 10, output: 20, cacheRead: 5, cacheWrite: 3 },
151
- });
152
- expect(r.totalTokens).toBe(38);
153
- });
154
-
155
- it("accumulates across multiple message_end events", () => {
156
- const r = makeRecord();
157
- updateFromEvent(r, { type: "message_end", usage: { input: 1, output: 1, cacheRead: 1, cacheWrite: 1 } });
158
- updateFromEvent(r, { type: "message_end", usage: { input: 2, output: 2, cacheRead: 2, cacheWrite: 2 } });
159
- expect(r.totalTokens).toBe(12);
160
- });
161
-
162
- it("ignores message_end without usage", () => {
163
- const r = makeRecord();
164
- updateFromEvent(r, { type: "message_end" });
165
- expect(r.totalTokens).toBe(0);
166
- });
167
-
168
- it("stores usageDelta on current turn", () => {
169
- const r = makeRecord();
170
- updateFromEvent(r, {
171
- type: "message_end",
172
- usage: { input: 10, output: 20, cacheRead: 5, cacheWrite: 3 },
173
- });
174
- expect(r.turns[0]?.usageDelta).toEqual({ input: 10, output: 20, cacheRead: 5, cacheWrite: 3 });
175
- });
176
- });
177
-
178
- // ============================================================
179
- // text / thinking accumulation (替代旧 chunking)
180
- // ============================================================
181
- describe("text accumulation", () => {
182
- it("accumulates text_delta into current turn.text (完整内容,非切片)", () => {
183
- const r = makeRecord();
184
- updateFromEvent(r, { type: "text_delta", delta: "Hello " });
185
- updateFromEvent(r, { type: "text_delta", delta: "world" });
186
- expect(r.turns[0]?.text).toBe("Hello world");
187
- });
188
-
189
- it("text accumulation survives long delta (>100 chars, no chunking)", () => {
190
- const r = makeRecord();
191
- const longText = "y".repeat(350);
192
- updateFromEvent(r, { type: "text_delta", delta: longText });
193
- // 完整存储,不切片——这是收口设计的核心
194
- expect(r.turns[0]?.text).toBe(longText);
195
- expect(r.turns).toHaveLength(1);
196
- });
197
- });
198
-
199
- describe("thinking accumulation", () => {
200
- it("accumulates thinking_delta into current turn.thinking (完整内容)", () => {
201
- const r = makeRecord();
202
- updateFromEvent(r, { type: "thinking_delta", delta: "Analyzing " });
203
- updateFromEvent(r, { type: "thinking_delta", delta: "the problem" });
204
- expect(r.turns[0]?.thinking).toBe("Analyzing the problem");
205
- });
206
- });
207
-
208
- describe("turn boundary", () => {
209
- it("turn_end closes current turn; next delta opens new turn", () => {
210
- const r = makeRecord();
211
- updateFromEvent(r, { type: "text_delta", delta: "turn 1 text" });
212
- updateFromEvent(r, { type: "turn_end" });
213
- expect(r.turns[0]?.closed).toBe(true);
214
- expect(r.turnCount).toBe(1);
215
-
216
- // 新 delta 开新 turn
217
- updateFromEvent(r, { type: "text_delta", delta: "turn 2 text" });
218
- expect(r.turns).toHaveLength(2);
219
- expect(r.turns[1]?.closed).toBe(false);
220
- expect(r.turns[1]?.text).toBe("turn 2 text");
221
- // turn 1 不受影响
222
- expect(r.turns[0]?.text).toBe("turn 1 text");
223
- });
224
-
225
- it("turn_end closes turn and records closedTs (real wall-clock)", () => {
226
- const r = makeRecord();
227
- updateFromEvent(r, { type: "text_delta", delta: "partial" });
228
- const before = Date.now();
229
- updateFromEvent(r, { type: "turn_end" });
230
- const after = Date.now();
231
- expect(r.turns[0]?.closed).toBe(true);
232
- expect(r.turns[0]?.closedTs).toBeGreaterThanOrEqual(before);
233
- expect(r.turns[0]?.closedTs).toBeLessThanOrEqual(after);
234
- // turn_end 不再覆盖已累积的 text(旧 dead branch 已移除)
235
- expect(r.turns[0]?.text).toBe("partial");
236
- expect(r.turnCount).toBe(1);
237
- });
238
-
239
- it("turn_end clears lastError (transient error recovery → success)", () => {
240
- // 瞬态 error 到达后,若 turn 正常闭合,lastError 应清空——
241
- // 否则 session-runner 会据残留 lastError 把成功误判为 success=false。
242
- const r = makeRecord();
243
- updateFromEvent(r, { type: "error", message: "transient" });
244
- expect(r.lastError).toBe("transient");
245
- updateFromEvent(r, { type: "turn_end" });
246
- expect(r.lastError).toBeUndefined();
247
- });
248
-
249
- it("turn_end after turn_end: next delta opens 3rd turn (not mutate 2nd)", () => {
250
- // 连续两次 turn_end 后再发 text_delta,应 push 第 3 个 turn,
251
- // 而非回填已 closed 的第 2 个空 turn。
252
- const r = makeRecord();
253
- updateFromEvent(r, { type: "text_delta", delta: "t1" });
254
- updateFromEvent(r, { type: "turn_end" });
255
- updateFromEvent(r, { type: "turn_end" }); // 第 2 个空 turn 立即 closed
256
- updateFromEvent(r, { type: "text_delta", delta: "t3" });
257
- expect(r.turns).toHaveLength(3);
258
- expect(r.turns[0]?.text).toBe("t1");
259
- expect(r.turns[1]?.text).toBe(""); // 第 2 个空 turn 未被回填
260
- expect(r.turns[2]?.text).toBe("t3");
261
- });
262
- });
263
-
264
- // ============================================================
265
- // tool events → turn.toolCalls
266
- // ============================================================
267
- describe("tool events", () => {
268
- it("tool_start pushes a running ToolCall into current turn", () => {
269
- const r = makeRecord();
270
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/a/b/foo.ts" } });
271
- expect(r.turns[0]?.toolCalls).toHaveLength(1);
272
- expect(r.turns[0]?.toolCalls[0]).toMatchObject({
273
- toolName: "read",
274
- _status: "running",
275
- });
276
- });
277
-
278
- it("tool_end matches back running toolCall and sets result/status", () => {
279
- const r = makeRecord();
280
- updateFromEvent(r, { type: "tool_start", toolName: "bash", args: { command: "ls" } });
281
- const result = { content: [{ type: "text", text: "file.ts" }] };
282
- updateFromEvent(r, { type: "tool_end", toolName: "bash", args: { command: "ls" }, result });
283
- const tc = r.turns[0]?.toolCalls[0];
284
- expect(tc).toMatchObject({ toolName: "bash", _status: "done", isError: false });
285
- expect(tc?.result).toBe(result);
286
- });
287
-
288
- it("tool_end sets failed status when isError", () => {
289
- const r = makeRecord();
290
- updateFromEvent(r, { type: "tool_start", toolName: "bash", args: { command: "rm" } });
291
- updateFromEvent(r, { type: "tool_end", toolName: "bash", args: { command: "rm" }, isError: true });
292
- expect(r.turns[0]?.toolCalls[0]?._status).toBe("failed");
293
- expect(r.turns[0]?.toolCalls[0]?.isError).toBe(true);
294
- });
295
-
296
- it("tool_end without matching tool_start pushes a completed ToolCall", () => {
297
- const r = makeRecord();
298
- updateFromEvent(r, { type: "tool_end", toolName: "external", args: {} });
299
- expect(r.turns[0]?.toolCalls).toHaveLength(1);
300
- expect(r.turns[0]?.toolCalls[0]?._status).toBe("done");
301
- });
302
-
303
- it("LIFO matching: same-name tool twice, tool_end matches last running", () => {
304
- // 同 turn 内两次 tool_start: bash → tool_end: bash 倒序匹配最后一个 running。
305
- // 正序匹配会错误地把 result 填到第一个 bash,留下第二个 running。
306
- const r = makeRecord();
307
- updateFromEvent(r, { type: "tool_start", toolName: "bash", args: { command: "cmd-a" } });
308
- updateFromEvent(r, { type: "tool_start", toolName: "bash", args: { command: "cmd-b" } });
309
- const resultA = { content: [{ type: "text", text: "A" }] };
310
- updateFromEvent(r, { type: "tool_end", toolName: "bash", args: { command: "cmd-b" }, result: resultA });
311
- const calls = r.turns[0]?.toolCalls ?? [];
312
- expect(calls).toHaveLength(2);
313
- // 第二个 bash(cmd-b)命中 LIFO,标记 done + result=A
314
- expect(calls[1]?._status).toBe("done");
315
- expect(calls[1]?.result).toBe(resultA);
316
- // 第一个 bash(cmd-a)仍未匹配,仍 running
317
- expect(calls[0]?._status).toBe("running");
318
- expect(calls[0]?.result).toBeUndefined();
319
- });
320
-
321
- it("tool_end without result leaves result undefined (SDK may omit result)", () => {
322
- // SDK 契约下 tool_end 的 result 可为 undefined——不应抛错,getEventLog 仍正常派生。
323
- const r = makeRecord();
324
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/a.ts" } });
325
- updateFromEvent(r, { type: "tool_end", toolName: "read", args: { path: "/a.ts" } });
326
- expect(r.turns[0]?.toolCalls[0]?.result).toBeUndefined();
327
- expect(r.turns[0]?.toolCalls[0]?._status).toBe("done");
328
- // getEventLog 仍能派生 tool_start/tool_end 对
329
- const log = getEventLog(r);
330
- expect(log.map((e) => e.type)).toEqual(["tool_start", "tool_end", "turn_end"].slice(0, 2));
331
- });
332
-
333
- it("tool_end matches running toolCall across turns (lagged SDK event)", () => {
334
- // SDK 在 turn_end 后仍可能补发滞后的 tool_end——跨 turn 扫描兜底,
335
- // 不误 push 幽灵 ToolCall。
336
- const r = makeRecord();
337
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/a.ts" } });
338
- updateFromEvent(r, { type: "turn_end" }); // turn[0] closed,read 仍 running
339
- updateFromEvent(r, { type: "tool_end", toolName: "read", args: { path: "/a.ts" } });
340
- // 匹配到 turn[0] 的 read(跨 turn 扫描命中),未产生幽灵 ToolCall
341
- expect(r.turns).toHaveLength(1); // 没有 turn[1]——tool_end 单独不开新 turn
342
- expect(r.turns[0]?.toolCalls[0]?._status).toBe("done");
343
- expect(r.turns[0]?.toolCalls).toHaveLength(1);
344
- });
345
- });
346
-
347
- // ============================================================
348
- // error event → record.lastError
349
- // ============================================================
350
- describe("error event", () => {
351
- it("stores error message in record.lastError", () => {
352
- const r = makeRecord();
353
- updateFromEvent(r, { type: "error", message: "boom" });
354
- expect(r.lastError).toBe("boom");
355
- });
356
-
357
- it("message_end with error field also sets lastError", () => {
358
- const r = makeRecord();
359
- updateFromEvent(r, { type: "message_end", error: "provider error" });
360
- expect(r.lastError).toBe("provider error");
361
- });
362
- });
363
-
364
- // ============================================================
365
- // compaction (no-op)
366
- // ============================================================
367
- describe("compaction", () => {
368
- it("compaction is a no-op", () => {
369
- const r = makeRecord();
370
- updateFromEvent(r, { type: "compaction" });
371
- expect(r.turns).toHaveLength(1);
372
- expect(r.turnCount).toBe(0);
373
- expect(r.totalTokens).toBe(0);
374
- });
375
- });
376
- });
377
-
378
- // ============================================================
379
- // getEventLog — 派生事件序列
380
- // ============================================================
381
- describe("getEventLog", () => {
382
- it("returns empty array for fresh record (empty turn, not closed)", () => {
383
- const r = makeRecord();
384
- expect(getEventLog(r)).toEqual([]);
385
- });
386
-
387
- it("derives tool_start/tool_end pairs from turns[].toolCalls", () => {
388
- const r = makeRecord();
389
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/x.ts" } });
390
- updateFromEvent(r, { type: "tool_end", toolName: "read", args: { path: "/x.ts" } });
391
- const log = getEventLog(r);
392
- expect(log.map((e) => e.type)).toEqual(["tool_start", "tool_end"]);
393
- expect(log[0]).toMatchObject({ type: "tool_start", label: "read x.ts", status: "running" });
394
- expect(log[1]).toMatchObject({ type: "tool_end", label: "read x.ts", status: "done" });
395
- });
396
-
397
- it("running toolCall (no tool_end yet) derives only tool_start", () => {
398
- const r = makeRecord();
399
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/x.ts" } });
400
- const log = getEventLog(r);
401
- expect(log.map((e) => e.type)).toEqual(["tool_start"]);
402
- });
403
-
404
- it("derives turn_end after turn closes (label from turn text)", () => {
405
- const r = makeRecord();
406
- updateFromEvent(r, { type: "text_delta", delta: "Result is 42" });
407
- updateFromEvent(r, { type: "turn_end" });
408
- const log = getEventLog(r);
409
- const turnEntry = log.find((e) => e.type === "turn_end");
410
- expect(turnEntry?.label).toBe("Result is 42");
411
- });
412
-
413
- it("turn_end label defaults to 'turn' when turn has no text", () => {
414
- const r = makeRecord();
415
- updateFromEvent(r, { type: "turn_end" });
416
- const log = getEventLog(r);
417
- const turnEntry = log.find((e) => e.type === "turn_end");
418
- expect(turnEntry?.label).toBe("turn");
419
- });
420
-
421
- it("truncates long turn text to TURN_SUMMARY_MAX in turn_end label", () => {
422
- const r = makeRecord();
423
- const longText = "s".repeat(TURN_SUMMARY_MAX + 20);
424
- updateFromEvent(r, { type: "text_delta", delta: longText });
425
- updateFromEvent(r, { type: "turn_end" });
426
- const log = getEventLog(r);
427
- const turnEntry = log.find((e) => e.type === "turn_end");
428
- expect(turnEntry?.label.length).toBe(TURN_SUMMARY_MAX);
429
- });
430
-
431
- it("appends error entry when record.lastError is set", () => {
432
- const r = makeRecord();
433
- updateFromEvent(r, { type: "error", message: "crashed" });
434
- const log = getEventLog(r);
435
- expect(log[log.length - 1]).toMatchObject({ type: "error", label: "crashed" });
436
- });
437
-
438
- it("multi-turn: events ordered across turns", () => {
439
- const r = makeRecord();
440
- // turn 1: tool A + text
441
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/a.ts" } });
442
- updateFromEvent(r, { type: "tool_end", toolName: "read", args: { path: "/a.ts" } });
443
- updateFromEvent(r, { type: "text_delta", delta: "done turn 1" });
444
- updateFromEvent(r, { type: "turn_end" });
445
- // turn 2: tool B
446
- updateFromEvent(r, { type: "tool_start", toolName: "edit", args: { path: "/b.ts" } });
447
- updateFromEvent(r, { type: "turn_end" });
448
- const types = getEventLog(r).map((e) => e.type);
449
- expect(types).toEqual([
450
- "tool_start", "tool_end", "turn_end", // turn 1
451
- "tool_start", "turn_end", // turn 2 (tool_start only, no tool_end)
452
- ]);
453
- });
454
-
455
- it("uses real wall-clock ts (tool: startedTs, turn_end: closedTs)", () => {
456
- // ts 不再是合成 +1,而是真实 Date.now()——消费方可按时序/时长分析。
457
- const before = Date.now();
458
- const r = makeRecord({ startedAt: before });
459
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/a.ts" } });
460
- const afterToolStart = Date.now();
461
- updateFromEvent(r, { type: "tool_end", toolName: "read", args: { path: "/a.ts" } });
462
- updateFromEvent(r, { type: "turn_end" });
463
- const afterTurnEnd = Date.now();
464
- const log = getEventLog(r);
465
- const toolStartTs = log[0]?.ts;
466
- const turnEndTs = log[2]?.ts;
467
- expect(toolStartTs).toBeGreaterThanOrEqual(before);
468
- expect(toolStartTs).toBeLessThanOrEqual(afterToolStart);
469
- expect(turnEndTs).toBeGreaterThanOrEqual(afterToolStart);
470
- expect(turnEndTs).toBeLessThanOrEqual(afterTurnEnd);
471
- });
472
- });
473
-
474
- // ============================================================
475
- // getCurrentActivity — 派生活动行
476
- // ============================================================
477
- describe("getCurrentActivity", () => {
478
- it("returns undefined when status is not running", () => {
479
- const r = makeRecord({ status: "done" });
480
- expect(getCurrentActivity(r)).toBeUndefined();
481
- });
482
-
483
- it("returns undefined when turn is closed", () => {
484
- const r = makeRecord();
485
- updateFromEvent(r, { type: "turn_end" });
486
- expect(getCurrentActivity(r)).toBeUndefined();
487
- });
488
-
489
- it("prefers running tool over thinking/text", () => {
490
- const r = makeRecord();
491
- updateFromEvent(r, { type: "tool_start", toolName: "edit", args: { path: "/a.ts" } });
492
- r.turns[0]!.thinking = "thinking...";
493
- r.turns[0]!.text = "text...";
494
- expect(getCurrentActivity(r)).toEqual({ type: "tool", label: "edit a.ts" });
495
- });
496
-
497
- it("falls back to thinking when no running tool", () => {
498
- const r = makeRecord();
499
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/a.ts" } });
500
- updateFromEvent(r, { type: "tool_end", toolName: "read", args: { path: "/a.ts" } });
501
- r.turns[0]!.thinking = "pondering";
502
- expect(getCurrentActivity(r)).toEqual({ type: "thinking", label: "pondering" });
503
- });
504
-
505
- it("falls back to text when no tool/thinking", () => {
506
- const r = makeRecord();
507
- r.turns[0]!.text = "writing output";
508
- expect(getCurrentActivity(r)).toEqual({ type: "text", label: "writing output" });
509
- });
510
-
511
- it("text label takes START not tail fragment (regression: text: } bug)", () => {
512
- // 原始 bug:compact view 显示流式文本的末尾碎片(如 "text: }")而非开头。
513
- // getCurrentActivity 必须取 turn.text 开头——本测试在 bug 回归时会失败。
514
- const r = makeRecord();
515
- updateFromEvent(r, { type: "text_delta", delta: "Hello world this is the response start" });
516
- updateFromEvent(r, { type: "text_delta", delta: " ... more content ... }" });
517
- const activity = getCurrentActivity(r);
518
- expect(activity?.type).toBe("text");
519
- // label 以开头而非尾巴开始
520
- expect(activity?.label.startsWith("Hello world")).toBe(true);
521
- // 绝不以尾巴碎片开头
522
- expect(activity?.label.startsWith("}")).toBe(false);
523
- expect(activity?.label.startsWith(" ... more")).toBe(false);
524
- });
525
-
526
- it("truncates text label to ACTIVITY_LABEL_MAX (60)", () => {
527
- const r = makeRecord();
528
- const longText = "y".repeat(120);
529
- r.turns[0]!.text = longText;
530
- const activity = getCurrentActivity(r);
531
- expect(activity?.label.length).toBe(60);
532
- expect(activity?.label).toBe(longText.slice(0, 60));
533
- });
534
-
535
- it("returns undefined when idle (empty turn)", () => {
536
- const r = makeRecord();
537
- expect(getCurrentActivity(r)).toBeUndefined();
538
- });
539
- });
540
-
541
- // ============================================================
542
- // getFullText — 聚合文本
543
- // ============================================================
544
- describe("getFullText", () => {
545
- it("returns empty string for fresh record", () => {
546
- const r = makeRecord();
547
- expect(getFullText(r)).toBe("");
548
- });
549
-
550
- it("returns single turn text", () => {
551
- const r = makeRecord();
552
- updateFromEvent(r, { type: "text_delta", delta: "Hello world" });
553
- expect(getFullText(r)).toBe("Hello world");
554
- });
555
-
556
- it("joins multiple turns with double newline", () => {
557
- const r = makeRecord();
558
- updateFromEvent(r, { type: "text_delta", delta: "Turn 1" });
559
- updateFromEvent(r, { type: "turn_end" });
560
- updateFromEvent(r, { type: "text_delta", delta: "Turn 2" });
561
- expect(getFullText(r)).toBe("Turn 1\n\nTurn 2");
562
- });
563
-
564
- it("skips empty turns", () => {
565
- const r = makeRecord();
566
- updateFromEvent(r, { type: "text_delta", delta: "Turn 1" });
567
- updateFromEvent(r, { type: "turn_end" });
568
- updateFromEvent(r, { type: "turn_end" }); // 空 turn 2
569
- updateFromEvent(r, { type: "text_delta", delta: "Turn 3" });
570
- expect(getFullText(r)).toBe("Turn 1\n\nTurn 3");
571
- });
572
-
573
- it("aggregates multiple text_deltas within a single turn", () => {
574
- const r = makeRecord();
575
- updateFromEvent(r, { type: "text_delta", delta: "Hello " });
576
- updateFromEvent(r, { type: "text_delta", delta: "world" });
577
- updateFromEvent(r, { type: "text_delta", delta: "!" });
578
- expect(getFullText(r)).toBe("Hello world!");
579
- });
580
- });
581
-
582
- // ============================================================
583
- // getFullTextFrom — 增量文本派生(round-base-increment wave1)
584
- // ============================================================
585
- describe("getFullTextFrom", () => {
586
- it("C1TC2: from=0 与 getFullText 逐字节等价(fresh / 单 turn / 多 turn 混空 turn 三形态)", () => {
587
- // 形态 1:fresh record(turns=[emptyTurn()])
588
- const fresh = makeRecord();
589
- expect(getFullTextFrom(fresh, 0)).toBe(getFullText(fresh));
590
- expect(getFullTextFrom(fresh, 0)).toBe("");
591
-
592
- // 形态 2:单 turn 有文本
593
- const single = makeRecord();
594
- updateFromEvent(single, { type: "text_delta", delta: "Hello world" });
595
- expect(getFullTextFrom(single, 0)).toBe(getFullText(single));
596
- expect(getFullTextFrom(single, 0)).toBe("Hello world");
597
-
598
- // 形态 3:多 turn 混空 turn(closed 空 turn + 空白串 turn + 未闭合尾 turn)
599
- const mixed = makeRecord();
600
- updateFromEvent(mixed, { type: "text_delta", delta: "Turn 1" });
601
- updateFromEvent(mixed, { type: "turn_end" });
602
- updateFromEvent(mixed, { type: "turn_end" }); // 空 turn 2(closed)
603
- updateFromEvent(mixed, { type: "text_delta", delta: " " }); // 空白串 turn 3
604
- updateFromEvent(mixed, { type: "turn_end" });
605
- updateFromEvent(mixed, { type: "text_delta", delta: "Turn 4" }); // 未闭合尾 turn
606
- expect(getFullTextFrom(mixed, 0)).toBe(getFullText(mixed));
607
- expect(getFullTextFrom(mixed, 0)).toBe("Turn 1\n\n \n\nTurn 4");
608
- });
609
-
610
- it("C1TC3: 多 turn 单轮增量——from=1 只含 Turn 2,对照全量含 Turn 1\\n\\nTurn 2", () => {
611
- const r = makeRecord();
612
- updateFromEvent(r, { type: "text_delta", delta: "Turn 1" });
613
- updateFromEvent(r, { type: "turn_end" });
614
- updateFromEvent(r, { type: "text_delta", delta: "Turn 2" });
615
- expect(getFullTextFrom(r, 1)).toBe("Turn 2");
616
- // 对照:全量 vs 增量差异锁定
617
- expect(getFullText(r)).toBe("Turn 1\n\nTurn 2");
618
- });
619
-
620
- it("C1TC4: 空文本轮过滤(join 不多出空段)+ 空白串按非空计入增量", () => {
621
- // turns: ["A"(closed), ""(closed), " "(closed), "B"(open)]
622
- const r = makeRecord();
623
- updateFromEvent(r, { type: "text_delta", delta: "A" });
624
- updateFromEvent(r, { type: "turn_end" });
625
- updateFromEvent(r, { type: "turn_end" }); // 空 turn(closed)
626
- updateFromEvent(r, { type: "text_delta", delta: " " });
627
- updateFromEvent(r, { type: "turn_end" });
628
- updateFromEvent(r, { type: "text_delta", delta: "B" });
629
- // from=2 起:空 turn 在 from 之前不参与;" " 按非空处理计入增量(filter 判据与 getFullText 逐字对齐)
630
- expect(getFullTextFrom(r, 2)).toBe(" \n\nB");
631
- // 空 turn 过滤:from=0 时不产出空段(join 不多出 \n\n 空段)
632
- expect(getFullTextFrom(r, 0)).toBe("A\n\n \n\nB");
633
- });
634
-
635
- it("C1TC5: from >= turns.length 越界返回空串(不抛错、不回绕)", () => {
636
- const r = makeRecord();
637
- updateFromEvent(r, { type: "text_delta", delta: "Turn 1" });
638
- updateFromEvent(r, { type: "turn_end" });
639
- updateFromEvent(r, { type: "text_delta", delta: "Turn 2" });
640
- expect(getFullTextFrom(r, r.turns.length)).toBe("");
641
- expect(getFullTextFrom(r, r.turns.length + 5)).toBe("");
642
- });
643
-
644
- it("C1TC6: from 为负规范化为 0(与 from=0 / getFullText 等价,无 slice 负偏移语义)", () => {
645
- const r = makeRecord();
646
- updateFromEvent(r, { type: "text_delta", delta: "Turn 1" });
647
- updateFromEvent(r, { type: "turn_end" });
648
- updateFromEvent(r, { type: "text_delta", delta: "Turn 2" });
649
- expect(getFullTextFrom(r, -1)).toBe(getFullTextFrom(r, 0));
650
- expect(getFullTextFrom(r, -100)).toBe(getFullText(r));
651
- expect(getFullTextFrom(r, -100)).toBe("Turn 1\n\nTurn 2");
652
- });
653
- });
654
-
655
- // ============================================================
656
- // nextRoundBaseTurnIndex — 轮次边界推进公式(round-base-increment wave1)
657
- // ============================================================
658
- describe("nextRoundBaseTurnIndex", () => {
659
- it("C1TC7: 全闭合推进 length / fresh record([emptyTurn()] 单未闭合空 turn)返回 0", () => {
660
- // 全闭合:2 个 closed turn → length
661
- const r = makeRecord();
662
- updateFromEvent(r, { type: "text_delta", delta: "Turn 1" });
663
- updateFromEvent(r, { type: "turn_end" });
664
- updateFromEvent(r, { type: "text_delta", delta: "Turn 2" });
665
- updateFromEvent(r, { type: "turn_end" });
666
- expect(r.turns.length).toBe(2);
667
- expect(nextRoundBaseTurnIndex(r)).toBe(2);
668
-
669
- // fresh 形态(createRecord:turns=[emptyTurn()] 单个未闭合空 turn)→ 0(ES6 重建回退语义)
670
- const fresh = createRecord("fresh-1", {
671
- agent: "worker",
672
- model: "test-model",
673
- thinkingLevel: undefined,
674
- mode: "sync",
675
- task: "t",
676
- slug: "t",
677
- startedAt: 1000,
678
- rootSessionId: "sess-main",
679
- parentRecordId: undefined,
680
- depth: 0,
681
- });
682
- expect(fresh.turns).toHaveLength(1);
683
- expect(nextRoundBaseTurnIndex(fresh)).toBe(0);
684
- });
685
-
686
- it("C1TC8 ⛔ ES1 探针: 滞后空 turn(防御分支)边界不丢下轮首段文本", () => {
687
- // 防御分支构造(pi 现事件序下不可达——带 usage 的 message_end 恒先于 turn_end;探针
688
- // 按防御分支构造仍有效,锁住公式在滞后交错下的正确性,防 pi 未来事件序变化)。
689
- // 两层 usage 守卫:session-runner.ts 转发层 if (msg?.usage)(bare message_end 不转发)
690
- // + execution-record.ts 累积层 if (event.usage)(bare message_end 不开 turn)——
691
- // 故本探针的 message_end 必须携带 usage(如 { input: 10, output: 5, cacheRead: 0, cacheWrite: 0 })才能开出空 turn。
692
- const r = makeRecord();
693
- updateFromEvent(r, { type: "text_delta", delta: "R1" });
694
- updateFromEvent(r, { type: "turn_end" }); // 闭合本轮
695
- updateFromEvent(r, {
696
- type: "message_end",
697
- usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0 },
698
- }); // 滞后 message_end 开新空 turn
699
- expect(r.turns.length).toBe(2);
700
- expect(r.turns[1]!.closed).toBe(false);
701
- expect(r.turns[1]!.text).toBe("");
702
-
703
- // 推进:滞后空 turn 不计入边界(turns.length - 1,留在下一轮增量内)
704
- const base = nextRoundBaseTurnIndex(r);
705
- expect(base).toBe(1);
706
-
707
- // 新轮 text_delta 复用该空 turn → 文本落在增量范围内,不丢
708
- updateFromEvent(r, { type: "text_delta", delta: "R2" });
709
- expect(r.turns.length).toBe(2); // 复用,未新开
710
- expect(getFullTextFrom(r, base)).toBe("R2");
711
-
712
- // 对照组:若直用 turns.length 推进,R2 被挤出 slice——被防住的丢文本负面行为锁定
713
- expect(getFullTextFrom(r, r.turns.length)).toBe("");
714
- });
715
-
716
- it("C1TC9: 末 turn 未闭合且非空计入本轮(防御形态公式锁定,R1 观测哨触发条件)", () => {
717
- // pi 现序不可达(agent_settled 在 agent_end 之后,error/aborted 也先 emit turn_end);
718
- // 构造该防御形态锁死公式行为:非空文本计入本轮增量、不 defer 到下轮。
719
- const r = makeRecord();
720
- updateFromEvent(r, { type: "text_delta", delta: "Turn 1" });
721
- updateFromEvent(r, { type: "turn_end" });
722
- updateFromEvent(r, { type: "text_delta", delta: "Unclosed tail" }); // 未闭合非空尾 turn
723
- expect(nextRoundBaseTurnIndex(r)).toBe(r.turns.length);
724
- expect(nextRoundBaseTurnIndex(r)).toBe(2);
725
- });
726
- });
727
-
728
- // ============================================================
729
- // getAllToolCalls / getTotalUsage — 聚合派生
730
- // ============================================================
731
- describe("getAllToolCalls", () => {
732
- it("flattens toolCalls across turns", () => {
733
- const r = makeRecord();
734
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/a.ts" } });
735
- updateFromEvent(r, { type: "tool_end", toolName: "read", args: { path: "/a.ts" } });
736
- updateFromEvent(r, { type: "turn_end" });
737
- updateFromEvent(r, { type: "tool_start", toolName: "edit", args: { path: "/b.ts" } });
738
- const calls = getAllToolCalls(r);
739
- expect(calls).toHaveLength(2);
740
- expect(calls.map((c) => c.toolName)).toEqual(["read", "edit"]);
741
- });
742
-
743
- it("strips internal _status / startedTs (exported shape is clean ToolCall)", () => {
744
- // 导出的 ToolCall 不应泄漏内部状态机字段(_status / startedTs)——
745
- // 这些是 execution-record 内部实现细节。
746
- const r = makeRecord();
747
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/a.ts" } });
748
- const calls = getAllToolCalls(r);
749
- const tc = calls[0];
750
- expect(tc).toBeDefined();
751
- // 导出形状只有 4 个语义字段
752
- expect(Object.keys(tc!).sort()).toEqual(["args", "isError", "result", "toolName"]);
753
- // 内部字段不存在
754
- expect((tc as Record<string, unknown>)._status).toBeUndefined();
755
- expect((tc as Record<string, unknown>).startedTs).toBeUndefined();
756
- });
757
- });
758
-
759
- describe("getTotalUsage", () => {
760
- it("returns undefined when no usage", () => {
761
- const r = makeRecord();
762
- expect(getTotalUsage(r)).toBeUndefined();
763
- });
764
-
765
- it("aggregates usageDelta across turns", () => {
766
- const r = makeRecord();
767
- updateFromEvent(r, { type: "message_end", usage: { input: 10, output: 20, cacheRead: 5, cacheWrite: 3 } });
768
- updateFromEvent(r, { type: "turn_end" });
769
- updateFromEvent(r, { type: "message_end", usage: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 } });
770
- const usage = getTotalUsage(r);
771
- expect(usage).toEqual({ input: 11, output: 22, cacheRead: 5, cacheWrite: 3, total: 41, cost: 0 });
772
- });
773
-
774
- it("accumulates cost from message_end usage.cost", () => {
775
- const r = makeRecord();
776
- updateFromEvent(r, { type: "message_end", usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, cost: 0.5 } });
777
- updateFromEvent(r, { type: "message_end", usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, cost: 0.25 } });
778
- const usage = getTotalUsage(r);
779
- expect(usage?.cost).toBe(0.75);
780
- });
781
-
782
- it("accumulates multiple message_end within same turn (no usage loss)", () => {
783
- // 同 turn 内多次 message_end——usageDelta 按 field 累加(非覆盖),不丢 usage。
784
- const r = makeRecord();
785
- updateFromEvent(r, { type: "message_end", usage: { input: 10, output: 20, cacheRead: 5, cacheWrite: 3 } });
786
- updateFromEvent(r, { type: "message_end", usage: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 } });
787
- const usage = getTotalUsage(r);
788
- expect(usage).toEqual({ input: 11, output: 22, cacheRead: 5, cacheWrite: 3, total: 41, cost: 0 });
789
- });
790
- });
791
-
792
- // ============================================================
793
- // tryTransition — CAS lock
794
- // ============================================================
795
- describe("tryTransition", () => {
796
- it("returns true and sets status when transitioning from running", () => {
797
- const r = makeRecord({ status: "running" });
798
- expect(tryTransition(r, "done")).toBe(true);
799
- expect(r.status).toBe("done");
800
- });
801
-
802
- it("returns false when already terminal (done)", () => {
803
- const r = makeRecord({ status: "done" });
804
- expect(tryTransition(r, "failed")).toBe(false);
805
- expect(r.status).toBe("done");
806
- });
807
-
808
- it("returns false when already terminal (cancelled)", () => {
809
- const r = makeRecord({ status: "cancelled" });
810
- expect(tryTransition(r, "done")).toBe(false);
811
- });
812
-
813
- it("returns false when already terminal (failed)", () => {
814
- const r = makeRecord({ status: "failed" });
815
- expect(tryTransition(r, "done")).toBe(false);
816
- });
817
-
818
- it("first transition wins in concurrent race (running → done beats running → cancelled)", () => {
819
- const r = makeRecord({ status: "running" });
820
- expect(tryTransition(r, "done")).toBe(true);
821
- expect(tryTransition(r, "cancelled")).toBe(false);
822
- expect(r.status).toBe("done");
823
- });
824
-
825
- it("returns false when trying to transition from crashed to done", () => {
826
- const r = makeRecord({ status: "crashed" });
827
- expect(tryTransition(r, "done")).toBe(false);
828
- expect(r.status).toBe("crashed");
829
- });
830
- });
831
-
832
- describe("markReconstructedStatus", () => {
833
- it("directly sets status without CAS check", () => {
834
- const r = makeRecord({ status: "running" });
835
- markReconstructedStatus(r, "crashed");
836
- expect(r.status).toBe("crashed");
837
- });
838
-
839
- it("can overwrite terminal status (bypass CAS)", () => {
840
- // 重建场景:旧 record 可能已有终态,重建时需要直接覆盖
841
- const r = makeRecord({ status: "done" });
842
- markReconstructedStatus(r, "crashed");
843
- expect(r.status).toBe("crashed");
844
- });
845
-
846
- it("can overwrite running status", () => {
847
- const r = makeRecord({ status: "running" });
848
- markReconstructedStatus(r, "failed");
849
- expect(r.status).toBe("failed");
850
- });
851
-
852
- it("can set crashed on running record", () => {
853
- const r = makeRecord({ status: "running" });
854
- markReconstructedStatus(r, "crashed");
855
- expect(r.status).toBe("crashed");
856
- });
857
-
858
- it("can set crashed on done record (reconstruction override)", () => {
859
- const r = makeRecord({ status: "done" });
860
- markReconstructedStatus(r, "crashed");
861
- expect(r.status).toBe("crashed");
862
- });
863
- });
864
-
865
- // ============================================================
866
- // completeRecord
867
- // ============================================================
868
- describe("completeRecord", () => {
869
- it("writes outcome fields without resetting turnCount/totalTokens", () => {
870
- const r = makeRecord({ turnCount: 5, totalTokens: 42 });
871
- r.status = "done";
872
- completeRecord(r, SAMPLE_RESULT, "done");
873
- expect(r.status).toBe("done");
874
- expect(r.endedAt).toBeTypeOf("number");
875
- expect(r.agentResult).toBe(SAMPLE_RESULT);
876
- expect(r.result).toBe("done");
877
- expect(r.error).toBeUndefined();
878
- expect(r.turnCount).toBe(5);
879
- expect(r.totalTokens).toBe(42);
880
- });
881
-
882
- it("stores error from result", () => {
883
- const r = makeRecord();
884
- r.status = "failed";
885
- const failedResult: AgentResult = { ...SAMPLE_RESULT, success: false, error: "oops" };
886
- completeRecord(r, failedResult, "failed");
887
- expect(r.error).toBe("oops");
888
- });
889
- });
890
-
891
- // ============================================================
892
- // project / snapshot — projections
893
- // ============================================================
894
- describe("projections", () => {
895
- describe("project", () => {
896
- it("returns SubagentToolDetails with all fields", () => {
897
- const r = makeRecord({ turnCount: 3, totalTokens: 100 });
898
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/x.ts" } });
899
- const d = project(r);
900
- expect(d.status).toBe("running");
901
- expect(d.agent).toBe("worker");
902
- expect(d.model).toBe("test-model");
903
- expect(d.turns).toBe(3);
904
- expect(d.totalTokens).toBe(100);
905
- expect(d.eventLog).toHaveLength(1); // tool_start derived
906
- expect(d.currentActivity).toEqual({ type: "tool", label: "read x.ts" });
907
- });
908
-
909
- it("eventLog is a fresh array each call (派生,非存储引用)", () => {
910
- const r = makeRecord();
911
- updateFromEvent(r, { type: "tool_start", toolName: "read" });
912
- const d1 = project(r);
913
- const d2 = project(r);
914
- expect(d1.eventLog).not.toBe(d2.eventLog); // 不同数组实例
915
- expect(d1.eventLog).toEqual(d2.eventLog); // 内容相同
916
- });
917
-
918
- it("currentActivity is undefined when status is not running", () => {
919
- const r = makeRecord({ status: "done" });
920
- expect(project(r).currentActivity).toBeUndefined();
921
- });
922
-
923
- it("outputs mode + sessionFile", () => {
924
- const r = makeRecord({ mode: "background", turnCount: 2 });
925
- r.sessionFile = "bg-1-abc.jsonl";
926
- const d = project(r);
927
- expect(d.mode).toBe("background");
928
- expect(d.sessionFile).toBe("bg-1-abc.jsonl");
929
- });
930
-
931
- it("sessionFile is undefined when record.sessionFile unset", () => {
932
- const r = makeRecord();
933
- expect(project(r).sessionFile).toBeUndefined();
934
- });
935
-
936
- it("currentActivity prefers tool over thinking over text", () => {
937
- const r = makeRecord();
938
- updateFromEvent(r, { type: "tool_start", toolName: "edit", args: { path: "/a.ts" } });
939
- r.turns[0]!.thinking = "thinking...";
940
- r.turns[0]!.text = "text...";
941
- expect(project(r).currentActivity).toEqual({ type: "tool", label: "edit a.ts" });
942
- });
943
-
944
- it("currentActivity falls back to thinking when no running tool", () => {
945
- const r = makeRecord();
946
- updateFromEvent(r, { type: "tool_start", toolName: "read", args: { path: "/a.ts" } });
947
- updateFromEvent(r, { type: "tool_end", toolName: "read", args: { path: "/a.ts" } });
948
- r.turns[0]!.thinking = "pondering";
949
- expect(project(r).currentActivity).toEqual({ type: "thinking", label: "pondering" });
950
- });
951
-
952
- it("currentActivity falls back to text when no tool/thinking", () => {
953
- const r = makeRecord();
954
- r.turns[0]!.text = "writing output";
955
- expect(project(r).currentActivity).toEqual({ type: "text", label: "writing output" });
956
- });
957
-
958
- it("currentActivity is undefined when idle", () => {
959
- const r = makeRecord();
960
- expect(project(r).currentActivity).toBeUndefined();
961
- });
962
- });
963
-
964
- describe("snapshot", () => {
965
- it("returns a readonly snapshot with identity + status fields", () => {
966
- const r = makeRecord({ turnCount: 2, status: "done", endedAt: 5000, result: "ok" });
967
- const s = snapshot(r);
968
- expect(s.id).toBe("test-1");
969
- expect(s.agent).toBe("worker");
970
- expect(s.mode).toBe("sync");
971
- expect(s.task).toBe("test task");
972
- expect(s.status).toBe("done");
973
- expect(s.turns).toBe(2);
974
- expect(s.endedAt).toBe(5000);
975
- expect(s.result).toBe("ok");
976
- });
977
-
978
- it("outputs sessionFile", () => {
979
- const r = makeRecord();
980
- r.sessionFile = "s.jsonl";
981
- expect(snapshot(r).sessionFile).toBe("s.jsonl");
982
- });
983
-
984
- it("sessionFile is undefined when unset", () => {
985
- const r = makeRecord();
986
- expect(snapshot(r).sessionFile).toBeUndefined();
987
- });
988
- });
989
- });
990
-
991
- // ============================================================
992
- // extractLabelFromArgs
993
- // ============================================================
994
- describe("extractLabelFromArgs", () => {
995
- it("returns bare toolName for non-object args", () => {
996
- expect(extractLabelFromArgs("read", undefined)).toBe("read");
997
- expect(extractLabelFromArgs("read", null)).toBe("read");
998
- expect(extractLabelFromArgs("read", "string")).toBe("read");
999
- });
1000
-
1001
- it("returns bare toolName when no recognized field", () => {
1002
- expect(extractLabelFromArgs("custom", { foo: "bar" })).toBe("custom");
1003
- });
1004
-
1005
- it("extracts basename from path", () => {
1006
- expect(extractLabelFromArgs("read", { path: "/home/user/foo.ts" })).toBe("read foo.ts");
1007
- expect(extractLabelFromArgs("edit", { file_path: "C:\\proj\\bar.js" })).toBe("edit bar.js");
1008
- expect(extractLabelFromArgs("write", { filePath: "baz.py" })).toBe("write baz.py");
1009
- });
1010
-
1011
- it("extracts first line of command for bash", () => {
1012
- expect(extractLabelFromArgs("bash", { command: "ls -la\necho done" })).toBe("bash ls -la");
1013
- });
1014
-
1015
- it("extracts query for web_search", () => {
1016
- expect(extractLabelFromArgs("web_search", { query: "hello world" })).toBe("web_search hello world");
1017
- });
1018
-
1019
- it("extracts url for web_fetch", () => {
1020
- expect(extractLabelFromArgs("web_fetch", { url: "https://example.com" })).toBe("web_fetch https://example.com");
1021
- });
1022
-
1023
- it("truncates long labels to TOOL_LABEL_MAX (TUI column-width stability)", () => {
1024
- // 设计意图:保持 TUI 列宽稳定,避免 10KB bash 命令撑爆 compact view。
1025
- const longCmd = "x".repeat(200);
1026
- const label = extractLabelFromArgs("bash", { command: longCmd });
1027
- // label = "bash " + 截断到 100 的 command
1028
- const expectedCmd = "x".repeat(100);
1029
- expect(label).toBe(`bash ${expectedCmd}`);
1030
- expect(label.length).toBe("bash ".length + 100);
1031
- });
1032
-
1033
- it("truncates long path basename", () => {
1034
- const longName = "f".repeat(150) + ".ts";
1035
- const label = extractLabelFromArgs("read", { path: `/dir/${longName}` });
1036
- // basename 截断到 100
1037
- expect(label.length).toBe("read ".length + 100);
1038
- });
1039
-
1040
- it("truncates long query and url", () => {
1041
- const longQuery = "q".repeat(150);
1042
- expect(extractLabelFromArgs("web_search", { query: longQuery }).length).toBe("web_search ".length + 100);
1043
- const longUrl = "u".repeat(150);
1044
- expect(extractLabelFromArgs("web_fetch", { url: longUrl }).length).toBe("web_fetch ".length + 100);
1045
- });
1046
- });
1047
-
1048
- // ============================================================
1049
- // computeElapsedSeconds — 共享 helper
1050
- // ============================================================
1051
- describe("computeElapsedSeconds", () => {
1052
- it("computes floor((endedAt - startedAt) / 1000)", () => {
1053
- expect(computeElapsedSeconds({ startedAt: 0, endedAt: 1500 })).toBe(1);
1054
- expect(computeElapsedSeconds({ startedAt: 1000, endedAt: 1599 })).toBe(0);
1055
- expect(computeElapsedSeconds({ startedAt: 1000, endedAt: 2600 })).toBe(1);
1056
- });
1057
-
1058
- it("uses Date.now() when endedAt is undefined (running state)", () => {
1059
- const startedAt = Date.now() - 3000;
1060
- const secs = computeElapsedSeconds({ startedAt });
1061
- // 至少 2 秒(允许调度延迟),不超过 10 秒(防止假死)
1062
- expect(secs).toBeGreaterThanOrEqual(2);
1063
- expect(secs).toBeLessThan(10);
1064
- });
1065
-
1066
- it("handles identical startedAt/endedAt (0 seconds)", () => {
1067
- expect(computeElapsedSeconds({ startedAt: 5000, endedAt: 5000 })).toBe(0);
1068
- });
1069
- });
1070
-
1071
- // ============================================================
1072
- // T3.21: projectLiveProgress 迁移保留(wave-3)
1073
- // ============================================================
1074
- describe("projectLiveProgress (T3.21)", () => {
1075
- it("projects live progress snapshot from running record", () => {
1076
- const record = makeRecord({
1077
- mode: "sync",
1078
- task: "test task",
1079
- startedAt: 1000,
1080
- status: "running",
1081
- turnCount: 2,
1082
- totalTokens: 500,
1083
- lastError: undefined,
1084
- turns: [
1085
- { ...emptyTurn(), closed: true, text: "turn 1", closedTs: 2000 },
1086
- { ...emptyTurn(), closed: false, text: "turn 2 in progress" },
1087
- ],
1088
- });
1089
-
1090
- const result = projectLiveProgress(record);
1091
- expect(result.status).toBe("running");
1092
- expect(result.turns).toBe(2);
1093
- expect(result.totalTokens).toBe(500);
1094
- expect(result.elapsedSeconds).toBeGreaterThanOrEqual(0);
1095
- expect(result.eventLog).toBeInstanceOf(Array);
1096
- expect(result.currentActivity).toBeDefined();
1097
- expect(result.lastError).toBeUndefined();
1098
- });
1099
-
1100
- it("projectLiveProgress returns lastError when set", () => {
1101
- const record = makeRecord({
1102
- mode: "sync", task: "failing", startedAt: 0, status: "running",
1103
- lastError: "something went wrong",
1104
- });
1105
- const result = projectLiveProgress(record);
1106
- expect(result.lastError).toBe("something went wrong");
1107
- });
1108
- });
1109
-
1110
- // ============================================================
1111
- // tool_end running 索引 [perf]
1112
- // ============================================================
1113
- // 索引弹尾必须与旧实现「倒序全扫找最后一个 running 同名」语义等价:
1114
- // 同名并发 toolCall 的 tool_end 交错到达时按 LIFO 配对,配对结果写回正确槽位。
1115
- describe("tool_end running index [perf]", () => {
1116
- const start = (record: ExecutionRecord, name: string, args: Record<string, unknown> = {}) =>
1117
- updateFromEvent(record, { type: "tool_start", toolName: name, args });
1118
-
1119
- const end = (record: ExecutionRecord, name: string, result: string) =>
1120
- updateFromEvent(record, { type: "tool_end", toolName: name, result, isError: false });
1121
-
1122
- it("同名并发两个 toolCall:tool_end LIFO 配对,结果写回各自槽位", () => {
1123
- const record = makeRecord();
1124
- start(record, "bash", { cmd: "first" });
1125
- start(record, "bash", { cmd: "second" });
1126
-
1127
- // 第一个 end 配对最后 push 的 running 同名(second)——与旧倒序全扫一致
1128
- end(record, "bash", "result-for-second");
1129
- const calls = record.turns[0]!.toolCalls;
1130
- expect(calls).toHaveLength(2);
1131
- expect(calls[0]).toMatchObject({ args: { cmd: "first" }, _status: "running" });
1132
- expect(calls[1]).toMatchObject({ result: "result-for-second", _status: "done" });
1133
-
1134
- // 第二个 end 配对剩下的 first
1135
- end(record, "bash", "result-for-first");
1136
- expect(calls[0]).toMatchObject({ result: "result-for-first", _status: "done" });
1137
- expect(calls[1]).toMatchObject({ result: "result-for-second", _status: "done" });
1138
- });
1139
-
1140
- it("索引 miss(无 tool_start 的外部注入 tool_end)→ fallback 全扫后 push 幽灵条目", () => {
1141
- const record = makeRecord();
1142
- end(record, "external_tool", "injected");
1143
- const calls = record.turns[0]!.toolCalls;
1144
- expect(calls).toHaveLength(1);
1145
- expect(calls[0]).toMatchObject({ toolName: "external_tool", result: "injected", _status: "done" });
1146
- });
1147
-
1148
- it("跨 turn 滞后 tool_end:索引跨 turn 命中(与旧全扫语义一致)", () => {
1149
- const record = makeRecord();
1150
- start(record, "read"); // turn 0 内 running
1151
- updateFromEvent(record, { type: "turn_end" }); // 闭合 turn 0,开 turn 1
1152
- start(record, "write"); // turn 1
1153
- end(record, "write", "w-done"); // 正常配对 turn 1
1154
- end(record, "read", "r-done"); // 滞后 end 配对 turn 0(跨 turn 兜底)
1155
-
1156
- expect(record.turns[0]!.toolCalls[0]).toMatchObject({ toolName: "read", result: "r-done", _status: "done" });
1157
- expect(record.turns[1]!.toolCalls[0]).toMatchObject({ toolName: "write", result: "w-done", _status: "done" });
1158
- });
1159
- });
1160
-
1161
- // ============================================================
1162
- // P4 引擎留痕字段(engine / engineFallback,D9①)
1163
- // ============================================================
1164
-
1165
- describe("createRecord 引擎留痕字段(P4)", () => {
1166
- it("identity 带 engine/engineFallback 时进 record,JSON 序列化保留(持久化语义)", () => {
1167
- const record = createRecord("sa-engine-1", {
1168
- agent: "reviewer",
1169
- model: "p/m",
1170
- mode: "background",
1171
- task: "t",
1172
- slug: "s",
1173
- startedAt: 1,
1174
- engine: "pi",
1175
- engineFallback: { from: "zcode", reason: "engine_probe_failed" },
1176
- });
1177
- expect(record.engine).toBe("pi");
1178
- expect(record.engineFallback).toEqual({ from: "zcode", reason: "engine_probe_failed" });
1179
- const serialized = JSON.parse(JSON.stringify(record)) as Record<string, unknown>;
1180
- expect(serialized["engine"]).toBe("pi");
1181
- expect(serialized["engineFallback"]).toEqual({ from: "zcode", reason: "engine_probe_failed" });
1182
- });
1183
-
1184
- it("不传时两字段缺省(存量 record 消费方零影响——序列化后无键产生)", () => {
1185
- const record = createRecord("sa-engine-2", {
1186
- agent: "worker",
1187
- model: "p/m",
1188
- mode: "background",
1189
- task: "t",
1190
- slug: "s",
1191
- startedAt: 1,
1192
- });
1193
- expect(record.engine).toBeUndefined();
1194
- expect(record.engineFallback).toBeUndefined();
1195
- const serialized = JSON.parse(JSON.stringify(record)) as Record<string, unknown>;
1196
- expect("engine" in serialized).toBe(false);
1197
- expect("engineFallback" in serialized).toBe(false);
1198
- });
1199
-
1200
- it("SubagentRecord → entry 投影保留两字段(record-entry 持久化链)", () => {
1201
- const base: SubagentRecord = {
1202
- id: "sa-engine-3",
1203
- agent: "reviewer",
1204
- task: "t",
1205
- slug: "s",
1206
- status: "running",
1207
- mode: "background",
1208
- startedAt: 1,
1209
- rootSessionId: undefined,
1210
- parentRecordId: undefined,
1211
- depth: 0,
1212
- endedAt: undefined,
1213
- turns: 0,
1214
- totalTokens: 0,
1215
- model: "p/m",
1216
- thinkingLevel: undefined,
1217
- eventLog: [],
1218
- displayItems: [],
1219
- };
1220
- const entry = toSubagentRecordEntry({ ...base, engine: "pi", engineFallback: { from: "zcode", reason: "engine_probe_failed" } });
1221
- expect(entry.engine).toBe("pi");
1222
- expect(entry.engineFallback).toEqual({ from: "zcode", reason: "engine_probe_failed" });
1223
- // 存量 record(无字段)投影后同样缺省——消费方按 pi 投影,零迁移
1224
- expect(toSubagentRecordEntry(base).engine).toBeUndefined();
1225
- });
1226
- });
1227
-
1228
- // ============================================================
1229
- // U1 engine 三字段 entry 往返(engineHandle 贯通)
1230
- // ============================================================
1231
- describe("SubagentRecord ↔ subagent-record entry 往返(U1 engineHandle)", () => {
1232
- const base: SubagentRecord = {
1233
- id: "sa-engine-4",
1234
- agent: "reviewer",
1235
- task: "t",
1236
- slug: "s",
1237
- status: "running",
1238
- mode: "background",
1239
- startedAt: 1,
1240
- rootSessionId: undefined,
1241
- parentRecordId: undefined,
1242
- depth: 0,
1243
- endedAt: undefined,
1244
- turns: 0,
1245
- totalTokens: 0,
1246
- model: "p/m",
1247
- thinkingLevel: undefined,
1248
- eventLog: [],
1249
- displayItems: [],
1250
- };
1251
- const handle = { sessionRef: { sessionId: "s-1", dbPath: "pool/zcode.db" }, journalPath: "/abs/journal.jsonl", poolKey: "p1" };
1252
-
1253
- it("record 含三字段 → entry JSON → 解析回 deep equal(sessionRef 键不枚举整体透传)", () => {
1254
- const entry = toSubagentRecordEntry({ ...base, engine: "zcode", engineFallback: { from: "zcode", reason: "engine_probe_failed" }, engineHandle: handle });
1255
- const roundTripped = JSON.parse(JSON.stringify(entry));
1256
- expect(roundTripped).toEqual({
1257
- v: 1,
1258
- id: "sa-engine-4",
1259
- agent: "reviewer",
1260
- task: "t",
1261
- slug: "s",
1262
- status: "running",
1263
- mode: "background",
1264
- startedAt: 1,
1265
- depth: 0,
1266
- turns: 0,
1267
- totalTokens: 0,
1268
- model: "p/m",
1269
- eventLog: [],
1270
- displayItems: [],
1271
- engine: "zcode",
1272
- engineFallback: { from: "zcode", reason: "engine_probe_failed" },
1273
- engineHandle: handle,
1274
- });
1275
- expect(toSubagentRecordEntry({ ...base, engineHandle: handle }).engineHandle).toEqual(handle);
1276
- });
1277
-
1278
- it("record 无三字段 → entry JSON 不含对应键(undefined 经 JSON.stringify 自然省略,存量零迁移)", () => {
1279
- const serialized = JSON.parse(JSON.stringify(toSubagentRecordEntry(base))) as Record<string, unknown>;
1280
- expect("engine" in serialized).toBe(false);
1281
- expect("engineFallback" in serialized).toBe(false);
1282
- expect("engineHandle" in serialized).toBe(false);
1283
- });
1284
- });