@stigmer/runner 3.15.1 → 3.15.2-dev.20260913223433

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 (518) hide show
  1. package/README.md +2 -4
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +14 -0
  4. package/dist/__test-utils__/execution-record-fixture.js +4 -0
  5. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  6. package/dist/__test-utils__/file-review-projection.d.ts +40 -0
  7. package/dist/__test-utils__/file-review-projection.js +136 -0
  8. package/dist/__test-utils__/file-review-projection.js.map +1 -0
  9. package/dist/__test-utils__/harness-contract/types.d.ts +23 -1
  10. package/dist/__test-utils__/harness-contract/types.js +15 -0
  11. package/dist/__test-utils__/harness-contract/types.js.map +1 -1
  12. package/dist/__test-utils__/hermetic-activity.d.ts +44 -12
  13. package/dist/__test-utils__/hermetic-activity.js +52 -14
  14. package/dist/__test-utils__/hermetic-activity.js.map +1 -1
  15. package/dist/__test-utils__/model-registry-fixture.d.ts +32 -7
  16. package/dist/__test-utils__/model-registry-fixture.js +31 -7
  17. package/dist/__test-utils__/model-registry-fixture.js.map +1 -1
  18. package/dist/__test-utils__/turn-input-fixture.d.ts +1 -1
  19. package/dist/__test-utils__/turn-input-fixture.js +1 -1
  20. package/dist/__test-utils__/turn-input-fixture.js.map +1 -1
  21. package/dist/activities/call-agent.js +2 -2
  22. package/dist/activities/call-agent.js.map +1 -1
  23. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +45 -3
  24. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -1
  25. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +0 -9
  26. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +2 -41
  27. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  28. package/dist/activities/execute-cursor/adapter.d.ts +7 -5
  29. package/dist/activities/execute-cursor/adapter.js +7 -5
  30. package/dist/activities/execute-cursor/adapter.js.map +1 -1
  31. package/dist/activities/execute-cursor/approval-policy.d.ts +5 -2
  32. package/dist/activities/execute-cursor/approval-policy.js +5 -2
  33. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  34. package/dist/activities/execute-cursor/approval-state.d.ts +16 -8
  35. package/dist/activities/execute-cursor/approval-state.js +6 -3
  36. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  37. package/dist/activities/execute-cursor/cas-observations.d.ts +14 -2
  38. package/dist/activities/execute-cursor/cas-observations.js +19 -2
  39. package/dist/activities/execute-cursor/cas-observations.js.map +1 -1
  40. package/dist/activities/execute-cursor/cursor-capabilities.d.ts +8 -4
  41. package/dist/activities/execute-cursor/cursor-capabilities.js +21 -4
  42. package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -1
  43. package/dist/activities/execute-cursor/message-translator.d.ts +12 -20
  44. package/dist/activities/execute-cursor/message-translator.js +19 -32
  45. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  46. package/dist/activities/execute-cursor/prompt-builder.d.ts +48 -58
  47. package/dist/activities/execute-cursor/prompt-builder.js +76 -95
  48. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  49. package/dist/activities/execute-cursor/turn-boundary.d.ts +27 -55
  50. package/dist/activities/execute-cursor/turn-boundary.js +30 -90
  51. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
  52. package/dist/activities/execute-cursor/turn-settle.d.ts +11 -7
  53. package/dist/activities/execute-cursor/turn-settle.js +37 -49
  54. package/dist/activities/execute-cursor/turn-settle.js.map +1 -1
  55. package/dist/activities/execute-cursor/turn-setup.d.ts +16 -24
  56. package/dist/activities/execute-cursor/turn-setup.js +29 -59
  57. package/dist/activities/execute-cursor/turn-setup.js.map +1 -1
  58. package/dist/activities/execute-cursor/turn-stream.d.ts +3 -8
  59. package/dist/activities/execute-cursor/turn-stream.js +9 -22
  60. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  61. package/dist/activities/execute-cursor/turn.js +5 -1
  62. package/dist/activities/execute-cursor/turn.js.map +1 -1
  63. package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.d.ts +93 -0
  64. package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.js +348 -0
  65. package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.js.map +1 -0
  66. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +170 -0
  67. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +225 -0
  68. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -0
  69. package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.d.ts +24 -0
  70. package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.js +41 -0
  71. package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.js.map +1 -0
  72. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.d.ts +54 -0
  73. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.js +72 -0
  74. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.js.map +1 -0
  75. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +187 -23
  76. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +238 -27
  77. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -1
  78. package/dist/activities/execute-deep-agent/adapter.d.ts +41 -0
  79. package/dist/activities/execute-deep-agent/adapter.js +78 -0
  80. package/dist/activities/execute-deep-agent/adapter.js.map +1 -0
  81. package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +8 -0
  82. package/dist/activities/execute-deep-agent/cas-capture-observer.js +9 -0
  83. package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -1
  84. package/dist/activities/execute-deep-agent/deep-agent-capabilities.d.ts +31 -0
  85. package/dist/activities/execute-deep-agent/deep-agent-capabilities.js +40 -0
  86. package/dist/activities/execute-deep-agent/deep-agent-capabilities.js.map +1 -0
  87. package/dist/activities/execute-deep-agent/execution-state.d.ts +4 -11
  88. package/dist/activities/execute-deep-agent/execution-state.js +4 -12
  89. package/dist/activities/execute-deep-agent/execution-state.js.map +1 -1
  90. package/dist/activities/execute-deep-agent/hitl.d.ts +49 -47
  91. package/dist/activities/execute-deep-agent/hitl.js +77 -103
  92. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  93. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +51 -23
  94. package/dist/activities/execute-deep-agent/prompt-builder.js +115 -76
  95. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  96. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +18 -32
  97. package/dist/activities/execute-deep-agent/status-builder-shared.js +10 -65
  98. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  99. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +10 -8
  100. package/dist/activities/execute-deep-agent/streaming-side-effects.js +12 -14
  101. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  102. package/dist/activities/execute-deep-agent/subagent-tracker.d.ts +36 -13
  103. package/dist/activities/execute-deep-agent/subagent-tracker.js +76 -39
  104. package/dist/activities/execute-deep-agent/subagent-tracker.js.map +1 -1
  105. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -28
  106. package/dist/activities/execute-deep-agent/subagent-transformer.js +29 -82
  107. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  108. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -2
  109. package/dist/activities/execute-deep-agent/subagent-wiring.js +2 -2
  110. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  111. package/dist/activities/execute-deep-agent/turn-settle.d.ts +42 -0
  112. package/dist/activities/execute-deep-agent/turn-settle.js +146 -0
  113. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -0
  114. package/dist/activities/execute-deep-agent/turn-setup.d.ts +212 -0
  115. package/dist/activities/execute-deep-agent/turn-setup.js +430 -0
  116. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -0
  117. package/dist/activities/execute-deep-agent/turn-stream.d.ts +97 -0
  118. package/dist/activities/execute-deep-agent/turn-stream.js +242 -0
  119. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -0
  120. package/dist/activities/execute-deep-agent/turn.d.ts +42 -0
  121. package/dist/activities/execute-deep-agent/turn.js +116 -0
  122. package/dist/activities/execute-deep-agent/turn.js.map +1 -0
  123. package/dist/activities/execute-deep-agent/v3-event-recorder.d.ts +6 -5
  124. package/dist/activities/execute-deep-agent/v3-event-recorder.js +6 -5
  125. package/dist/activities/execute-deep-agent/v3-event-recorder.js.map +1 -1
  126. package/dist/activities/execute-deep-agent/v3-events.d.ts +1 -1
  127. package/dist/activities/execute-deep-agent/v3-events.js +1 -1
  128. package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +60 -17
  129. package/dist/activities/execute-deep-agent/v3-status-builder.js +72 -57
  130. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  131. package/dist/harness/approval-decisions.d.ts +61 -0
  132. package/dist/harness/approval-decisions.js +102 -0
  133. package/dist/harness/approval-decisions.js.map +1 -0
  134. package/dist/harness/capabilities.d.ts +16 -5
  135. package/dist/harness/capabilities.js +9 -0
  136. package/dist/harness/capabilities.js.map +1 -1
  137. package/dist/harness/capture.d.ts +144 -0
  138. package/dist/harness/capture.js +219 -0
  139. package/dist/harness/capture.js.map +1 -0
  140. package/dist/harness/persist-chokepoint.d.ts +19 -4
  141. package/dist/harness/persist-chokepoint.js +17 -4
  142. package/dist/harness/persist-chokepoint.js.map +1 -1
  143. package/dist/harness/run-turn.js +115 -29
  144. package/dist/harness/run-turn.js.map +1 -1
  145. package/dist/harness/terminal-table.d.ts +40 -11
  146. package/dist/harness/terminal-table.js +58 -19
  147. package/dist/harness/terminal-table.js.map +1 -1
  148. package/dist/harness/turn-context.d.ts +141 -76
  149. package/dist/harness/turn-context.js +194 -99
  150. package/dist/harness/turn-context.js.map +1 -1
  151. package/dist/harness/types.d.ts +65 -12
  152. package/dist/harness/types.js +8 -2
  153. package/dist/harness/types.js.map +1 -1
  154. package/dist/harness-adapters.d.ts +12 -8
  155. package/dist/harness-adapters.js +17 -9
  156. package/dist/harness-adapters.js.map +1 -1
  157. package/dist/middleware/approval-gate.d.ts +1 -1
  158. package/dist/middleware/approval-gate.js +1 -1
  159. package/dist/middleware/approval-gate.js.map +1 -1
  160. package/dist/middleware/cost-advisory.d.ts +39 -0
  161. package/dist/middleware/{cost-cap.js → cost-advisory.js} +37 -55
  162. package/dist/middleware/cost-advisory.js.map +1 -0
  163. package/dist/middleware/index.d.ts +23 -12
  164. package/dist/middleware/index.js +26 -18
  165. package/dist/middleware/index.js.map +1 -1
  166. package/dist/middleware/types.d.ts +3 -2
  167. package/dist/runner-manager.js +1 -5
  168. package/dist/runner-manager.js.map +1 -1
  169. package/dist/runner.js +1 -5
  170. package/dist/runner.js.map +1 -1
  171. package/dist/shared/attachment-resolver.d.ts +41 -18
  172. package/dist/shared/attachment-resolver.js +178 -46
  173. package/dist/shared/attachment-resolver.js.map +1 -1
  174. package/dist/shared/attachment-zip.d.ts +85 -0
  175. package/dist/shared/attachment-zip.js +185 -0
  176. package/dist/shared/attachment-zip.js.map +1 -0
  177. package/dist/shared/connect-backfill.js +5 -4
  178. package/dist/shared/connect-backfill.js.map +1 -1
  179. package/dist/shared/cost-guard.d.ts +3 -3
  180. package/dist/shared/cost-guard.js +3 -3
  181. package/dist/shared/execution-status-writer.d.ts +5 -5
  182. package/dist/shared/execution-status-writer.js +5 -5
  183. package/dist/shared/extract-structured-output.d.ts +8 -7
  184. package/dist/shared/extract-structured-output.js +8 -7
  185. package/dist/shared/extract-structured-output.js.map +1 -1
  186. package/dist/shared/filereview/cas-progress.d.ts +1 -18
  187. package/dist/shared/filereview/cas-progress.js +4 -0
  188. package/dist/shared/filereview/cas-progress.js.map +1 -1
  189. package/dist/shared/filereview/cas-touched.d.ts +56 -0
  190. package/dist/shared/filereview/cas-touched.js +63 -0
  191. package/dist/shared/filereview/cas-touched.js.map +1 -0
  192. package/dist/shared/filereview/index.d.ts +2 -1
  193. package/dist/shared/filereview/index.js +2 -1
  194. package/dist/shared/filereview/index.js.map +1 -1
  195. package/dist/shared/mcp-resolver.d.ts +4 -5
  196. package/dist/shared/mcp-resolver.js +4 -5
  197. package/dist/shared/mcp-resolver.js.map +1 -1
  198. package/dist/shared/persist-decision.d.ts +3 -4
  199. package/dist/shared/persist-decision.js +3 -4
  200. package/dist/shared/persist-decision.js.map +1 -1
  201. package/dist/shared/plan-mode-permissions.d.ts +2 -2
  202. package/dist/shared/plan-mode-permissions.js +2 -2
  203. package/dist/shared/plan-mode-prompt.d.ts +1 -1
  204. package/dist/shared/plan-mode-prompt.js +1 -1
  205. package/dist/shared/prompt-sections.d.ts +163 -0
  206. package/dist/shared/prompt-sections.js +164 -0
  207. package/dist/shared/prompt-sections.js.map +1 -0
  208. package/dist/shared/skill-resolver.d.ts +4 -2
  209. package/dist/shared/skill-resolver.js +4 -2
  210. package/dist/shared/skill-resolver.js.map +1 -1
  211. package/dist/shared/tool-rounds.d.ts +32 -0
  212. package/dist/shared/tool-rounds.js +43 -0
  213. package/dist/shared/tool-rounds.js.map +1 -1
  214. package/dist/shared/tool-row.d.ts +34 -0
  215. package/dist/shared/tool-row.js +52 -0
  216. package/dist/shared/tool-row.js.map +1 -1
  217. package/dist/shared/workspace/session-provision.d.ts +3 -3
  218. package/dist/shared/workspace/session-provision.js +3 -3
  219. package/dist/shared/workspace/stigmer-link.d.ts +3 -3
  220. package/dist/shared/workspace/stigmer-link.js +3 -3
  221. package/dist/shared/workspace/writeback-coordinator.d.ts +15 -22
  222. package/dist/shared/workspace/writeback-coordinator.js +16 -53
  223. package/dist/shared/workspace/writeback-coordinator.js.map +1 -1
  224. package/dist/tools/index.d.ts +1 -1
  225. package/dist/tools/index.js +1 -1
  226. package/package.json +4 -4
  227. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +1 -0
  228. package/src/__test-utils__/execution-record-fixture.ts +18 -2
  229. package/src/__test-utils__/file-review-projection.ts +156 -0
  230. package/src/__test-utils__/git-workspace-fixture.ts +58 -0
  231. package/src/__test-utils__/harness-boot-order-child.ts +7 -2
  232. package/src/__test-utils__/harness-contract/contract.ts +33 -20
  233. package/src/__test-utils__/harness-contract/recording-sink.ts +11 -1
  234. package/src/__test-utils__/harness-contract/runtime-contract.ts +328 -10
  235. package/src/__test-utils__/harness-contract/scripted-adapter.ts +38 -6
  236. package/src/__test-utils__/harness-contract/types.ts +27 -3
  237. package/src/__test-utils__/hermetic-activity.ts +52 -14
  238. package/src/__test-utils__/model-registry-fixture.ts +32 -7
  239. package/src/__test-utils__/module-specifiers.ts +24 -0
  240. package/src/__test-utils__/turn-input-fixture.ts +2 -2
  241. package/src/__tests__/harness-boot-order.test.ts +9 -2
  242. package/src/activities/call-agent.ts +2 -2
  243. package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +47 -3
  244. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +2 -48
  245. package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +1 -1
  246. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +6 -1
  247. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +52 -27
  248. package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +25 -0
  249. package/src/activities/execute-cursor/__tests__/goldens/prompt.enhanced.everything.prompt.md +166 -0
  250. package/src/activities/execute-cursor/__tests__/goldens/prompt.enhanced.plan-mode.prompt.md +186 -0
  251. package/src/activities/execute-cursor/__tests__/goldens/prompt.hitl.recovery.prompt.md +191 -0
  252. package/src/activities/execute-cursor/__tests__/goldens/prompt.hitl.reinvocation.prompt.md +13 -0
  253. package/src/activities/execute-cursor/__tests__/goldens/prompt.resumed.prefixed.prompt.md +32 -0
  254. package/src/activities/execute-cursor/__tests__/goldens/prompt.resumed.raw.prompt.md +1 -0
  255. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +1 -2
  256. package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +4 -0
  257. package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +24 -4
  258. package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +6 -5
  259. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +9 -0
  260. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +11 -8
  261. package/src/activities/execute-cursor/__tests__/prompt-goldens.test.ts +229 -0
  262. package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +3 -3
  263. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +2 -2
  264. package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +78 -234
  265. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +1 -26
  266. package/src/activities/execute-cursor/adapter.ts +7 -5
  267. package/src/activities/execute-cursor/approval-policy.ts +5 -2
  268. package/src/activities/execute-cursor/approval-state.ts +16 -8
  269. package/src/activities/execute-cursor/cas-observations.ts +20 -2
  270. package/src/activities/execute-cursor/cursor-capabilities.ts +24 -5
  271. package/src/activities/execute-cursor/message-translator.ts +29 -41
  272. package/src/activities/execute-cursor/prompt-builder.ts +99 -157
  273. package/src/activities/execute-cursor/turn-boundary.ts +34 -140
  274. package/src/activities/execute-cursor/turn-settle.ts +37 -51
  275. package/src/activities/execute-cursor/turn-setup.ts +30 -70
  276. package/src/activities/execute-cursor/turn-stream.ts +8 -31
  277. package/src/activities/execute-cursor/turn.ts +5 -1
  278. package/src/activities/execute-deep-agent/__test-utils__/contract-subject.ts +391 -0
  279. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +313 -0
  280. package/src/activities/execute-deep-agent/__test-utils__/hitl-script.ts +48 -0
  281. package/src/activities/execute-deep-agent/__test-utils__/scripted-model-module.ts +85 -0
  282. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +383 -36
  283. package/src/activities/execute-deep-agent/__tests__/adapter-graph-is-sdk-free.test.ts +76 -0
  284. package/src/activities/execute-deep-agent/__tests__/adapter-is-temporal-free.test.ts +47 -0
  285. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +80 -0
  286. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +1 -1
  287. package/src/activities/execute-deep-agent/__tests__/execution-state-extended.test.ts +0 -2
  288. package/src/activities/execute-deep-agent/__tests__/execution-state.test.ts +0 -3
  289. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +172 -0
  290. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +167 -0
  291. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.minimal.prompt.md +36 -0
  292. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +178 -0
  293. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +158 -0
  294. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +210 -0
  295. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve-all-lease.turn1.status.json +74 -0
  296. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve-all-lease.turn2.status.json +104 -0
  297. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve.turn2.status.json +78 -0
  298. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +138 -0
  299. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/gate.turn1.status.json +68 -0
  300. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +154 -0
  301. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +116 -0
  302. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.after-tool.status.json +69 -0
  303. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.mid-tool.status.json +67 -0
  304. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/plain-turn.status.json +47 -0
  305. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/platform-stop.status.json +68 -0
  306. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +280 -0
  307. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/reject.turn2.status.json +77 -0
  308. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
  309. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sequential-gates.turn2.status.json +99 -0
  310. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sequential-gates.turn3.status.json +109 -0
  311. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/skip.turn2.status.json +76 -0
  312. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/structured-output.text-fallback.status.json +51 -0
  313. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/structured-output.tier2.status.json +51 -0
  314. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/structured-output.tool-strategy.status.json +56 -0
  315. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sub-agent-delegation.status.json +90 -0
  316. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/tool-call.status.json +70 -0
  317. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/worker-shutdown.status.json +71 -0
  318. package/src/activities/execute-deep-agent/__tests__/hermetic/harness-contract.test.ts +144 -0
  319. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-approve-all-lease.test.ts +184 -0
  320. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-round-trips.test.ts +203 -0
  321. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-sequential-gates.test.ts +170 -0
  322. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +175 -0
  323. package/src/activities/execute-deep-agent/__tests__/hermetic/pause-vs-shutdown.test.ts +206 -0
  324. package/src/activities/execute-deep-agent/__tests__/hermetic/plain-turn.test.ts +142 -0
  325. package/src/activities/execute-deep-agent/__tests__/hermetic/platform-stop.test.ts +126 -0
  326. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +139 -0
  327. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +122 -0
  328. package/src/activities/execute-deep-agent/__tests__/hermetic/structured-output.test.ts +236 -0
  329. package/src/activities/execute-deep-agent/__tests__/hermetic/sub-agent-delegation.test.ts +161 -0
  330. package/src/activities/execute-deep-agent/__tests__/hermetic/tool-call.test.ts +157 -0
  331. package/src/activities/execute-deep-agent/__tests__/hermetic/workspace-lock-timeout.test.ts +152 -0
  332. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +50 -122
  333. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +8 -6
  334. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +2 -2
  335. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +35 -35
  336. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +247 -0
  337. package/src/activities/execute-deep-agent/__tests__/scripted-model.test.ts +329 -0
  338. package/src/activities/execute-deep-agent/__tests__/status-builder-shared.test.ts +1 -20
  339. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +78 -0
  340. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +1 -1
  341. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +2 -2
  342. package/src/activities/execute-deep-agent/__tests__/subagent-tracker.test.ts +57 -31
  343. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +19 -79
  344. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +15 -15
  345. package/src/activities/execute-deep-agent/__tests__/summarization-verification.test.ts +1 -1
  346. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +391 -0
  347. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +343 -38
  348. package/src/activities/execute-deep-agent/__tests__/vision-input.test.ts +5 -5
  349. package/src/activities/execute-deep-agent/adapter.ts +94 -0
  350. package/src/activities/execute-deep-agent/cas-capture-observer.ts +11 -0
  351. package/src/activities/execute-deep-agent/deep-agent-capabilities.ts +43 -0
  352. package/src/activities/execute-deep-agent/execution-state.ts +4 -13
  353. package/src/activities/execute-deep-agent/hitl.ts +100 -123
  354. package/src/activities/execute-deep-agent/prompt-builder.ts +141 -115
  355. package/src/activities/execute-deep-agent/status-builder-shared.ts +21 -84
  356. package/src/activities/execute-deep-agent/streaming-side-effects.ts +13 -19
  357. package/src/activities/execute-deep-agent/subagent-tracker.ts +77 -42
  358. package/src/activities/execute-deep-agent/subagent-transformer.ts +46 -105
  359. package/src/activities/execute-deep-agent/subagent-wiring.ts +5 -4
  360. package/src/activities/execute-deep-agent/turn-settle.ts +164 -0
  361. package/src/activities/execute-deep-agent/turn-setup.ts +541 -0
  362. package/src/activities/execute-deep-agent/turn-stream.ts +313 -0
  363. package/src/activities/execute-deep-agent/turn.ts +131 -0
  364. package/src/activities/execute-deep-agent/v3-event-recorder.ts +6 -5
  365. package/src/activities/execute-deep-agent/v3-events.ts +1 -1
  366. package/src/activities/execute-deep-agent/v3-status-builder.ts +84 -63
  367. package/src/harness/__tests__/approval-decisions.test.ts +116 -0
  368. package/src/harness/__tests__/capture.test.ts +268 -0
  369. package/src/harness/__tests__/mount-skills.test.ts +169 -0
  370. package/src/harness/__tests__/persist-chokepoint.test.ts +38 -2
  371. package/src/harness/__tests__/run-turn-deterministic.test.ts +94 -0
  372. package/src/harness/__tests__/run-turn.test.ts +39 -23
  373. package/src/harness/__tests__/turn-attachments-vision.test.ts +80 -0
  374. package/src/harness/__tests__/turn-context.test.ts +73 -26
  375. package/src/harness/approval-decisions.ts +104 -0
  376. package/src/harness/capabilities.ts +16 -5
  377. package/src/harness/capture.ts +301 -0
  378. package/src/harness/persist-chokepoint.ts +24 -4
  379. package/src/harness/run-turn.ts +120 -29
  380. package/src/harness/terminal-table.ts +61 -20
  381. package/src/harness/turn-context.ts +208 -111
  382. package/src/harness/types.ts +66 -12
  383. package/src/harness-adapters.ts +17 -9
  384. package/src/middleware/__tests__/approval-gate.test.ts +1 -1
  385. package/src/middleware/__tests__/cost-advisory.test.ts +113 -0
  386. package/src/middleware/approval-gate.ts +2 -2
  387. package/src/middleware/{cost-cap.ts → cost-advisory.ts} +40 -70
  388. package/src/middleware/index.ts +29 -29
  389. package/src/middleware/types.ts +3 -2
  390. package/src/runner-manager.ts +0 -8
  391. package/src/runner.ts +0 -8
  392. package/src/shared/__tests__/attachment-naming.test.ts +6 -19
  393. package/src/shared/__tests__/attachment-resolver.test.ts +158 -8
  394. package/src/shared/__tests__/attachment-zip.test.ts +236 -0
  395. package/src/shared/__tests__/bedrock-adapter.test.ts +1 -1
  396. package/src/shared/__tests__/blueprint-resolver.test.ts +43 -0
  397. package/src/shared/__tests__/cost-guard.test.ts +3 -3
  398. package/src/shared/__tests__/prompt-sections.test.ts +167 -0
  399. package/src/shared/__tests__/secret-leak-scan.test.ts +2 -2
  400. package/src/{activities/execute-deep-agent → shared}/__tests__/stamp-flowed-rows.test.ts +33 -10
  401. package/src/shared/__tests__/vertex-adapter.test.ts +1 -1
  402. package/src/shared/attachment-resolver.ts +218 -55
  403. package/src/shared/attachment-zip.ts +281 -0
  404. package/src/shared/connect-backfill.ts +9 -11
  405. package/src/shared/cost-guard.ts +3 -3
  406. package/src/shared/execution-status-writer.ts +5 -5
  407. package/src/shared/extract-structured-output.ts +8 -7
  408. package/src/shared/filereview/__tests__/capture.test.ts +86 -4
  409. package/src/shared/filereview/__tests__/cas-progress.test.ts +2 -1
  410. package/src/shared/filereview/cas-progress.ts +5 -19
  411. package/src/shared/filereview/cas-touched.ts +89 -0
  412. package/src/shared/filereview/index.ts +4 -2
  413. package/src/shared/mcp-resolver.ts +4 -5
  414. package/src/shared/persist-decision.ts +3 -4
  415. package/src/shared/plan-mode-permissions.ts +2 -2
  416. package/src/shared/plan-mode-prompt.ts +1 -1
  417. package/src/shared/prompt-sections.ts +256 -0
  418. package/src/shared/skill-resolver.ts +4 -2
  419. package/src/shared/tool-rounds.ts +48 -0
  420. package/src/shared/tool-row.ts +61 -0
  421. package/src/shared/workspace/__tests__/stigmer-link.test.ts +7 -22
  422. package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +20 -61
  423. package/src/shared/workspace/session-provision.ts +3 -3
  424. package/src/shared/workspace/stigmer-link.ts +3 -3
  425. package/src/shared/workspace/writeback-coordinator.ts +16 -64
  426. package/src/tools/index.ts +1 -1
  427. package/dist/activities/execute-cursor/capture-flow.d.ts +0 -142
  428. package/dist/activities/execute-cursor/capture-flow.js +0 -285
  429. package/dist/activities/execute-cursor/capture-flow.js.map +0 -1
  430. package/dist/activities/execute-cursor/command-provenance.d.ts +0 -48
  431. package/dist/activities/execute-cursor/command-provenance.js +0 -38
  432. package/dist/activities/execute-cursor/command-provenance.js.map +0 -1
  433. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +0 -134
  434. package/dist/activities/execute-deep-agent/attachment-injector.js +0 -386
  435. package/dist/activities/execute-deep-agent/attachment-injector.js.map +0 -1
  436. package/dist/activities/execute-deep-agent/command-provenance.d.ts +0 -61
  437. package/dist/activities/execute-deep-agent/command-provenance.js +0 -72
  438. package/dist/activities/execute-deep-agent/command-provenance.js.map +0 -1
  439. package/dist/activities/execute-deep-agent/environment.d.ts +0 -24
  440. package/dist/activities/execute-deep-agent/environment.js +0 -58
  441. package/dist/activities/execute-deep-agent/environment.js.map +0 -1
  442. package/dist/activities/execute-deep-agent/event-recorder.d.ts +0 -21
  443. package/dist/activities/execute-deep-agent/event-recorder.js +0 -67
  444. package/dist/activities/execute-deep-agent/event-recorder.js.map +0 -1
  445. package/dist/activities/execute-deep-agent/index.d.ts +0 -15
  446. package/dist/activities/execute-deep-agent/index.js +0 -918
  447. package/dist/activities/execute-deep-agent/index.js.map +0 -1
  448. package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -28
  449. package/dist/activities/execute-deep-agent/mcp-gate.js +0 -22
  450. package/dist/activities/execute-deep-agent/mcp-gate.js.map +0 -1
  451. package/dist/activities/execute-deep-agent/post-stream.d.ts +0 -23
  452. package/dist/activities/execute-deep-agent/post-stream.js +0 -71
  453. package/dist/activities/execute-deep-agent/post-stream.js.map +0 -1
  454. package/dist/activities/execute-deep-agent/setup.d.ts +0 -136
  455. package/dist/activities/execute-deep-agent/setup.js +0 -767
  456. package/dist/activities/execute-deep-agent/setup.js.map +0 -1
  457. package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +0 -36
  458. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +0 -56
  459. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +0 -1
  460. package/dist/activities/execute-deep-agent/status-builder.d.ts +0 -93
  461. package/dist/activities/execute-deep-agent/status-builder.js +0 -365
  462. package/dist/activities/execute-deep-agent/status-builder.js.map +0 -1
  463. package/dist/activities/execute-deep-agent/streaming-terminal.d.ts +0 -21
  464. package/dist/activities/execute-deep-agent/streaming-terminal.js +0 -80
  465. package/dist/activities/execute-deep-agent/streaming-terminal.js.map +0 -1
  466. package/dist/activities/execute-deep-agent/streaming-v3.d.ts +0 -13
  467. package/dist/activities/execute-deep-agent/streaming-v3.js +0 -174
  468. package/dist/activities/execute-deep-agent/streaming-v3.js.map +0 -1
  469. package/dist/activities/execute-deep-agent/streaming.d.ts +0 -81
  470. package/dist/activities/execute-deep-agent/streaming.js +0 -160
  471. package/dist/activities/execute-deep-agent/streaming.js.map +0 -1
  472. package/dist/middleware/cost-cap.d.ts +0 -22
  473. package/dist/middleware/cost-cap.js.map +0 -1
  474. package/dist/middleware/graceful-stop.d.ts +0 -17
  475. package/dist/middleware/graceful-stop.js +0 -63
  476. package/dist/middleware/graceful-stop.js.map +0 -1
  477. package/dist/shared/skill-writer.d.ts +0 -75
  478. package/dist/shared/skill-writer.js +0 -207
  479. package/dist/shared/skill-writer.js.map +0 -1
  480. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +0 -1032
  481. package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +0 -240
  482. package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +0 -169
  483. package/src/activities/execute-cursor/capture-flow.ts +0 -379
  484. package/src/activities/execute-cursor/command-provenance.ts +0 -73
  485. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +0 -1161
  486. package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +0 -252
  487. package/src/activities/execute-deep-agent/__tests__/environment.test.ts +0 -108
  488. package/src/activities/execute-deep-agent/__tests__/event-recorder.test.ts +0 -150
  489. package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +0 -226
  490. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -318
  491. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -351
  492. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -293
  493. package/src/activities/execute-deep-agent/__tests__/index.test.ts +0 -99
  494. package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +0 -44
  495. package/src/activities/execute-deep-agent/__tests__/post-stream.test.ts +0 -112
  496. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -358
  497. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +0 -1933
  498. package/src/activities/execute-deep-agent/__tests__/streaming-terminal.test.ts +0 -40
  499. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +0 -597
  500. package/src/activities/execute-deep-agent/__tests__/streaming.test.ts +0 -508
  501. package/src/activities/execute-deep-agent/attachment-injector.ts +0 -627
  502. package/src/activities/execute-deep-agent/command-provenance.ts +0 -102
  503. package/src/activities/execute-deep-agent/environment.ts +0 -76
  504. package/src/activities/execute-deep-agent/event-recorder.ts +0 -95
  505. package/src/activities/execute-deep-agent/index.ts +0 -1072
  506. package/src/activities/execute-deep-agent/mcp-gate.ts +0 -37
  507. package/src/activities/execute-deep-agent/post-stream.ts +0 -109
  508. package/src/activities/execute-deep-agent/setup.ts +0 -1081
  509. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +0 -64
  510. package/src/activities/execute-deep-agent/status-builder.ts +0 -481
  511. package/src/activities/execute-deep-agent/streaming-terminal.ts +0 -106
  512. package/src/activities/execute-deep-agent/streaming-v3.ts +0 -277
  513. package/src/activities/execute-deep-agent/streaming.ts +0 -313
  514. package/src/middleware/__tests__/cost-cap.test.ts +0 -192
  515. package/src/middleware/__tests__/graceful-stop.test.ts +0 -105
  516. package/src/middleware/graceful-stop.ts +0 -86
  517. package/src/shared/__tests__/skill-writer.test.ts +0 -372
  518. package/src/shared/skill-writer.ts +0 -266
@@ -1,1933 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { create } from "@bufbuild/protobuf";
3
- import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
4
- import {
5
- AgentMessageSchema,
6
- ToolCallSchema,
7
- } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
8
- import {
9
- ApprovalAction,
10
- ExecutionPhase,
11
- MessageType,
12
- ToolCallStatus,
13
- ToolKind,
14
- TodoStatus,
15
- } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
16
- import { StatusBuilder, type StreamEvent, type ApprovalPolicyProvider } from "../status-builder.js";
17
- import type { MergedToolPolicy } from "../../../shared/approval-policy.js";
18
-
19
- function makeBuilder() {
20
- const status = create(AgentExecutionStatusSchema, {});
21
- return new StatusBuilder("exec-test", status);
22
- }
23
-
24
- function chatStreamEvent(
25
- runId: string,
26
- content: string | Record<string, unknown>[],
27
- metadata?: Record<string, unknown>,
28
- ): StreamEvent {
29
- return {
30
- event: "on_chat_model_stream",
31
- name: "ChatAnthropic",
32
- run_id: runId,
33
- data: { chunk: { content } },
34
- metadata,
35
- };
36
- }
37
-
38
- function chatEndEvent(
39
- runId: string,
40
- usageMetadata?: Record<string, unknown>,
41
- ): StreamEvent {
42
- return {
43
- event: "on_chat_model_end",
44
- name: "ChatAnthropic",
45
- run_id: runId,
46
- data: {
47
- output: {
48
- content: "final text",
49
- usage_metadata: usageMetadata,
50
- },
51
- },
52
- };
53
- }
54
-
55
- function toolStartEvent(
56
- runId: string,
57
- toolName: string,
58
- input?: Record<string, unknown>,
59
- ): StreamEvent {
60
- return {
61
- event: "on_tool_start",
62
- name: toolName,
63
- run_id: runId,
64
- data: { input: input ?? {} },
65
- };
66
- }
67
-
68
- function toolEndEvent(
69
- runId: string,
70
- output: unknown,
71
- ): StreamEvent {
72
- return {
73
- event: "on_tool_end",
74
- name: "some_tool",
75
- run_id: runId,
76
- data: { output },
77
- };
78
- }
79
-
80
- describe("StatusBuilder", () => {
81
- describe("initialization", () => {
82
- it("sets phase to IN_PROGRESS", () => {
83
- const sb = makeBuilder();
84
- expect(sb.currentStatus.phase).toBe(ExecutionPhase.EXECUTION_IN_PROGRESS);
85
- });
86
-
87
- it("sets startedAt timestamp", () => {
88
- const sb = makeBuilder();
89
- expect(sb.currentStatus.startedAt).toBeTruthy();
90
- });
91
-
92
- it("starts with forceNextUpdate = false", () => {
93
- expect(makeBuilder().forceNextUpdate).toBe(false);
94
- });
95
- });
96
-
97
- describe("on_chat_model_stream — text tokens", () => {
98
- it("creates an AI message and appends text", () => {
99
- const sb = makeBuilder();
100
- sb.processEvent(chatStreamEvent("run-1", "Hello"));
101
- sb.processEvent(chatStreamEvent("run-1", " world"));
102
-
103
- expect(sb.currentStatus.messages).toHaveLength(1);
104
- const msg = sb.currentStatus.messages[0];
105
- expect(msg.type).toBe(MessageType.MESSAGE_AI);
106
- expect(msg.content).toBe("Hello world");
107
- expect(msg.isStreaming).toBe(true);
108
- });
109
-
110
- it("handles content block array with text blocks", () => {
111
- const sb = makeBuilder();
112
- sb.processEvent(chatStreamEvent("run-1", [
113
- { type: "text", text: "block text" },
114
- ]));
115
-
116
- expect(sb.currentStatus.messages).toHaveLength(1);
117
- expect(sb.currentStatus.messages[0].content).toBe("block text");
118
- });
119
-
120
- it("ignores empty string content", () => {
121
- const sb = makeBuilder();
122
- sb.processEvent(chatStreamEvent("run-1", ""));
123
-
124
- expect(sb.currentStatus.messages).toHaveLength(0);
125
- });
126
-
127
- it("ignores events with no chunk data", () => {
128
- const sb = makeBuilder();
129
- sb.processEvent({
130
- event: "on_chat_model_stream",
131
- run_id: "run-1",
132
- data: {},
133
- });
134
-
135
- expect(sb.currentStatus.messages).toHaveLength(0);
136
- });
137
- });
138
-
139
- describe("on_chat_model_stream — thinking blocks", () => {
140
- it("creates a THINKING message for thinking content", () => {
141
- const sb = makeBuilder();
142
- sb.processEvent(chatStreamEvent("run-1", [
143
- { type: "thinking", thinking: "Let me reason..." },
144
- ]));
145
-
146
- expect(sb.currentStatus.messages).toHaveLength(1);
147
- const msg = sb.currentStatus.messages[0];
148
- expect(msg.type).toBe(MessageType.MESSAGE_THINKING);
149
- expect(msg.content).toBe("Let me reason...");
150
- expect(msg.isStreaming).toBe(true);
151
- });
152
-
153
- it("appends to existing thinking message in same namespace", () => {
154
- const sb = makeBuilder();
155
- sb.processEvent(chatStreamEvent("run-1", [
156
- { type: "thinking", thinking: "Step 1. " },
157
- ]));
158
- sb.processEvent(chatStreamEvent("run-1", [
159
- { type: "thinking", thinking: "Step 2." },
160
- ]));
161
-
162
- expect(sb.currentStatus.messages).toHaveLength(1);
163
- expect(sb.currentStatus.messages[0].content).toBe("Step 1. Step 2.");
164
- });
165
-
166
- it("handles mixed thinking and text in same event", () => {
167
- const sb = makeBuilder();
168
- sb.processEvent(chatStreamEvent("run-1", [
169
- { type: "thinking", thinking: "hmm..." },
170
- { type: "text", text: "answer" },
171
- ]));
172
-
173
- expect(sb.currentStatus.messages).toHaveLength(2);
174
- expect(sb.currentStatus.messages[0].type).toBe(MessageType.MESSAGE_THINKING);
175
- expect(sb.currentStatus.messages[1].type).toBe(MessageType.MESSAGE_AI);
176
- });
177
- });
178
-
179
- describe("turn boundary detection", () => {
180
- it("creates new message when run_id changes", () => {
181
- const sb = makeBuilder();
182
- sb.processEvent(chatStreamEvent("run-1", "First turn"));
183
- sb.processEvent(chatStreamEvent("run-2", "Second turn"));
184
-
185
- expect(sb.currentStatus.messages).toHaveLength(2);
186
- expect(sb.currentStatus.messages[0].content).toBe("First turn");
187
- expect(sb.currentStatus.messages[0].isStreaming).toBe(false);
188
- expect(sb.currentStatus.messages[1].content).toBe("Second turn");
189
- expect(sb.currentStatus.messages[1].isStreaming).toBe(true);
190
- });
191
-
192
- it("reuses same message for same run_id", () => {
193
- const sb = makeBuilder();
194
- sb.processEvent(chatStreamEvent("run-1", "a"));
195
- sb.processEvent(chatStreamEvent("run-1", "b"));
196
-
197
- expect(sb.currentStatus.messages).toHaveLength(1);
198
- expect(sb.currentStatus.messages[0].content).toBe("ab");
199
- });
200
- });
201
-
202
- describe("on_chat_model_end", () => {
203
- it("finalizes message streaming flag", () => {
204
- const sb = makeBuilder();
205
- sb.processEvent(chatStreamEvent("run-1", "text"));
206
- expect(sb.currentStatus.messages[0].isStreaming).toBe(true);
207
-
208
- sb.processEvent(chatEndEvent("run-1"));
209
- expect(sb.currentStatus.messages[0].isStreaming).toBe(false);
210
- });
211
-
212
- it("accumulates usage metadata", () => {
213
- const sb = makeBuilder();
214
- sb.processEvent(chatStreamEvent("run-1", "text"));
215
- sb.processEvent(chatEndEvent("run-1", {
216
- input_tokens: 100,
217
- output_tokens: 50,
218
- cache_read_input_tokens: 10,
219
- cache_creation_input_tokens: 5,
220
- }));
221
-
222
- const usage = sb.currentStatus.streamingUsage;
223
- expect(usage).toBeDefined();
224
- expect(usage!.inputTokens).toBe(100n);
225
- expect(usage!.outputTokens).toBe(50n);
226
- expect(usage!.cacheReadTokens).toBe(10n);
227
- expect(usage!.cacheWriteTokens).toBe(5n);
228
- expect(usage!.totalTokens).toBe(165n);
229
- expect(usage!.turnCount).toBe(1);
230
- });
231
-
232
- it("accumulates across multiple turns", () => {
233
- const sb = makeBuilder();
234
- sb.processEvent(chatStreamEvent("run-1", "turn 1"));
235
- sb.processEvent(chatEndEvent("run-1", {
236
- input_tokens: 100,
237
- output_tokens: 50,
238
- }));
239
- sb.processEvent(chatStreamEvent("run-2", "turn 2"));
240
- sb.processEvent(chatEndEvent("run-2", {
241
- input_tokens: 200,
242
- output_tokens: 100,
243
- }));
244
-
245
- const usage = sb.currentStatus.streamingUsage!;
246
- expect(usage.inputTokens).toBe(300n);
247
- expect(usage.outputTokens).toBe(150n);
248
- expect(usage.turnCount).toBe(2);
249
- });
250
- });
251
-
252
- describe("on_tool_start", () => {
253
- it("creates a RUNNING tool call on the current AI message", () => {
254
- const sb = makeBuilder();
255
- sb.processEvent(chatStreamEvent("run-1", "I'll read a file"));
256
- sb.processEvent(toolStartEvent("tool-run-1", "read", { path: "/foo.ts" }));
257
-
258
- const msg = sb.currentStatus.messages[0];
259
- expect(msg.toolCalls).toHaveLength(1);
260
- expect(msg.toolCalls[0].id).toBe("tool-run-1");
261
- expect(msg.toolCalls[0].name).toBe("read");
262
- expect(msg.toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
263
- expect(msg.toolCalls[0].startedAt).toBeTruthy();
264
- expect(msg.toolCalls[0].args).toEqual({ path: "/foo.ts" });
265
- });
266
-
267
- it("sets forceNextUpdate", () => {
268
- const sb = makeBuilder();
269
- sb.processEvent(chatStreamEvent("run-1", "text"));
270
- sb.processEvent(toolStartEvent("tool-run-1", "read"));
271
-
272
- expect(sb.forceNextUpdate).toBe(true);
273
- });
274
-
275
- it("creates AI message for tool-only turn when no prior AI message exists", () => {
276
- const sb = makeBuilder();
277
- sb.processEvent(toolStartEvent("tool-run-1", "read"));
278
-
279
- expect(sb.currentStatus.messages).toHaveLength(1);
280
- expect(sb.currentStatus.messages[0].toolCalls).toHaveLength(1);
281
- expect(sb.currentStatus.messages[0].toolCalls[0].name).toBe("read");
282
- });
283
- });
284
-
285
- describe("on_tool_end", () => {
286
- it("marks tool call as COMPLETED with result", () => {
287
- const sb = makeBuilder();
288
- sb.processEvent(chatStreamEvent("run-1", "reading file"));
289
- sb.processEvent(toolStartEvent("tool-run-1", "read"));
290
- sb.processEvent(toolEndEvent("tool-run-1", "file contents here"));
291
-
292
- const tc = sb.currentStatus.messages[0].toolCalls[0];
293
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
294
- expect(tc.result).toBe("file contents here");
295
- expect(tc.completedAt).toBeTruthy();
296
- expect(tc.isStreaming).toBe(false);
297
- });
298
-
299
- it("marks tool call as FAILED when output has error", () => {
300
- const sb = makeBuilder();
301
- sb.processEvent(chatStreamEvent("run-1", "text"));
302
- sb.processEvent(toolStartEvent("tool-run-1", "write"));
303
- sb.processEvent(toolEndEvent("tool-run-1", { error: "permission denied" }));
304
-
305
- const tc = sb.currentStatus.messages[0].toolCalls[0];
306
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
307
- expect(tc.error).toBe("permission denied");
308
- });
309
-
310
- it("stores the full result faithfully (no builder-level truncation)", () => {
311
- // Size-bounding is owned by the persist chokepoint (offload + enforce),
312
- // not the builder. The builder must reflect the stream verbatim so binary
313
- // content (e.g. a screenshot's base64) survives intact for offload.
314
- const sb = makeBuilder();
315
- sb.processEvent(chatStreamEvent("run-1", "text"));
316
- sb.processEvent(toolStartEvent("tool-run-1", "read"));
317
-
318
- const longResult = "x".repeat(60_000);
319
- sb.processEvent(toolEndEvent("tool-run-1", longResult));
320
-
321
- const tc = sb.currentStatus.messages[0].toolCalls[0];
322
- expect(tc.result).toBe(longResult);
323
- expect(tc.result).not.toContain("[truncated:");
324
- });
325
-
326
- it("sets forceNextUpdate", () => {
327
- const sb = makeBuilder();
328
- sb.processEvent(chatStreamEvent("run-1", "text"));
329
- sb.processEvent(toolStartEvent("tool-run-1", "read"));
330
- sb.clearForceFlag();
331
- sb.processEvent(toolEndEvent("tool-run-1", "done"));
332
-
333
- expect(sb.forceNextUpdate).toBe(true);
334
- });
335
-
336
- it("ignores unknown tool run_id", () => {
337
- const sb = makeBuilder();
338
- sb.processEvent(toolEndEvent("unknown-run", "result"));
339
- // Should not throw
340
- expect(sb.currentStatus.messages).toHaveLength(0);
341
- });
342
-
343
- it("handles object output with content field", () => {
344
- const sb = makeBuilder();
345
- sb.processEvent(chatStreamEvent("run-1", "text"));
346
- sb.processEvent(toolStartEvent("tool-run-1", "shell"));
347
- sb.processEvent(toolEndEvent("tool-run-1", { content: "stdout output" }));
348
-
349
- const tc = sb.currentStatus.messages[0].toolCalls[0];
350
- expect(tc.result).toBe("stdout output");
351
- });
352
- });
353
-
354
- describe("unknown events", () => {
355
- it("silently ignores unhandled event types", () => {
356
- const sb = makeBuilder();
357
- sb.processEvent({
358
- event: "on_chain_start",
359
- run_id: "run-1",
360
- data: {},
361
- });
362
- expect(sb.currentStatus.messages).toHaveLength(0);
363
- });
364
- });
365
-
366
- describe("clearForceFlag", () => {
367
- it("resets forceNextUpdate to false", () => {
368
- const sb = makeBuilder();
369
- sb.processEvent(chatStreamEvent("run-1", "text"));
370
- sb.processEvent(toolStartEvent("tool-run-1", "read"));
371
- expect(sb.forceNextUpdate).toBe(true);
372
-
373
- sb.clearForceFlag();
374
- expect(sb.forceNextUpdate).toBe(false);
375
- });
376
- });
377
-
378
- describe("addArtifact", () => {
379
- it("appends a new artifact and sets forceNextUpdate", () => {
380
- const sb = makeBuilder();
381
- sb.clearForceFlag();
382
-
383
- sb.addArtifact({
384
- name: "report.txt",
385
- sandboxPath: "workspace/report.txt",
386
- kind: 0,
387
- sizeBytes: 100n,
388
- storageKey: "artifacts/exec-1/report.txt",
389
- createdAt: "2026-05-19T12:00:00Z",
390
- expiresAt: "",
391
- entries: [],
392
- contentHash: "abc123",
393
- $typeName: "ai.stigmer.agentic.agentexecution.v1.ExecutionArtifact",
394
- } as any);
395
-
396
- expect(sb.currentStatus.artifacts).toHaveLength(1);
397
- expect(sb.currentStatus.artifacts[0].name).toBe("report.txt");
398
- expect(sb.forceNextUpdate).toBe(true);
399
- });
400
-
401
- it("deduplicates by sandboxPath — skips same contentHash", () => {
402
- const sb = makeBuilder();
403
- const artifact = {
404
- name: "f.txt",
405
- sandboxPath: "ws/f.txt",
406
- kind: 0,
407
- sizeBytes: 10n,
408
- storageKey: "k",
409
- createdAt: "",
410
- expiresAt: "",
411
- entries: [],
412
- contentHash: "hash1",
413
- $typeName: "ai.stigmer.agentic.agentexecution.v1.ExecutionArtifact",
414
- } as any;
415
-
416
- sb.addArtifact(artifact);
417
- sb.clearForceFlag();
418
- sb.addArtifact({ ...artifact });
419
-
420
- expect(sb.currentStatus.artifacts).toHaveLength(1);
421
- expect(sb.forceNextUpdate).toBe(false);
422
- });
423
-
424
- it("replaces artifact when contentHash changes", () => {
425
- const sb = makeBuilder();
426
- const artifact = {
427
- name: "f.txt",
428
- sandboxPath: "ws/f.txt",
429
- kind: 0,
430
- sizeBytes: 10n,
431
- storageKey: "k",
432
- createdAt: "",
433
- expiresAt: "",
434
- entries: [],
435
- contentHash: "hash1",
436
- $typeName: "ai.stigmer.agentic.agentexecution.v1.ExecutionArtifact",
437
- } as any;
438
-
439
- sb.addArtifact(artifact);
440
- sb.clearForceFlag();
441
- sb.addArtifact({ ...artifact, contentHash: "hash2", sizeBytes: 20n });
442
-
443
- expect(sb.currentStatus.artifacts).toHaveLength(1);
444
- expect(sb.currentStatus.artifacts[0].contentHash).toBe("hash2");
445
- expect(sb.forceNextUpdate).toBe(true);
446
- });
447
- });
448
-
449
- describe("addWriteBack", () => {
450
- it("appends a new writeback entry and sets forceNextUpdate", () => {
451
- const sb = makeBuilder();
452
- sb.clearForceFlag();
453
-
454
- sb.addWriteBack({
455
- workspaceEntryName: "my-app",
456
- branchName: "stigmer/a1b2c3d4",
457
- baseBranch: "main",
458
- commitSha: "abc",
459
- pullRequestUrl: "",
460
- pullRequestNumber: 0,
461
- diffSummary: "1 file changed",
462
- phase: 1,
463
- error: "",
464
- $typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
465
- } as any);
466
-
467
- expect(sb.currentStatus.workspaceWriteBacks).toHaveLength(1);
468
- expect(sb.forceNextUpdate).toBe(true);
469
- });
470
-
471
- it("upserts by workspaceEntryName", () => {
472
- const sb = makeBuilder();
473
-
474
- sb.addWriteBack({
475
- workspaceEntryName: "my-app",
476
- branchName: "stigmer/a1b2",
477
- baseBranch: "main",
478
- commitSha: "sha1",
479
- pullRequestUrl: "",
480
- pullRequestNumber: 0,
481
- diffSummary: "",
482
- phase: 1,
483
- error: "",
484
- $typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
485
- } as any);
486
-
487
- sb.addWriteBack({
488
- workspaceEntryName: "my-app",
489
- branchName: "stigmer/a1b2",
490
- baseBranch: "main",
491
- commitSha: "sha2",
492
- pullRequestUrl: "https://github.com/pr/1",
493
- pullRequestNumber: 1,
494
- diffSummary: "2 files changed",
495
- phase: 3,
496
- error: "",
497
- $typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
498
- } as any);
499
-
500
- expect(sb.currentStatus.workspaceWriteBacks).toHaveLength(1);
501
- expect(sb.currentStatus.workspaceWriteBacks[0].commitSha).toBe("sha2");
502
- expect(sb.currentStatus.workspaceWriteBacks[0].phase).toBe(3);
503
- });
504
-
505
- it("tracks multiple workspace entries independently", () => {
506
- const sb = makeBuilder();
507
-
508
- sb.addWriteBack({
509
- workspaceEntryName: "frontend",
510
- branchName: "stigmer/x",
511
- baseBranch: "main",
512
- commitSha: "a",
513
- pullRequestUrl: "",
514
- pullRequestNumber: 0,
515
- diffSummary: "",
516
- phase: 1,
517
- error: "",
518
- $typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
519
- } as any);
520
-
521
- sb.addWriteBack({
522
- workspaceEntryName: "backend",
523
- branchName: "stigmer/x",
524
- baseBranch: "develop",
525
- commitSha: "b",
526
- pullRequestUrl: "",
527
- pullRequestNumber: 0,
528
- diffSummary: "",
529
- phase: 2,
530
- error: "",
531
- $typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
532
- } as any);
533
-
534
- expect(sb.currentStatus.workspaceWriteBacks).toHaveLength(2);
535
- });
536
- });
537
-
538
- // ─────────────────────────────────────────────────────────────────────────
539
- // Approval provider integration
540
- // ─────────────────────────────────────────────────────────────────────────
541
-
542
- describe("approval provider integration", () => {
543
- function makeApprovalProvider(
544
- overrides: Partial<ApprovalPolicyProvider> = {},
545
- ): ApprovalPolicyProvider {
546
- return {
547
- policies: new Map(),
548
- toolServerMap: new Map(),
549
- globalBypass: false,
550
- ...overrides,
551
- };
552
- }
553
-
554
- function policyFor(
555
- serverSlug: string,
556
- toolName: string,
557
- message = "Approve?",
558
- ): [string, MergedToolPolicy] {
559
- return [
560
- `${serverSlug}/${toolName}`,
561
- {
562
- toolName,
563
- mcpServerSlug: serverSlug,
564
- requiresApproval: true,
565
- approvalMessage: message,
566
- source: "classifier_default",
567
- },
568
- ];
569
- }
570
-
571
- it("sets tool status to WAITING_APPROVAL when policy requires it", () => {
572
- const sb = makeBuilder();
573
- const [key, policy] = policyFor("github", "create_issue");
574
- sb.setApprovalProvider(makeApprovalProvider({
575
- policies: new Map([[key, policy]]),
576
- toolServerMap: new Map([["create_issue", "github"]]),
577
- }));
578
-
579
- sb.processEvent(chatStreamEvent("run-1", "I'll create an issue"));
580
- sb.processEvent(toolStartEvent("tool-1", "create_issue", { title: "Bug fix" }));
581
-
582
- const tc = sb.currentStatus.messages[0].toolCalls[0];
583
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
584
- expect(tc.requiresApproval).toBe(true);
585
- expect(tc.approvalMessage).toBe("Approve?");
586
- expect(tc.mcpServerSlug).toBe("github");
587
- });
588
-
589
- it("sets phase to WAITING_FOR_APPROVAL when tool requires approval", () => {
590
- const sb = makeBuilder();
591
- const [key, policy] = policyFor("github", "push");
592
- sb.setApprovalProvider(makeApprovalProvider({
593
- policies: new Map([[key, policy]]),
594
- toolServerMap: new Map([["push", "github"]]),
595
- }));
596
-
597
- sb.processEvent(chatStreamEvent("run-1", "pushing"));
598
- sb.processEvent(toolStartEvent("tool-1", "push"));
599
-
600
- expect(sb.currentStatus.phase).toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
601
- });
602
-
603
- it("does not require approval under the global bypass (spec.auto_approve_all)", () => {
604
- const sb = makeBuilder();
605
- const [key, policy] = policyFor("github", "delete_repo");
606
- sb.setApprovalProvider(makeApprovalProvider({
607
- policies: new Map([[key, policy]]),
608
- toolServerMap: new Map([["delete_repo", "github"]]),
609
- globalBypass: true,
610
- }));
611
-
612
- sb.processEvent(chatStreamEvent("run-1", "deleting"));
613
- sb.processEvent(toolStartEvent("tool-1", "delete_repo"));
614
-
615
- const tc = sb.currentStatus.messages[0].toolCalls[0];
616
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
617
- expect(tc.requiresApproval).toBe(false);
618
- });
619
-
620
- it("sets mcpServerSlug even under the global bypass (spec.auto_approve_all)", () => {
621
- const sb = makeBuilder();
622
- sb.setApprovalProvider(makeApprovalProvider({
623
- toolServerMap: new Map([["echo", "test-mcp-server"]]),
624
- globalBypass: true,
625
- }));
626
-
627
- sb.processEvent(chatStreamEvent("run-1", "echoing"));
628
- sb.processEvent(toolStartEvent("tool-1", "echo", { input: "hello" }));
629
-
630
- const tc = sb.currentStatus.messages[0].toolCalls[0];
631
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
632
- expect(tc.requiresApproval).toBe(false);
633
- expect(tc.mcpServerSlug).toBe("test-mcp-server");
634
- });
635
-
636
- it("does not require approval for tools without policies", () => {
637
- const sb = makeBuilder();
638
- sb.setApprovalProvider(makeApprovalProvider({
639
- policies: new Map(),
640
- toolServerMap: new Map([["read", "filesystem"]]),
641
- }));
642
-
643
- sb.processEvent(chatStreamEvent("run-1", "reading"));
644
- sb.processEvent(toolStartEvent("tool-1", "read"));
645
-
646
- const tc = sb.currentStatus.messages[0].toolCalls[0];
647
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
648
- expect(tc.mcpServerSlug).toBe("filesystem");
649
- });
650
-
651
- it("does not require approval for tools not in toolServerMap", () => {
652
- const sb = makeBuilder();
653
- const [key, policy] = policyFor("github", "create_pr");
654
- sb.setApprovalProvider(makeApprovalProvider({
655
- policies: new Map([[key, policy]]),
656
- toolServerMap: new Map(),
657
- }));
658
-
659
- sb.processEvent(chatStreamEvent("run-1", "creating PR"));
660
- sb.processEvent(toolStartEvent("tool-1", "create_pr"));
661
-
662
- const tc = sb.currentStatus.messages[0].toolCalls[0];
663
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
664
- });
665
-
666
- it("resolves {{args.field}} placeholders in approval message", () => {
667
- const sb = makeBuilder();
668
- const [key, policy] = policyFor(
669
- "github", "create_issue",
670
- "Create issue '{{args.title}}' in {{args.repo}}?",
671
- );
672
- sb.setApprovalProvider(makeApprovalProvider({
673
- policies: new Map([[key, policy]]),
674
- toolServerMap: new Map([["create_issue", "github"]]),
675
- }));
676
-
677
- sb.processEvent(chatStreamEvent("run-1", "creating issue"));
678
- sb.processEvent(toolStartEvent("tool-1", "create_issue", {
679
- title: "Fix crash",
680
- repo: "stigmer/stigmer",
681
- }));
682
-
683
- const tc = sb.currentStatus.messages[0].toolCalls[0];
684
- expect(tc.approvalMessage).toBe("Create issue 'Fix crash' in stigmer/stigmer?");
685
- });
686
-
687
- it("sets approvalRequestedAt when approval required", () => {
688
- const sb = makeBuilder();
689
- const [key, policy] = policyFor("db", "drop_table");
690
- sb.setApprovalProvider(makeApprovalProvider({
691
- policies: new Map([[key, policy]]),
692
- toolServerMap: new Map([["drop_table", "db"]]),
693
- }));
694
-
695
- sb.processEvent(chatStreamEvent("run-1", "dropping"));
696
- sb.processEvent(toolStartEvent("tool-1", "drop_table"));
697
-
698
- const tc = sb.currentStatus.messages[0].toolCalls[0];
699
- expect(tc.approvalRequestedAt).toBeTruthy();
700
- expect(tc.approvalRequestedAt).toContain("T");
701
- });
702
- });
703
-
704
- // ─────────────────────────────────────────────────────────────────────────
705
- // Args sanitization (argsPreview)
706
- // ─────────────────────────────────────────────────────────────────────────
707
-
708
- describe("args preview sanitization", () => {
709
- function builderWithApproval(
710
- toolName: string,
711
- serverSlug: string,
712
- ): StatusBuilder {
713
- const sb = makeBuilder();
714
- sb.setApprovalProvider({
715
- policies: new Map([
716
- [`${serverSlug}/${toolName}`, {
717
- toolName,
718
- mcpServerSlug: serverSlug,
719
- requiresApproval: true,
720
- approvalMessage: "Approve?",
721
- source: "classifier_default",
722
- }],
723
- ]),
724
- toolServerMap: new Map([[toolName, serverSlug]]),
725
- globalBypass: false,
726
- });
727
- return sb;
728
- }
729
-
730
- it("redacts sensitive arg keys", () => {
731
- const sb = builderWithApproval("connect", "db");
732
- sb.processEvent(chatStreamEvent("run-1", "connecting"));
733
- sb.processEvent(toolStartEvent("tool-1", "connect", {
734
- host: "localhost",
735
- password: "super-secret",
736
- token: "jwt-token",
737
- }));
738
-
739
- const tc = sb.currentStatus.messages[0].toolCalls[0];
740
- const preview = JSON.parse(tc.argsPreview);
741
- expect(preview.host).toBe("localhost");
742
- expect(preview.password).toBe("[REDACTED]");
743
- expect(preview.token).toBe("[REDACTED]");
744
- });
745
-
746
- it("redacts case-insensitively", () => {
747
- const sb = builderWithApproval("call", "api");
748
- sb.processEvent(chatStreamEvent("run-1", "calling"));
749
- sb.processEvent(toolStartEvent("tool-1", "call", {
750
- Authorization: "Bearer xyz",
751
- API_KEY: "key-123",
752
- }));
753
-
754
- const tc = sb.currentStatus.messages[0].toolCalls[0];
755
- const preview = JSON.parse(tc.argsPreview);
756
- expect(preview.Authorization).toBe("[REDACTED]");
757
- expect(preview.API_KEY).toBe("[REDACTED]");
758
- });
759
-
760
- it("truncates long args preview", () => {
761
- const sb = builderWithApproval("write", "fs");
762
- sb.processEvent(chatStreamEvent("run-1", "writing"));
763
- sb.processEvent(toolStartEvent("tool-1", "write", {
764
- content: "x".repeat(1000),
765
- }));
766
-
767
- const tc = sb.currentStatus.messages[0].toolCalls[0];
768
- expect(tc.argsPreview.length).toBeLessThanOrEqual(501);
769
- expect(tc.argsPreview.endsWith("…")).toBe(true);
770
- });
771
-
772
- it("does not set argsPreview for non-approval tools", () => {
773
- const sb = makeBuilder();
774
- sb.processEvent(chatStreamEvent("run-1", "reading"));
775
- sb.processEvent(toolStartEvent("tool-1", "read", { path: "/foo" }));
776
-
777
- const tc = sb.currentStatus.messages[0].toolCalls[0];
778
- expect(tc.argsPreview).toBe("");
779
- });
780
-
781
- it("sets argsPreview as empty when args serialization fails", () => {
782
- const sb = builderWithApproval("call", "api");
783
- sb.processEvent(chatStreamEvent("run-1", "calling"));
784
-
785
- const circular: Record<string, unknown> = {};
786
- circular.self = circular;
787
- sb.processEvent(toolStartEvent("tool-1", "call", circular));
788
-
789
- const tc = sb.currentStatus.messages[0].toolCalls[0];
790
- expect(tc.argsPreview).toBe("");
791
- });
792
- });
793
-
794
- // ─────────────────────────────────────────────────────────────────────────
795
- // Namespace extraction
796
- // ─────────────────────────────────────────────────────────────────────────
797
-
798
- describe("namespace extraction", () => {
799
- it("extracts namespace from langgraph_checkpoint_ns", () => {
800
- const sb = makeBuilder();
801
- sb.processEvent({
802
- event: "on_chat_model_stream",
803
- name: "ChatAnthropic",
804
- run_id: "run-ns-1",
805
- data: { chunk: { content: "hello" } },
806
- metadata: { langgraph_checkpoint_ns: "tools:task-123|agent_node:inner" },
807
- });
808
-
809
- expect(sb.currentStatus.messages).toHaveLength(1);
810
- });
811
-
812
- it("extracts namespace from checkpoint_ns fallback", () => {
813
- const sb = makeBuilder();
814
- sb.processEvent({
815
- event: "on_chat_model_stream",
816
- name: "ChatAnthropic",
817
- run_id: "run-ns-2",
818
- data: { chunk: { content: "hello" } },
819
- metadata: { checkpoint_ns: "some-namespace" },
820
- });
821
-
822
- expect(sb.currentStatus.messages).toHaveLength(1);
823
- });
824
-
825
- it("uses empty namespace when no metadata", () => {
826
- const sb = makeBuilder();
827
- sb.processEvent({
828
- event: "on_chat_model_stream",
829
- run_id: "run-ns-3",
830
- data: { chunk: { content: "hello" } },
831
- });
832
-
833
- expect(sb.currentStatus.messages).toHaveLength(1);
834
- });
835
-
836
- it("different namespaces create separate message trees", () => {
837
- const sb = makeBuilder();
838
-
839
- sb.processEvent({
840
- event: "on_chat_model_stream",
841
- name: "ChatAnthropic",
842
- run_id: "main-run",
843
- data: { chunk: { content: "main text" } },
844
- metadata: {},
845
- });
846
-
847
- sb.processEvent({
848
- event: "on_chat_model_stream",
849
- name: "ChatAnthropic",
850
- run_id: "sub-run",
851
- data: { chunk: { content: "sub text" } },
852
- metadata: { langgraph_checkpoint_ns: "tools:task-1|agent_node:inner" },
853
- });
854
-
855
- expect(sb.currentStatus.messages).toHaveLength(2);
856
- expect(sb.currentStatus.messages[0].content).toBe("main text");
857
- expect(sb.currentStatus.messages[1].content).toBe("sub text");
858
- });
859
-
860
- it("same namespace reuses message within the same run_id", () => {
861
- const sb = makeBuilder();
862
- const ns = "tools:task-1|agent_node:inner";
863
-
864
- sb.processEvent({
865
- event: "on_chat_model_stream",
866
- name: "ChatAnthropic",
867
- run_id: "sub-run-1",
868
- data: { chunk: { content: "part 1" } },
869
- metadata: { langgraph_checkpoint_ns: ns },
870
- });
871
-
872
- sb.processEvent({
873
- event: "on_chat_model_stream",
874
- name: "ChatAnthropic",
875
- run_id: "sub-run-1",
876
- data: { chunk: { content: " part 2" } },
877
- metadata: { langgraph_checkpoint_ns: ns },
878
- });
879
-
880
- expect(sb.currentStatus.messages).toHaveLength(1);
881
- expect(sb.currentStatus.messages[0].content).toBe("part 1 part 2");
882
- });
883
- });
884
-
885
- // ─────────────────────────────────────────────────────────────────────────
886
- // Usage accumulation edge cases
887
- // ─────────────────────────────────────────────────────────────────────────
888
-
889
- describe("usage accumulation edge cases", () => {
890
- it("handles missing usage_metadata gracefully", () => {
891
- const sb = makeBuilder();
892
- sb.processEvent(chatStreamEvent("run-1", "text"));
893
- sb.processEvent(chatEndEvent("run-1"));
894
-
895
- expect(sb.currentStatus.streamingUsage).toBeUndefined();
896
- });
897
-
898
- it("handles usage with zero tokens", () => {
899
- const sb = makeBuilder();
900
- sb.processEvent(chatStreamEvent("run-1", "text"));
901
- sb.processEvent(chatEndEvent("run-1", {
902
- input_tokens: 0,
903
- output_tokens: 0,
904
- }));
905
-
906
- const usage = sb.currentStatus.streamingUsage!;
907
- expect(usage.inputTokens).toBe(0n);
908
- expect(usage.outputTokens).toBe(0n);
909
- expect(usage.turnCount).toBe(1);
910
- });
911
-
912
- it("handles non-numeric token values gracefully", () => {
913
- const sb = makeBuilder();
914
- sb.processEvent(chatStreamEvent("run-1", "text"));
915
- sb.processEvent(chatEndEvent("run-1", {
916
- input_tokens: "not a number",
917
- output_tokens: null,
918
- cache_read_input_tokens: undefined,
919
- }));
920
-
921
- const usage = sb.currentStatus.streamingUsage!;
922
- expect(usage.inputTokens).toBe(0n);
923
- expect(usage.outputTokens).toBe(0n);
924
- expect(usage.cacheReadTokens).toBe(0n);
925
- expect(usage.turnCount).toBe(1);
926
- });
927
-
928
- it("handles bigint token values", () => {
929
- const sb = makeBuilder();
930
- sb.processEvent(chatStreamEvent("run-1", "text"));
931
- sb.processEvent(chatEndEvent("run-1", {
932
- input_tokens: 100n,
933
- output_tokens: 50n,
934
- }));
935
-
936
- const usage = sb.currentStatus.streamingUsage!;
937
- expect(usage.inputTokens).toBe(100n);
938
- expect(usage.outputTokens).toBe(50n);
939
- });
940
-
941
- it("computes totalTokens as sum of all token fields", () => {
942
- const sb = makeBuilder();
943
- sb.processEvent(chatStreamEvent("run-1", "text"));
944
- sb.processEvent(chatEndEvent("run-1", {
945
- input_tokens: 100,
946
- output_tokens: 50,
947
- cache_read_input_tokens: 20,
948
- cache_creation_input_tokens: 10,
949
- }));
950
-
951
- const usage = sb.currentStatus.streamingUsage!;
952
- expect(usage.totalTokens).toBe(180n);
953
- });
954
-
955
- it("usage from data.usage_metadata takes precedence when output missing", () => {
956
- const sb = makeBuilder();
957
- sb.processEvent(chatStreamEvent("run-1", "text"));
958
-
959
- sb.processEvent({
960
- event: "on_chat_model_end",
961
- name: "ChatAnthropic",
962
- run_id: "run-1",
963
- data: {
964
- output: {},
965
- usage_metadata: {
966
- input_tokens: 42,
967
- output_tokens: 13,
968
- },
969
- },
970
- });
971
-
972
- const usage = sb.currentStatus.streamingUsage!;
973
- expect(usage.inputTokens).toBe(42n);
974
- expect(usage.outputTokens).toBe(13n);
975
- });
976
-
977
- it("sets observedAt timestamp on usage snapshot", () => {
978
- const sb = makeBuilder();
979
- sb.processEvent(chatStreamEvent("run-1", "text"));
980
- sb.processEvent(chatEndEvent("run-1", {
981
- input_tokens: 10,
982
- output_tokens: 5,
983
- }));
984
-
985
- const usage = sb.currentStatus.streamingUsage!;
986
- expect(usage.observedAt).toBeTruthy();
987
- expect(usage.observedAt).toContain("T");
988
- });
989
- });
990
-
991
- // ─────────────────────────────────────────────────────────────────────────
992
- // Error handling in event handlers
993
- // ─────────────────────────────────────────────────────────────────────────
994
-
995
- describe("event handler error resilience", () => {
996
- it("does not throw when handler encounters an error", () => {
997
- const sb = makeBuilder();
998
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
999
-
1000
- sb.processEvent({
1001
- event: "on_chat_model_stream",
1002
- run_id: "run-err",
1003
- data: { chunk: { content: { toString() { throw new Error("boom"); } } } },
1004
- });
1005
-
1006
- errorSpy.mockRestore();
1007
- });
1008
-
1009
- it("logs error with execution context on handler failure", () => {
1010
- const sb = makeBuilder();
1011
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
1012
-
1013
- // Need a tool call in the map so handleToolEnd reaches the code
1014
- // that accesses event.data.output (which we make throw)
1015
- sb.processEvent(chatStreamEvent("run-1", "text"));
1016
- sb.processEvent(toolStartEvent("bad-run", "read"));
1017
-
1018
- sb.processEvent({
1019
- event: "on_tool_end",
1020
- name: "read",
1021
- run_id: "bad-run",
1022
- data: { get output() { throw new Error("property access failed"); } } as any,
1023
- });
1024
-
1025
- expect(errorSpy).toHaveBeenCalled();
1026
- const logMsg = errorSpy.mock.calls[0][0] as string;
1027
- expect(logMsg).toContain("exec-test");
1028
- expect(logMsg).toContain("on_tool_end");
1029
- expect(logMsg).toContain("bad-run");
1030
-
1031
- errorSpy.mockRestore();
1032
- });
1033
- });
1034
-
1035
- // ─────────────────────────────────────────────────────────────────────────
1036
- // Tool result extraction edge cases
1037
- // ─────────────────────────────────────────────────────────────────────────
1038
-
1039
- describe("tool result extraction", () => {
1040
- it("extracts string result directly", () => {
1041
- const sb = makeBuilder();
1042
- sb.processEvent(chatStreamEvent("run-1", "text"));
1043
- sb.processEvent(toolStartEvent("tool-1", "read"));
1044
- sb.processEvent(toolEndEvent("tool-1", "file contents"));
1045
-
1046
- expect(sb.currentStatus.messages[0].toolCalls[0].result).toBe("file contents");
1047
- });
1048
-
1049
- it("extracts content from object result", () => {
1050
- const sb = makeBuilder();
1051
- sb.processEvent(chatStreamEvent("run-1", "text"));
1052
- sb.processEvent(toolStartEvent("tool-1", "shell"));
1053
- sb.processEvent(toolEndEvent("tool-1", { content: "stdout output" }));
1054
-
1055
- expect(sb.currentStatus.messages[0].toolCalls[0].result).toBe("stdout output");
1056
- });
1057
-
1058
- it("JSON-serializes object without content field", () => {
1059
- const sb = makeBuilder();
1060
- sb.processEvent(chatStreamEvent("run-1", "text"));
1061
- sb.processEvent(toolStartEvent("tool-1", "api"));
1062
- sb.processEvent(toolEndEvent("tool-1", { status: 200, body: "ok" }));
1063
-
1064
- const result = sb.currentStatus.messages[0].toolCalls[0].result;
1065
- expect(JSON.parse(result)).toEqual({ status: 200, body: "ok" });
1066
- });
1067
-
1068
- it("JSON-serializes full data when output is undefined", () => {
1069
- const sb = makeBuilder();
1070
- sb.processEvent(chatStreamEvent("run-1", "text"));
1071
- sb.processEvent(toolStartEvent("tool-1", "noop"));
1072
- sb.processEvent({
1073
- event: "on_tool_end",
1074
- name: "noop",
1075
- run_id: "tool-1",
1076
- data: { something: "else" },
1077
- });
1078
-
1079
- const result = sb.currentStatus.messages[0].toolCalls[0].result;
1080
- const parsed = JSON.parse(result);
1081
- expect(parsed.something).toBe("else");
1082
- });
1083
-
1084
- it("returns serialization error for circular references", () => {
1085
- const sb = makeBuilder();
1086
- sb.processEvent(chatStreamEvent("run-1", "text"));
1087
- sb.processEvent(toolStartEvent("tool-1", "bad"));
1088
-
1089
- const circular: Record<string, unknown> = { a: 1 };
1090
- circular.self = circular;
1091
-
1092
- sb.processEvent({
1093
- event: "on_tool_end",
1094
- name: "bad",
1095
- run_id: "tool-1",
1096
- data: { output: circular },
1097
- });
1098
-
1099
- expect(sb.currentStatus.messages[0].toolCalls[0].result).toBe("[serialization error]");
1100
- });
1101
- });
1102
-
1103
- // ─────────────────────────────────────────────────────────────────────────
1104
- // Multiple concurrent tool calls
1105
- // ─────────────────────────────────────────────────────────────────────────
1106
-
1107
- describe("multiple concurrent tool calls", () => {
1108
- it("tracks multiple running tools on the same message", () => {
1109
- const sb = makeBuilder();
1110
- sb.processEvent(chatStreamEvent("run-1", "I'll do two things"));
1111
- sb.processEvent(toolStartEvent("tool-a", "read"));
1112
- sb.processEvent(toolStartEvent("tool-b", "search"));
1113
-
1114
- const msg = sb.currentStatus.messages[0];
1115
- expect(msg.toolCalls).toHaveLength(2);
1116
- expect(msg.toolCalls[0].name).toBe("read");
1117
- expect(msg.toolCalls[1].name).toBe("search");
1118
- expect(msg.toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
1119
- expect(msg.toolCalls[1].status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
1120
- });
1121
-
1122
- it("completes tools independently", () => {
1123
- const sb = makeBuilder();
1124
- sb.processEvent(chatStreamEvent("run-1", "parallel work"));
1125
- sb.processEvent(toolStartEvent("tool-a", "read"));
1126
- sb.processEvent(toolStartEvent("tool-b", "search"));
1127
-
1128
- sb.processEvent(toolEndEvent("tool-a", "file data"));
1129
-
1130
- const msg = sb.currentStatus.messages[0];
1131
- expect(msg.toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
1132
- expect(msg.toolCalls[1].status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
1133
-
1134
- sb.processEvent(toolEndEvent("tool-b", "search results"));
1135
- expect(msg.toolCalls[1].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
1136
- });
1137
-
1138
- it("handles interleaved tool start and end events", () => {
1139
- const sb = makeBuilder();
1140
- sb.processEvent(chatStreamEvent("run-1", "working"));
1141
- sb.processEvent(toolStartEvent("tool-1", "read"));
1142
- sb.processEvent(toolEndEvent("tool-1", "data 1"));
1143
- sb.processEvent(toolStartEvent("tool-2", "write"));
1144
- sb.processEvent(toolEndEvent("tool-2", "done"));
1145
-
1146
- const msg = sb.currentStatus.messages[0];
1147
- expect(msg.toolCalls).toHaveLength(2);
1148
- expect(msg.toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
1149
- expect(msg.toolCalls[1].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
1150
- });
1151
- });
1152
-
1153
- // ─────────────────────────────────────────────────────────────────────────
1154
- // ExecutionState lifecycle
1155
- // ─────────────────────────────────────────────────────────────────────────
1156
-
1157
- describe("execution state lifecycle", () => {
1158
- it("tool start times are cleaned up on tool end", () => {
1159
- const sb = makeBuilder();
1160
- sb.processEvent(chatStreamEvent("run-1", "text"));
1161
- sb.processEvent(toolStartEvent("tool-1", "read"));
1162
- sb.processEvent(toolEndEvent("tool-1", "done"));
1163
-
1164
- // Internal state should have cleaned up the start time
1165
- // Verified by the fact that there's no leak — the TS implementation
1166
- // deletes from toolStartTimes in handleToolEnd
1167
- });
1168
-
1169
- it("handles chat_model_end without prior stream (no message exists)", () => {
1170
- const sb = makeBuilder();
1171
- sb.processEvent(chatEndEvent("orphan-run", {
1172
- input_tokens: 50,
1173
- output_tokens: 25,
1174
- }));
1175
-
1176
- // Usage should still accumulate even without a prior message
1177
- const usage = sb.currentStatus.streamingUsage!;
1178
- expect(usage.inputTokens).toBe(50n);
1179
- expect(usage.outputTokens).toBe(25n);
1180
- expect(usage.turnCount).toBe(1);
1181
- });
1182
-
1183
- it("handles tool_end for unknown run_id gracefully", () => {
1184
- const sb = makeBuilder();
1185
- sb.processEvent(toolEndEvent("never-started", "result"));
1186
- // Should not throw, no messages or tool calls created
1187
- expect(sb.currentStatus.messages).toHaveLength(0);
1188
- });
1189
-
1190
- it("creates AI message for tool_start without prior text stream", () => {
1191
- const sb = makeBuilder();
1192
- sb.processEvent(toolStartEvent("orphan-tool", "read"));
1193
- expect(sb.currentStatus.messages).toHaveLength(1);
1194
- expect(sb.currentStatus.messages[0].toolCalls).toHaveLength(1);
1195
- expect(sb.currentStatus.messages[0].toolCalls[0].name).toBe("read");
1196
- });
1197
- });
1198
-
1199
- // ─────────────────────────────────────────────────────────────────────────
1200
- // Thinking + text interleaving
1201
- // ─────────────────────────────────────────────────────────────────────────
1202
-
1203
- describe("thinking and text interleaving", () => {
1204
- it("creates separate messages for thinking and text in sequence", () => {
1205
- const sb = makeBuilder();
1206
-
1207
- sb.processEvent(chatStreamEvent("run-1", [
1208
- { type: "thinking", thinking: "Let me think..." },
1209
- ]));
1210
- sb.processEvent(chatStreamEvent("run-1", [
1211
- { type: "text", text: "Here is my answer" },
1212
- ]));
1213
-
1214
- expect(sb.currentStatus.messages).toHaveLength(2);
1215
- expect(sb.currentStatus.messages[0].type).toBe(MessageType.MESSAGE_THINKING);
1216
- expect(sb.currentStatus.messages[0].content).toBe("Let me think...");
1217
- expect(sb.currentStatus.messages[1].type).toBe(MessageType.MESSAGE_AI);
1218
- expect(sb.currentStatus.messages[1].content).toBe("Here is my answer");
1219
- });
1220
-
1221
- it("multiple thinking blocks accumulate in same thinking message", () => {
1222
- const sb = makeBuilder();
1223
-
1224
- sb.processEvent(chatStreamEvent("run-1", [
1225
- { type: "thinking", thinking: "Step 1. " },
1226
- ]));
1227
- sb.processEvent(chatStreamEvent("run-1", [
1228
- { type: "thinking", thinking: "Step 2. " },
1229
- ]));
1230
- sb.processEvent(chatStreamEvent("run-1", [
1231
- { type: "thinking", thinking: "Step 3." },
1232
- ]));
1233
-
1234
- expect(sb.currentStatus.messages).toHaveLength(1);
1235
- expect(sb.currentStatus.messages[0].content).toBe("Step 1. Step 2. Step 3.");
1236
- });
1237
-
1238
- it("thinking from different namespaces creates separate thinking messages", () => {
1239
- const sb = makeBuilder();
1240
-
1241
- sb.processEvent({
1242
- event: "on_chat_model_stream",
1243
- name: "ChatAnthropic",
1244
- run_id: "run-main",
1245
- data: { chunk: { content: [{ type: "thinking", thinking: "main thinking" }] } },
1246
- metadata: {},
1247
- });
1248
-
1249
- sb.processEvent({
1250
- event: "on_chat_model_stream",
1251
- name: "ChatAnthropic",
1252
- run_id: "run-sub",
1253
- data: { chunk: { content: [{ type: "thinking", thinking: "sub thinking" }] } },
1254
- metadata: { langgraph_checkpoint_ns: "tools:task-1" },
1255
- });
1256
-
1257
- expect(sb.currentStatus.messages).toHaveLength(2);
1258
- expect(sb.currentStatus.messages[0].content).toBe("main thinking");
1259
- expect(sb.currentStatus.messages[1].content).toBe("sub thinking");
1260
- });
1261
- });
1262
-
1263
- // ─────────────────────────────────────────────────────────────────────────
1264
- // Content block edge cases
1265
- // ─────────────────────────────────────────────────────────────────────────
1266
-
1267
- describe("content block edge cases", () => {
1268
- it("ignores non-text non-thinking blocks", () => {
1269
- const sb = makeBuilder();
1270
- sb.processEvent(chatStreamEvent("run-1", [
1271
- { type: "image_url", image_url: "https://example.com/img.png" },
1272
- ]));
1273
-
1274
- expect(sb.currentStatus.messages).toHaveLength(0);
1275
- });
1276
-
1277
- it("handles null content blocks gracefully", () => {
1278
- const sb = makeBuilder();
1279
- sb.processEvent(chatStreamEvent("run-1", [null as any]));
1280
-
1281
- expect(sb.currentStatus.messages).toHaveLength(0);
1282
- });
1283
-
1284
- it("handles empty array content", () => {
1285
- const sb = makeBuilder();
1286
- sb.processEvent(chatStreamEvent("run-1", []));
1287
-
1288
- expect(sb.currentStatus.messages).toHaveLength(0);
1289
- });
1290
-
1291
- it("handles numeric content gracefully", () => {
1292
- const sb = makeBuilder();
1293
- sb.processEvent(chatStreamEvent("run-1", 42 as any));
1294
-
1295
- expect(sb.currentStatus.messages).toHaveLength(0);
1296
- });
1297
- });
1298
-
1299
- // ═══════════════════════════════════════════════════════════════════════
1300
- // GOLDEN SEQUENCES
1301
- //
1302
- // Realistic multi-event conversation sequences that assert on the
1303
- // complete AgentExecutionStatus shape. These serve as the regression
1304
- // contract for V3StatusBuilder in Phase 2 of the v3 streaming migration.
1305
- //
1306
- // Each sequence represents a real-world conversation pattern and
1307
- // validates the final proto output end-to-end through StatusBuilder.
1308
- // ═══════════════════════════════════════════════════════════════════════
1309
-
1310
- describe("golden sequences", () => {
1311
-
1312
- describe("plain chat — 2-turn text-only conversation", () => {
1313
- it("produces 2 AI messages with accumulated usage", () => {
1314
- const sb = makeBuilder();
1315
-
1316
- // Turn 1: stream tokens → end
1317
- sb.processEvent(chatStreamEvent("run-1", "Hello, "));
1318
- sb.processEvent(chatStreamEvent("run-1", "I can help "));
1319
- sb.processEvent(chatStreamEvent("run-1", "with that."));
1320
- sb.processEvent(chatEndEvent("run-1", {
1321
- input_tokens: 50,
1322
- output_tokens: 12,
1323
- }));
1324
-
1325
- // Turn 2: stream tokens → end
1326
- sb.processEvent(chatStreamEvent("run-2", "Here is "));
1327
- sb.processEvent(chatStreamEvent("run-2", "more detail."));
1328
- sb.processEvent(chatEndEvent("run-2", {
1329
- input_tokens: 80,
1330
- output_tokens: 8,
1331
- }));
1332
-
1333
- const status = sb.currentStatus;
1334
-
1335
- // 2 AI messages, both finished streaming
1336
- expect(status.messages).toHaveLength(2);
1337
- expect(status.messages[0].type).toBe(MessageType.MESSAGE_AI);
1338
- expect(status.messages[0].content).toBe("Hello, I can help with that.");
1339
- expect(status.messages[0].isStreaming).toBe(false);
1340
- expect(status.messages[0].toolCalls).toHaveLength(0);
1341
-
1342
- expect(status.messages[1].type).toBe(MessageType.MESSAGE_AI);
1343
- expect(status.messages[1].content).toBe("Here is more detail.");
1344
- expect(status.messages[1].isStreaming).toBe(false);
1345
- expect(status.messages[1].toolCalls).toHaveLength(0);
1346
-
1347
- // Usage accumulated across turns
1348
- const usage = status.streamingUsage!;
1349
- expect(usage.inputTokens).toBe(130n);
1350
- expect(usage.outputTokens).toBe(20n);
1351
- expect(usage.turnCount).toBe(2);
1352
- expect(usage.totalTokens).toBe(150n);
1353
-
1354
- // Phase stays IN_PROGRESS (terminal phase set by caller)
1355
- expect(status.phase).toBe(ExecutionPhase.EXECUTION_IN_PROGRESS);
1356
- });
1357
- });
1358
-
1359
- describe("Anthropic thinking + text", () => {
1360
- it("produces THINKING then AI messages with correct turn boundaries", () => {
1361
- const sb = makeBuilder();
1362
-
1363
- // Turn 1: thinking blocks → text blocks (same run_id)
1364
- sb.processEvent(chatStreamEvent("run-1", [
1365
- { type: "thinking", thinking: "Let me analyze " },
1366
- ]));
1367
- sb.processEvent(chatStreamEvent("run-1", [
1368
- { type: "thinking", thinking: "this problem carefully." },
1369
- ]));
1370
- sb.processEvent(chatStreamEvent("run-1", [
1371
- { type: "text", text: "Based on my analysis, " },
1372
- ]));
1373
- sb.processEvent(chatStreamEvent("run-1", [
1374
- { type: "text", text: "here is the answer." },
1375
- ]));
1376
- sb.processEvent(chatEndEvent("run-1", {
1377
- input_tokens: 200,
1378
- output_tokens: 80,
1379
- cache_read_input_tokens: 50,
1380
- }));
1381
-
1382
- // Turn 2: new run_id, text only (follow-up)
1383
- sb.processEvent(chatStreamEvent("run-2", "Let me elaborate."));
1384
- sb.processEvent(chatEndEvent("run-2", {
1385
- input_tokens: 300,
1386
- output_tokens: 40,
1387
- }));
1388
-
1389
- const status = sb.currentStatus;
1390
-
1391
- // 3 messages: thinking, AI (turn 1), AI (turn 2)
1392
- expect(status.messages).toHaveLength(3);
1393
-
1394
- expect(status.messages[0].type).toBe(MessageType.MESSAGE_THINKING);
1395
- expect(status.messages[0].content).toBe("Let me analyze this problem carefully.");
1396
- expect(status.messages[0].isStreaming).toBe(true); // thinking messages aren't finalized by chat_model_end
1397
-
1398
- expect(status.messages[1].type).toBe(MessageType.MESSAGE_AI);
1399
- expect(status.messages[1].content).toBe("Based on my analysis, here is the answer.");
1400
- expect(status.messages[1].isStreaming).toBe(false);
1401
-
1402
- expect(status.messages[2].type).toBe(MessageType.MESSAGE_AI);
1403
- expect(status.messages[2].content).toBe("Let me elaborate.");
1404
- expect(status.messages[2].isStreaming).toBe(false);
1405
-
1406
- // Usage across both turns with cache tokens
1407
- const usage = status.streamingUsage!;
1408
- expect(usage.inputTokens).toBe(500n);
1409
- expect(usage.outputTokens).toBe(120n);
1410
- expect(usage.cacheReadTokens).toBe(50n);
1411
- expect(usage.turnCount).toBe(2);
1412
- expect(usage.totalTokens).toBe(670n);
1413
- });
1414
- });
1415
-
1416
- describe("single tool call — ReAct pattern", () => {
1417
- it("produces AI message with tool call followed by response", () => {
1418
- const sb = makeBuilder();
1419
-
1420
- // Turn 1: model decides to call a tool
1421
- sb.processEvent(chatStreamEvent("run-1", "I'll read the file for you."));
1422
- sb.processEvent(chatEndEvent("run-1", {
1423
- input_tokens: 100,
1424
- output_tokens: 15,
1425
- }));
1426
-
1427
- // Tool execution
1428
- sb.processEvent(toolStartEvent("tool-run-1", "read_file", { path: "/src/main.ts" }));
1429
- sb.processEvent(toolEndEvent("tool-run-1", "export function main() { console.log('hello'); }"));
1430
-
1431
- // Turn 2: model responds with tool result
1432
- sb.processEvent(chatStreamEvent("run-2", "The file contains a main function that logs 'hello'."));
1433
- sb.processEvent(chatEndEvent("run-2", {
1434
- input_tokens: 200,
1435
- output_tokens: 20,
1436
- }));
1437
-
1438
- const status = sb.currentStatus;
1439
-
1440
- // 2 messages: AI with tool call, AI response
1441
- expect(status.messages).toHaveLength(2);
1442
-
1443
- const firstMsg = status.messages[0];
1444
- expect(firstMsg.type).toBe(MessageType.MESSAGE_AI);
1445
- expect(firstMsg.content).toBe("I'll read the file for you.");
1446
- expect(firstMsg.isStreaming).toBe(false);
1447
- expect(firstMsg.toolCalls).toHaveLength(1);
1448
-
1449
- const tc = firstMsg.toolCalls[0];
1450
- expect(tc.id).toBe("tool-run-1");
1451
- expect(tc.name).toBe("read_file");
1452
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
1453
- expect(tc.args).toEqual({ path: "/src/main.ts" });
1454
- expect(tc.result).toBe("export function main() { console.log('hello'); }");
1455
- expect(tc.error).toBe("");
1456
- expect(tc.isStreaming).toBe(false);
1457
- expect(tc.startedAt).toBeTruthy();
1458
- expect(tc.completedAt).toBeTruthy();
1459
-
1460
- const secondMsg = status.messages[1];
1461
- expect(secondMsg.type).toBe(MessageType.MESSAGE_AI);
1462
- expect(secondMsg.content).toBe("The file contains a main function that logs 'hello'.");
1463
- expect(secondMsg.toolCalls).toHaveLength(0);
1464
-
1465
- // Usage accumulated across both LLM turns
1466
- const usage = status.streamingUsage!;
1467
- expect(usage.inputTokens).toBe(300n);
1468
- expect(usage.outputTokens).toBe(35n);
1469
- expect(usage.turnCount).toBe(2);
1470
- });
1471
- });
1472
-
1473
- describe("tool error — failed tool call", () => {
1474
- it("marks tool as FAILED with error string", () => {
1475
- const sb = makeBuilder();
1476
-
1477
- sb.processEvent(chatStreamEvent("run-1", "I'll write to the file."));
1478
- sb.processEvent(chatEndEvent("run-1", {
1479
- input_tokens: 50,
1480
- output_tokens: 10,
1481
- }));
1482
-
1483
- sb.processEvent(toolStartEvent("tool-run-1", "write_file", {
1484
- path: "/etc/passwd",
1485
- content: "malicious",
1486
- }));
1487
- sb.processEvent(toolEndEvent("tool-run-1", {
1488
- error: "EACCES: permission denied, open '/etc/passwd'",
1489
- }));
1490
-
1491
- // Model recovers with error explanation
1492
- sb.processEvent(chatStreamEvent("run-2", "I don't have permission to write to that file."));
1493
- sb.processEvent(chatEndEvent("run-2", {
1494
- input_tokens: 120,
1495
- output_tokens: 18,
1496
- }));
1497
-
1498
- const status = sb.currentStatus;
1499
-
1500
- expect(status.messages).toHaveLength(2);
1501
-
1502
- const tc = status.messages[0].toolCalls[0];
1503
- expect(tc.name).toBe("write_file");
1504
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
1505
- expect(tc.error).toBe("EACCES: permission denied, open '/etc/passwd'");
1506
- expect(tc.result).toBe("");
1507
- expect(tc.completedAt).toBeTruthy();
1508
-
1509
- expect(status.messages[1].content).toBe(
1510
- "I don't have permission to write to that file.",
1511
- );
1512
-
1513
- const usage = status.streamingUsage!;
1514
- expect(usage.turnCount).toBe(2);
1515
- });
1516
- });
1517
-
1518
- describe("multi-tool concurrent — parallel tool execution", () => {
1519
- it("tracks concurrent tools independently on same message", () => {
1520
- const sb = makeBuilder();
1521
-
1522
- // Model decides to call two tools in parallel
1523
- sb.processEvent(chatStreamEvent("run-1", "I'll search both files."));
1524
- sb.processEvent(chatEndEvent("run-1", {
1525
- input_tokens: 60,
1526
- output_tokens: 10,
1527
- }));
1528
-
1529
- sb.processEvent(toolStartEvent("tool-a", "read_file", { path: "/a.ts" }));
1530
- sb.processEvent(toolStartEvent("tool-b", "read_file", { path: "/b.ts" }));
1531
-
1532
- // Tool B finishes first (out of order)
1533
- sb.processEvent(toolEndEvent("tool-b", "content of b.ts"));
1534
- sb.processEvent(toolEndEvent("tool-a", "content of a.ts"));
1535
-
1536
- // Model synthesizes results
1537
- sb.processEvent(chatStreamEvent("run-2", "Both files have been read."));
1538
- sb.processEvent(chatEndEvent("run-2", {
1539
- input_tokens: 180,
1540
- output_tokens: 12,
1541
- }));
1542
-
1543
- const status = sb.currentStatus;
1544
-
1545
- expect(status.messages).toHaveLength(2);
1546
-
1547
- const toolMsg = status.messages[0];
1548
- expect(toolMsg.toolCalls).toHaveLength(2);
1549
-
1550
- const tcA = toolMsg.toolCalls.find(t => t.id === "tool-a")!;
1551
- const tcB = toolMsg.toolCalls.find(t => t.id === "tool-b")!;
1552
-
1553
- expect(tcA.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
1554
- expect(tcA.result).toBe("content of a.ts");
1555
- expect(tcB.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
1556
- expect(tcB.result).toBe("content of b.ts");
1557
-
1558
- // Both completed independently
1559
- expect(tcA.completedAt).toBeTruthy();
1560
- expect(tcB.completedAt).toBeTruthy();
1561
- });
1562
- });
1563
-
1564
- describe("HITL approval gate — tool requiring approval", () => {
1565
- it("transitions phase to WAITING_FOR_APPROVAL with approval fields", () => {
1566
- const sb = makeBuilder();
1567
-
1568
- sb.setApprovalProvider({
1569
- policies: new Map([
1570
- ["github/create_pull_request", {
1571
- toolName: "create_pull_request",
1572
- mcpServerSlug: "github",
1573
- requiresApproval: true,
1574
- approvalMessage: "Create PR '{{args.title}}' in {{args.repo}}?",
1575
- source: "classifier_default",
1576
- }],
1577
- ]),
1578
- toolServerMap: new Map([["create_pull_request", "github"]]),
1579
- globalBypass: false,
1580
- });
1581
-
1582
- // Model streams text then calls the approval-gated tool
1583
- sb.processEvent(chatStreamEvent("run-1", "I'll create a pull request."));
1584
- sb.processEvent(chatEndEvent("run-1", {
1585
- input_tokens: 90,
1586
- output_tokens: 12,
1587
- }));
1588
-
1589
- sb.processEvent(toolStartEvent("tool-pr-1", "create_pull_request", {
1590
- title: "Fix login bug",
1591
- repo: "stigmer/stigmer",
1592
- }));
1593
-
1594
- const status = sb.currentStatus;
1595
-
1596
- // Phase transitioned to waiting
1597
- expect(status.phase).toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
1598
-
1599
- const tc = status.messages[0].toolCalls[0];
1600
- expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
1601
- expect(tc.requiresApproval).toBe(true);
1602
- expect(tc.approvalMessage).toBe(
1603
- "Create PR 'Fix login bug' in stigmer/stigmer?",
1604
- );
1605
- expect(tc.mcpServerSlug).toBe("github");
1606
- expect(tc.approvalRequestedAt).toBeTruthy();
1607
- expect(tc.argsPreview).toBeTruthy();
1608
-
1609
- // Args preview has sensitive fields redacted
1610
- const preview = JSON.parse(tc.argsPreview);
1611
- expect(preview.title).toBe("Fix login bug");
1612
- expect(preview.repo).toBe("stigmer/stigmer");
1613
- });
1614
- });
1615
-
1616
- describe("usage accumulation — 3-turn with cache tokens", () => {
1617
- it("accumulates input, output, cache read/write across 3 turns", () => {
1618
- const sb = makeBuilder();
1619
-
1620
- // Turn 1: initial prompt
1621
- sb.processEvent(chatStreamEvent("run-1", "First."));
1622
- sb.processEvent(chatEndEvent("run-1", {
1623
- input_tokens: 100,
1624
- output_tokens: 5,
1625
- cache_read_input_tokens: 0,
1626
- cache_creation_input_tokens: 80,
1627
- }));
1628
-
1629
- // Turn 2: tool call turn
1630
- sb.processEvent(chatStreamEvent("run-2", "Reading."));
1631
- sb.processEvent(chatEndEvent("run-2", {
1632
- input_tokens: 200,
1633
- output_tokens: 8,
1634
- cache_read_input_tokens: 80,
1635
- cache_creation_input_tokens: 0,
1636
- }));
1637
-
1638
- // Turn 3: final response
1639
- sb.processEvent(chatStreamEvent("run-3", "Done."));
1640
- sb.processEvent(chatEndEvent("run-3", {
1641
- input_tokens: 350,
1642
- output_tokens: 30,
1643
- cache_read_input_tokens: 80,
1644
- cache_creation_input_tokens: 20,
1645
- }));
1646
-
1647
- const usage = sb.currentStatus.streamingUsage!;
1648
-
1649
- expect(usage.inputTokens).toBe(650n);
1650
- expect(usage.outputTokens).toBe(43n);
1651
- expect(usage.cacheReadTokens).toBe(160n);
1652
- expect(usage.cacheWriteTokens).toBe(100n);
1653
- expect(usage.totalTokens).toBe(953n);
1654
- expect(usage.turnCount).toBe(3);
1655
- expect(usage.observedAt).toBeTruthy();
1656
- });
1657
- });
1658
-
1659
- describe("namespace isolation — parent + subagent", () => {
1660
- it("creates separate message trees by namespace", () => {
1661
- const sb = makeBuilder();
1662
-
1663
- const parentNs = "";
1664
- const subagentNs = "tools:delegate-123|agent_node:researcher";
1665
-
1666
- // Parent agent starts
1667
- sb.processEvent({
1668
- event: "on_chat_model_stream",
1669
- name: "ChatAnthropic",
1670
- run_id: "parent-run-1",
1671
- data: { chunk: { content: "I'll delegate this research task." } },
1672
- metadata: {},
1673
- });
1674
- sb.processEvent({
1675
- event: "on_chat_model_end",
1676
- name: "ChatAnthropic",
1677
- run_id: "parent-run-1",
1678
- data: {
1679
- output: { content: "done" },
1680
- usage_metadata: { input_tokens: 100, output_tokens: 15 },
1681
- },
1682
- });
1683
-
1684
- // Subagent thinking
1685
- sb.processEvent({
1686
- event: "on_chat_model_stream",
1687
- name: "ChatAnthropic",
1688
- run_id: "sub-run-1",
1689
- data: { chunk: { content: [{ type: "thinking", thinking: "Researching the topic..." }] } },
1690
- metadata: { langgraph_checkpoint_ns: subagentNs },
1691
- });
1692
-
1693
- // Subagent text response
1694
- sb.processEvent({
1695
- event: "on_chat_model_stream",
1696
- name: "ChatAnthropic",
1697
- run_id: "sub-run-1",
1698
- data: { chunk: { content: [{ type: "text", text: "I found the following results." }] } },
1699
- metadata: { langgraph_checkpoint_ns: subagentNs },
1700
- });
1701
- sb.processEvent({
1702
- event: "on_chat_model_end",
1703
- name: "ChatAnthropic",
1704
- run_id: "sub-run-1",
1705
- data: {
1706
- output: { content: "done" },
1707
- usage_metadata: { input_tokens: 150, output_tokens: 25 },
1708
- },
1709
- metadata: { langgraph_checkpoint_ns: subagentNs },
1710
- });
1711
-
1712
- // Subagent tool call
1713
- sb.processEvent({
1714
- event: "on_tool_start",
1715
- name: "web_search",
1716
- run_id: "sub-tool-1",
1717
- data: { input: { query: "LangGraph v3 streaming" } },
1718
- metadata: { langgraph_checkpoint_ns: subagentNs },
1719
- });
1720
- sb.processEvent({
1721
- event: "on_tool_end",
1722
- name: "web_search",
1723
- run_id: "sub-tool-1",
1724
- data: { output: "3 results found" },
1725
- metadata: { langgraph_checkpoint_ns: subagentNs },
1726
- });
1727
-
1728
- // Parent resumes after delegation
1729
- sb.processEvent({
1730
- event: "on_chat_model_stream",
1731
- name: "ChatAnthropic",
1732
- run_id: "parent-run-2",
1733
- data: { chunk: { content: "The research is complete." } },
1734
- metadata: {},
1735
- });
1736
- sb.processEvent({
1737
- event: "on_chat_model_end",
1738
- name: "ChatAnthropic",
1739
- run_id: "parent-run-2",
1740
- data: {
1741
- output: { content: "done" },
1742
- usage_metadata: { input_tokens: 400, output_tokens: 10 },
1743
- },
1744
- });
1745
-
1746
- const status = sb.currentStatus;
1747
-
1748
- // 5 messages: parent-text, sub-thinking, sub-text, parent-text-2
1749
- // (sub-tool attaches to sub-text message)
1750
- expect(status.messages).toHaveLength(4);
1751
-
1752
- // Parent turn 1
1753
- expect(status.messages[0].type).toBe(MessageType.MESSAGE_AI);
1754
- expect(status.messages[0].content).toBe("I'll delegate this research task.");
1755
- expect(status.messages[0].toolCalls).toHaveLength(0);
1756
-
1757
- // Subagent thinking
1758
- expect(status.messages[1].type).toBe(MessageType.MESSAGE_THINKING);
1759
- expect(status.messages[1].content).toBe("Researching the topic...");
1760
-
1761
- // Subagent text + tool call
1762
- expect(status.messages[2].type).toBe(MessageType.MESSAGE_AI);
1763
- expect(status.messages[2].content).toBe("I found the following results.");
1764
- expect(status.messages[2].toolCalls).toHaveLength(1);
1765
- expect(status.messages[2].toolCalls[0].name).toBe("web_search");
1766
- expect(status.messages[2].toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
1767
- expect(status.messages[2].toolCalls[0].result).toBe("3 results found");
1768
-
1769
- // Parent turn 2
1770
- expect(status.messages[3].type).toBe(MessageType.MESSAGE_AI);
1771
- expect(status.messages[3].content).toBe("The research is complete.");
1772
-
1773
- // Usage accumulated across ALL namespaces
1774
- const usage = status.streamingUsage!;
1775
- expect(usage.inputTokens).toBe(650n);
1776
- expect(usage.outputTokens).toBe(50n);
1777
- expect(usage.turnCount).toBe(3);
1778
- });
1779
- });
1780
- });
1781
-
1782
- // Durable-checkpoint resume on the legacy v2 path. The activity seeds the
1783
- // builder from the persisted transcript (see seedStatusFromExecution in
1784
- // index.ts); the resumed stream re-emits the gated tool's lifecycle with a
1785
- // FRESH LangGraph run_id that does not match the seeded tool_call_id. The
1786
- // builder must reconcile by name so the existing call is resolved in place,
1787
- // preserving history with no duplicate (the documented v2 fallback; v3, the
1788
- // default, matches by the exact tool_call_id).
1789
- describe("resume reconciliation (durable checkpoint)", () => {
1790
- const GATED_ID = "toolu_seeded_01";
1791
-
1792
- function seededBuilder(): StatusBuilder {
1793
- const status = create(AgentExecutionStatusSchema, {
1794
- phase: ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
1795
- messages: [
1796
- create(AgentMessageSchema, {
1797
- type: MessageType.MESSAGE_AI,
1798
- content: "I'll call the gated tool.",
1799
- toolCalls: [
1800
- create(ToolCallSchema, {
1801
- id: GATED_ID,
1802
- name: "dangerous_tool",
1803
- status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
1804
- requiresApproval: true,
1805
- approvalAction: ApprovalAction.APPROVE,
1806
- mcpServerSlug: "my-server",
1807
- }),
1808
- ],
1809
- }),
1810
- ],
1811
- });
1812
- const sb = new StatusBuilder("exec-resume", status);
1813
- sb.setApprovalProvider({
1814
- policies: new Map([
1815
- ["my-server/dangerous_tool", {
1816
- toolName: "dangerous_tool",
1817
- mcpServerSlug: "my-server",
1818
- requiresApproval: true,
1819
- approvalMessage: "Execute dangerous_tool",
1820
- source: "classifier_default",
1821
- }],
1822
- ]),
1823
- toolServerMap: new Map([["dangerous_tool", "my-server"]]),
1824
- globalBypass: false,
1825
- });
1826
- return sb;
1827
- }
1828
-
1829
- it("resolves the seeded gated tool call in place — no duplicate, history kept", () => {
1830
- const sb = seededBuilder();
1831
-
1832
- // Resumed lifecycle: fresh run_id, no tool_call_id available in v2.
1833
- sb.processEvent(toolStartEvent("run-fresh-99", "dangerous_tool", { target: "prod" }));
1834
- sb.processEvent(toolEndEvent("run-fresh-99", "tool executed ok"));
1835
-
1836
- // Following assistant turn appends after the seeded message.
1837
- sb.processEvent(chatStreamEvent("llm-after", "All done."));
1838
- sb.processEvent(chatEndEvent("llm-after"));
1839
-
1840
- const status = sb.currentStatus;
1841
-
1842
- const gated = status.messages
1843
- .flatMap(m => m.toolCalls)
1844
- .filter(tc => tc.id === GATED_ID);
1845
- expect(gated).toHaveLength(1);
1846
- expect(gated[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
1847
- expect(gated[0].result).toContain("tool executed ok");
1848
-
1849
- const allText = status.messages.map(m => m.content).join("\n");
1850
- expect(allText).toContain("I'll call the gated tool.");
1851
- expect(allText).toContain("All done.");
1852
-
1853
- // No re-gate: the resumed tool must not flip the run back to waiting.
1854
- expect(status.phase).not.toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
1855
- });
1856
- });
1857
-
1858
- // ── Todo extraction (write_todos → status.todos) ─────────────────
1859
- //
1860
- // v2 parity with V3StatusBuilder: a completed write_todos projects into
1861
- // status.todos through the same shared mapper; a failed one does not.
1862
-
1863
- describe("todo extraction", () => {
1864
- it("projects a completed write_todos into status.todos", () => {
1865
- const sb = makeBuilder();
1866
- sb.processEvent(
1867
- toolStartEvent("run-1", "write_todos", {
1868
- todos: [
1869
- { content: "Step one", status: "in_progress" },
1870
- { content: "Step two", status: "pending" },
1871
- ],
1872
- }),
1873
- );
1874
- sb.processEvent(toolEndEvent("run-1", "Todos updated"));
1875
-
1876
- const todos = sb.currentStatus.todos;
1877
- expect(Object.keys(todos)).toEqual(["todo-0", "todo-1"]);
1878
- expect(todos["todo-0"].content).toBe("Step one");
1879
- expect(todos["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
1880
- expect(todos["todo-1"].status).toBe(TodoStatus.TODO_PENDING);
1881
- });
1882
-
1883
- it("keeps the write_todos ToolCall in messages stamped ToolKind.TODO", () => {
1884
- const sb = makeBuilder();
1885
- sb.processEvent(
1886
- toolStartEvent("run-1", "write_todos", {
1887
- todos: [{ content: "A", status: "pending" }],
1888
- }),
1889
- );
1890
- sb.processEvent(toolEndEvent("run-1", "ok"));
1891
-
1892
- const tc = sb.currentStatus.messages
1893
- .flatMap((m) => m.toolCalls)
1894
- .find((t) => t.name === "write_todos");
1895
- expect(tc).toBeDefined();
1896
- expect(tc!.toolKind).toBe(ToolKind.TODO);
1897
- });
1898
-
1899
- it("does not project on a failed write_todos (Command never ran)", () => {
1900
- const sb = makeBuilder();
1901
- sb.processEvent(
1902
- toolStartEvent("run-1", "write_todos", {
1903
- todos: [{ content: "A", status: "pending" }],
1904
- }),
1905
- );
1906
- sb.processEvent(toolEndEvent("run-1", { error: "boom" }));
1907
- expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
1908
- });
1909
-
1910
- it("full-replaces the map on a subsequent write_todos", () => {
1911
- const sb = makeBuilder();
1912
- sb.processEvent(
1913
- toolStartEvent("run-1", "write_todos", {
1914
- todos: [
1915
- { content: "one", status: "completed" },
1916
- { content: "two", status: "in_progress" },
1917
- ],
1918
- }),
1919
- );
1920
- sb.processEvent(toolEndEvent("run-1", "ok"));
1921
- sb.processEvent(
1922
- toolStartEvent("run-2", "write_todos", {
1923
- todos: [{ content: "two", status: "completed" }],
1924
- }),
1925
- );
1926
- sb.processEvent(toolEndEvent("run-2", "ok"));
1927
-
1928
- const todos = sb.currentStatus.todos;
1929
- expect(Object.keys(todos)).toEqual(["todo-0"]);
1930
- expect(todos["todo-0"].status).toBe(TodoStatus.TODO_COMPLETED);
1931
- });
1932
- });
1933
- });