@stigmer/runner 3.14.1 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/execution-record-fixture.d.ts +47 -0
  3. package/dist/__test-utils__/execution-record-fixture.js +68 -0
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -0
  5. package/dist/__test-utils__/harness-contract/types.d.ts +166 -0
  6. package/dist/__test-utils__/harness-contract/types.js +49 -0
  7. package/dist/__test-utils__/harness-contract/types.js.map +1 -0
  8. package/dist/__test-utils__/hermetic-activity.d.ts +28 -5
  9. package/dist/__test-utils__/hermetic-activity.js +23 -7
  10. package/dist/__test-utils__/hermetic-activity.js.map +1 -1
  11. package/dist/__test-utils__/mock-workspace.d.ts +15 -0
  12. package/dist/__test-utils__/mock-workspace.js +21 -0
  13. package/dist/__test-utils__/mock-workspace.js.map +1 -0
  14. package/dist/__test-utils__/model-registry-fixture.d.ts +56 -0
  15. package/dist/__test-utils__/model-registry-fixture.js +66 -0
  16. package/dist/__test-utils__/model-registry-fixture.js.map +1 -0
  17. package/dist/__test-utils__/turn-input-fixture.d.ts +60 -0
  18. package/dist/__test-utils__/turn-input-fixture.js +118 -0
  19. package/dist/__test-utils__/turn-input-fixture.js.map +1 -0
  20. package/dist/activities/execute-cursor/__test-utils__/contract-subject.d.ts +107 -0
  21. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +344 -0
  22. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -0
  23. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +20 -0
  24. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +58 -0
  25. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  26. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +2 -32
  27. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -1
  28. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +38 -62
  29. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +47 -121
  30. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  31. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +17 -1
  32. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +27 -4
  33. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -1
  34. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +52 -11
  35. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +66 -7
  36. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -1
  37. package/dist/activities/execute-cursor/adapter.d.ts +55 -0
  38. package/dist/activities/execute-cursor/adapter.js +119 -0
  39. package/dist/activities/execute-cursor/adapter.js.map +1 -0
  40. package/dist/activities/execute-cursor/approval-state.d.ts +2 -2
  41. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  42. package/dist/activities/execute-cursor/capture-flow.d.ts +15 -30
  43. package/dist/activities/execute-cursor/capture-flow.js +17 -26
  44. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  45. package/dist/activities/execute-cursor/cursor-capabilities.d.ts +23 -0
  46. package/dist/activities/execute-cursor/cursor-capabilities.js +33 -0
  47. package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -0
  48. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +2 -2
  49. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -1
  50. package/dist/activities/execute-cursor/fetch-interceptor.d.ts +15 -25
  51. package/dist/activities/execute-cursor/fetch-interceptor.js +39 -33
  52. package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
  53. package/dist/activities/execute-cursor/http2-interceptor.d.ts +21 -17
  54. package/dist/activities/execute-cursor/http2-interceptor.js +38 -31
  55. package/dist/activities/execute-cursor/http2-interceptor.js.map +1 -1
  56. package/dist/activities/execute-cursor/message-translator.d.ts +28 -19
  57. package/dist/activities/execute-cursor/message-translator.js +76 -27
  58. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  59. package/dist/activities/execute-cursor/prompt-builder.d.ts +180 -11
  60. package/dist/activities/execute-cursor/prompt-builder.js +171 -0
  61. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  62. package/dist/activities/execute-cursor/rejection-capture.d.ts +3 -6
  63. package/dist/activities/execute-cursor/rejection-capture.js +5 -7
  64. package/dist/activities/execute-cursor/rejection-capture.js.map +1 -1
  65. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -4
  66. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  67. package/dist/activities/execute-cursor/turn-boundary.d.ts +1 -1
  68. package/dist/activities/execute-cursor/turn-settle.d.ts +49 -0
  69. package/dist/activities/execute-cursor/turn-settle.js +507 -0
  70. package/dist/activities/execute-cursor/turn-settle.js.map +1 -0
  71. package/dist/activities/execute-cursor/turn-setup.d.ts +173 -0
  72. package/dist/activities/execute-cursor/turn-setup.js +496 -0
  73. package/dist/activities/execute-cursor/turn-setup.js.map +1 -0
  74. package/dist/activities/execute-cursor/turn-stream.d.ts +41 -71
  75. package/dist/activities/execute-cursor/turn-stream.js +70 -135
  76. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  77. package/dist/activities/execute-cursor/turn.d.ts +43 -0
  78. package/dist/activities/execute-cursor/turn.js +222 -0
  79. package/dist/activities/execute-cursor/turn.js.map +1 -0
  80. package/dist/activities/execute-cursor/usage-pricing.d.ts +38 -0
  81. package/dist/activities/execute-cursor/usage-pricing.js +50 -0
  82. package/dist/activities/execute-cursor/usage-pricing.js.map +1 -0
  83. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +1 -1
  84. package/dist/activities/execute-deep-agent/approval-file-change.js +1 -1
  85. package/dist/activities/execute-deep-agent/environment.js +1 -1
  86. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  87. package/dist/activities/execute-deep-agent/setup.js +3 -2
  88. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  89. package/dist/activities/hydrate-workflow-execution.js +1 -1
  90. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  91. package/dist/config.d.ts +15 -0
  92. package/dist/config.js.map +1 -1
  93. package/dist/harness/capabilities.d.ts +16 -0
  94. package/dist/harness/capabilities.js +1 -0
  95. package/dist/harness/capabilities.js.map +1 -1
  96. package/dist/harness/persist-chokepoint.d.ts +74 -0
  97. package/dist/harness/persist-chokepoint.js +88 -0
  98. package/dist/harness/persist-chokepoint.js.map +1 -0
  99. package/dist/harness/registry.d.ts +44 -5
  100. package/dist/harness/registry.js +61 -5
  101. package/dist/harness/registry.js.map +1 -1
  102. package/dist/harness/run-turn.d.ts +53 -0
  103. package/dist/harness/run-turn.js +528 -0
  104. package/dist/harness/run-turn.js.map +1 -0
  105. package/dist/harness/stop-controller.d.ts +50 -0
  106. package/dist/harness/stop-controller.js +62 -0
  107. package/dist/harness/stop-controller.js.map +1 -0
  108. package/dist/harness/terminal-table.d.ts +141 -0
  109. package/dist/harness/terminal-table.js +239 -0
  110. package/dist/harness/terminal-table.js.map +1 -0
  111. package/dist/harness/turn-context.d.ts +523 -0
  112. package/dist/harness/turn-context.js +739 -0
  113. package/dist/harness/turn-context.js.map +1 -0
  114. package/dist/harness/types.d.ts +232 -52
  115. package/dist/harness/types.js +17 -8
  116. package/dist/harness/types.js.map +1 -1
  117. package/dist/harness/usage-accumulator.d.ts +68 -0
  118. package/dist/harness/usage-accumulator.js +101 -0
  119. package/dist/harness/usage-accumulator.js.map +1 -0
  120. package/dist/harness-adapters.d.ts +27 -0
  121. package/dist/harness-adapters.js +28 -0
  122. package/dist/harness-adapters.js.map +1 -0
  123. package/dist/main.js.map +1 -1
  124. package/dist/runner-manager.js +64 -48
  125. package/dist/runner-manager.js.map +1 -1
  126. package/dist/runner.d.ts +5 -4
  127. package/dist/runner.js +65 -64
  128. package/dist/runner.js.map +1 -1
  129. package/dist/shared/approval-policy.d.ts +1 -1
  130. package/dist/shared/approval-policy.js.map +1 -1
  131. package/dist/{activities/execute-cursor → shared}/attachment-resolver.d.ts +2 -2
  132. package/dist/{activities/execute-cursor → shared}/attachment-resolver.js +5 -5
  133. package/dist/shared/attachment-resolver.js.map +1 -0
  134. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.d.ts +7 -5
  135. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.js +3 -63
  136. package/dist/shared/blueprint-resolver.js.map +1 -0
  137. package/dist/shared/channel-attachment.d.ts +1 -1
  138. package/dist/shared/channel-attachment.js.map +1 -1
  139. package/dist/{activities/execute-cursor → shared}/cost-guard.d.ts +5 -1
  140. package/dist/{activities/execute-cursor → shared}/cost-guard.js +5 -1
  141. package/dist/shared/cost-guard.js.map +1 -0
  142. package/dist/{activities/execute-cursor → shared}/env-resolver.d.ts +1 -1
  143. package/dist/shared/env-resolver.js.map +1 -0
  144. package/dist/{activities/execute-cursor → shared}/exact-apply.d.ts +1 -1
  145. package/dist/{activities/execute-cursor → shared}/exact-apply.js +6 -6
  146. package/dist/shared/exact-apply.js.map +1 -0
  147. package/dist/shared/execution-context.d.ts +35 -0
  148. package/dist/shared/execution-context.js +38 -0
  149. package/dist/shared/execution-context.js.map +1 -0
  150. package/dist/{activities/execute-cursor → shared}/extract-structured-output.d.ts +5 -2
  151. package/dist/{activities/execute-cursor → shared}/extract-structured-output.js +9 -6
  152. package/dist/shared/extract-structured-output.js.map +1 -0
  153. package/dist/shared/filereview/capture.d.ts +1 -1
  154. package/dist/shared/filereview/capture.js +1 -1
  155. package/dist/shared/heartbeat.d.ts +13 -11
  156. package/dist/shared/heartbeat.js +14 -29
  157. package/dist/shared/heartbeat.js.map +1 -1
  158. package/dist/{activities/execute-cursor → shared}/persist-decision.d.ts +5 -2
  159. package/dist/{activities/execute-cursor → shared}/persist-decision.js +4 -1
  160. package/dist/shared/persist-decision.js.map +1 -0
  161. package/dist/{activities/execute-cursor → shared}/skill-resolver.d.ts +16 -3
  162. package/dist/{activities/execute-cursor → shared}/skill-resolver.js +12 -4
  163. package/dist/shared/skill-resolver.js.map +1 -0
  164. package/dist/shared/subagent-rows.d.ts +21 -0
  165. package/dist/shared/subagent-rows.js +34 -0
  166. package/dist/shared/subagent-rows.js.map +1 -0
  167. package/dist/shared/worker-shutdown.d.ts +43 -16
  168. package/dist/shared/worker-shutdown.js +28 -13
  169. package/dist/shared/worker-shutdown.js.map +1 -1
  170. package/dist/shared/workspace/platform-dir.d.ts +1 -1
  171. package/dist/shared/workspace/platform-dir.js +1 -1
  172. package/dist/shared/workspace/session-provision.d.ts +54 -0
  173. package/dist/{activities/execute-cursor/workspace-provision.js → shared/workspace/session-provision.js} +15 -12
  174. package/dist/shared/workspace/session-provision.js.map +1 -0
  175. package/package.json +4 -4
  176. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +110 -14
  177. package/src/__test-utils__/execution-record-fixture.ts +104 -0
  178. package/src/__test-utils__/harness-boot-order-child.ts +58 -0
  179. package/src/__test-utils__/harness-contract/contract.ts +128 -83
  180. package/src/__test-utils__/harness-contract/recording-sink.ts +15 -4
  181. package/src/__test-utils__/harness-contract/runtime-contract.ts +666 -0
  182. package/src/__test-utils__/harness-contract/scripted-adapter.ts +92 -32
  183. package/src/__test-utils__/harness-contract/types.ts +85 -9
  184. package/src/__test-utils__/hermetic-activity.ts +33 -8
  185. package/src/__test-utils__/model-registry-fixture.ts +72 -0
  186. package/src/__test-utils__/module-specifiers.ts +82 -0
  187. package/src/__test-utils__/turn-input-fixture.ts +155 -0
  188. package/src/__tests__/harness-boot-order.test.ts +77 -0
  189. package/src/__tests__/harness-contract.test.ts +7 -5
  190. package/src/__tests__/runner-manager.test.ts +18 -0
  191. package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +102 -0
  192. package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +396 -0
  193. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +66 -0
  194. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +2 -43
  195. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +65 -145
  196. package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +30 -4
  197. package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +96 -8
  198. package/src/activities/execute-cursor/__tests__/adapter-is-temporal-free.test.ts +45 -0
  199. package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +90 -0
  200. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -8
  201. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +28 -1
  202. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +1 -1
  203. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +64 -6
  204. package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +1 -1
  205. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +1 -1
  206. package/src/activities/execute-cursor/__tests__/hermetic/goldens/cost-cap.status.json +53 -0
  207. package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +1 -6
  208. package/src/activities/execute-cursor/__tests__/hermetic/goldens/platform-stop.status.json +56 -0
  209. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-transport-timeout.status.json +48 -0
  210. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
  211. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-cancelled.status.json +36 -0
  212. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-error-non-retryable.status.json +37 -0
  213. package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +38 -0
  214. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +59 -0
  215. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +79 -0
  216. package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +2 -7
  217. package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +170 -0
  218. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +22 -18
  219. package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +1 -1
  220. package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +1 -1
  221. package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +286 -0
  222. package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +332 -0
  223. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +193 -0
  224. package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +1 -1
  225. package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +185 -0
  226. package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +151 -0
  227. package/src/activities/execute-cursor/__tests__/http2-interceptor.test.ts +65 -35
  228. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +0 -40
  229. package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +171 -0
  230. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +118 -243
  231. package/src/activities/execute-cursor/__tests__/usage-pricing.test.ts +84 -0
  232. package/src/activities/execute-cursor/adapter.ts +138 -0
  233. package/src/activities/execute-cursor/approval-state.ts +2 -2
  234. package/src/activities/execute-cursor/capture-flow.ts +18 -41
  235. package/src/activities/execute-cursor/cursor-capabilities.ts +35 -0
  236. package/src/activities/execute-cursor/cursor-mcp-config.ts +2 -2
  237. package/src/activities/execute-cursor/fetch-interceptor.ts +48 -45
  238. package/src/activities/execute-cursor/http2-interceptor.ts +47 -34
  239. package/src/activities/execute-cursor/message-translator.ts +90 -45
  240. package/src/activities/execute-cursor/prompt-builder.ts +327 -12
  241. package/src/activities/execute-cursor/rejection-capture.ts +5 -12
  242. package/src/activities/execute-cursor/session-lifecycle.ts +1 -5
  243. package/src/activities/execute-cursor/turn-boundary.ts +1 -1
  244. package/src/activities/execute-cursor/turn-settle.ts +601 -0
  245. package/src/activities/execute-cursor/turn-setup.ts +645 -0
  246. package/src/activities/execute-cursor/turn-stream.ts +84 -210
  247. package/src/activities/execute-cursor/turn.ts +240 -0
  248. package/src/activities/execute-cursor/usage-pricing.ts +67 -0
  249. package/src/activities/execute-deep-agent/approval-file-change.ts +1 -1
  250. package/src/activities/execute-deep-agent/environment.ts +1 -1
  251. package/src/activities/execute-deep-agent/setup.ts +3 -2
  252. package/src/activities/hydrate-workflow-execution.ts +1 -1
  253. package/src/config.ts +13 -0
  254. package/src/harness/__tests__/import-direction.test.ts +168 -0
  255. package/src/harness/__tests__/persist-chokepoint.test.ts +146 -0
  256. package/src/harness/__tests__/registry.test.ts +1 -0
  257. package/src/harness/__tests__/run-turn.test.ts +165 -0
  258. package/src/harness/__tests__/stop-controller.test.ts +49 -0
  259. package/src/harness/__tests__/turn-context.test.ts +178 -0
  260. package/src/harness/__tests__/usage-accumulator.test.ts +83 -0
  261. package/src/harness/capabilities.ts +17 -0
  262. package/src/harness/persist-chokepoint.ts +112 -0
  263. package/src/harness/registry.ts +79 -5
  264. package/src/harness/run-turn.ts +618 -0
  265. package/src/harness/stop-controller.ts +83 -0
  266. package/src/harness/terminal-table.ts +276 -0
  267. package/src/harness/turn-context.ts +1028 -0
  268. package/src/harness/types.ts +248 -53
  269. package/src/harness/usage-accumulator.ts +122 -0
  270. package/src/harness-adapters.ts +30 -0
  271. package/src/main.ts +1 -1
  272. package/src/runner-manager.ts +68 -61
  273. package/src/runner.ts +67 -78
  274. package/src/shared/__tests__/attachment-naming.test.ts +1 -1
  275. package/src/{activities/execute-cursor → shared}/__tests__/attachment-resolver.test.ts +6 -4
  276. package/src/{activities/execute-cursor → shared}/__tests__/cost-guard.test.ts +2 -1
  277. package/src/{activities/execute-cursor → shared}/__tests__/exact-apply.test.ts +3 -3
  278. package/src/{activities/execute-cursor → shared}/__tests__/extract-structured-output.test.ts +8 -8
  279. package/src/{activities/execute-cursor → shared}/__tests__/persist-decision.test.ts +4 -2
  280. package/src/{activities/execute-cursor → shared}/__tests__/skill-resolver.test.ts +1 -1
  281. package/src/shared/__tests__/subagent-rows.test.ts +34 -0
  282. package/src/shared/__tests__/worker-shutdown.test.ts +44 -39
  283. package/src/shared/approval-policy.ts +1 -1
  284. package/src/{activities/execute-cursor → shared}/attachment-resolver.ts +6 -6
  285. package/src/{activities/execute-cursor → shared}/blueprint-resolver.ts +7 -78
  286. package/src/shared/channel-attachment.ts +1 -1
  287. package/src/{activities/execute-cursor → shared}/cost-guard.ts +5 -1
  288. package/src/{activities/execute-cursor → shared}/env-resolver.ts +1 -1
  289. package/src/{activities/execute-cursor → shared}/exact-apply.ts +7 -7
  290. package/src/shared/execution-context.ts +45 -0
  291. package/src/{activities/execute-cursor → shared}/extract-structured-output.ts +10 -7
  292. package/src/shared/filereview/capture.ts +1 -1
  293. package/src/shared/heartbeat.ts +13 -33
  294. package/src/{activities/execute-cursor → shared}/persist-decision.ts +5 -2
  295. package/src/{activities/execute-cursor → shared}/skill-resolver.ts +20 -6
  296. package/src/shared/subagent-rows.ts +39 -0
  297. package/src/shared/worker-shutdown.ts +55 -25
  298. package/src/{activities/execute-cursor/__tests__/workspace-provision.test.ts → shared/workspace/__tests__/session-provision.test.ts} +25 -28
  299. package/src/shared/workspace/platform-dir.ts +1 -1
  300. package/src/{activities/execute-cursor/workspace-provision.ts → shared/workspace/session-provision.ts} +29 -16
  301. package/dist/activities/execute-cursor/attachment-resolver.js.map +0 -1
  302. package/dist/activities/execute-cursor/blueprint-resolver.js.map +0 -1
  303. package/dist/activities/execute-cursor/cost-guard.js.map +0 -1
  304. package/dist/activities/execute-cursor/env-resolver.js.map +0 -1
  305. package/dist/activities/execute-cursor/exact-apply.js.map +0 -1
  306. package/dist/activities/execute-cursor/extract-structured-output.js.map +0 -1
  307. package/dist/activities/execute-cursor/index.d.ts +0 -209
  308. package/dist/activities/execute-cursor/index.js +0 -2345
  309. package/dist/activities/execute-cursor/index.js.map +0 -1
  310. package/dist/activities/execute-cursor/persist-decision.js.map +0 -1
  311. package/dist/activities/execute-cursor/skill-resolver.js.map +0 -1
  312. package/dist/activities/execute-cursor/usage-accumulator.d.ts +0 -89
  313. package/dist/activities/execute-cursor/usage-accumulator.js +0 -121
  314. package/dist/activities/execute-cursor/usage-accumulator.js.map +0 -1
  315. package/dist/activities/execute-cursor/workspace-provision.d.ts +0 -42
  316. package/dist/activities/execute-cursor/workspace-provision.js.map +0 -1
  317. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +0 -164
  318. package/src/activities/execute-cursor/index.ts +0 -2876
  319. package/src/activities/execute-cursor/usage-accumulator.ts +0 -169
  320. /package/dist/{activities/execute-cursor → shared}/env-resolver.js +0 -0
@@ -2,24 +2,26 @@
2
2
  * Unit tests for the Cursor harness's shared stream seam (turn-stream.ts).
3
3
  *
4
4
  * These lock the behaviors the old bare retry loops used to DROP — live persist,
5
- * DD-32/DD-33 mid-run progress, sub-agent tracking, the first-denial early stop,
6
- * and correct pause/platform-stop mapping so that unifying the primary turn and
7
- * both recovery retries onto this one loop cannot silently regress any of them.
5
+ * DD-32/DD-33 mid-run progress, sub-agent tracking, the first-denial early stop
6
+ * and the loop's side of the adapter contract: progress reported per event
7
+ * and per delta, usage reported priced, the persist awaited, and the runtime's
8
+ * stop signal honoured at every event boundary and inside a blocked pull by
9
+ * cancelling the SDK run. Since S2 M3 the loop no longer decides WHY it
10
+ * stopped (stall, cost cap, pause, platform stop are the runtime's evidence;
11
+ * `harness/__tests__/run-turn.test.ts` proves that table); it reports
12
+ * `completed`, `first-denial` or `interrupted`.
8
13
  *
9
14
  * The loop is driven with a structural `mockRun` (an async-iterable `.stream()`)
10
- * and injected `heartbeat`/`isCancelled`, so it runs without the live Cursor SDK
11
- * or Temporal — mirroring the deep-agent `streaming.test.ts` pattern.
15
+ * and the kit's `RecordingTurnSink`, so it runs without the live Cursor SDK or
16
+ * Temporal — mirroring the deep-agent `streaming.test.ts` pattern.
12
17
  */
13
18
 
14
19
  import { describe, it, expect, vi, afterEach } from "vitest";
15
20
  import { mkdtemp, writeFile, rm } from "node:fs/promises";
16
21
  import { tmpdir } from "node:os";
17
22
  import { join } from "node:path";
18
- import { create } from "@bufbuild/protobuf";
19
- import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
20
- import { ExecutionControlSignal } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
21
- import { CancelledFailure } from "@temporalio/activity";
22
23
  import type { SDKMessage } from "@cursor/sdk";
24
+ import { RecordingTurnSink } from "../../../__test-utils__/harness-contract/recording-sink.js";
23
25
  import {
24
26
  consumeCursorTurnStream,
25
27
  makeCursorTurnOnDelta,
@@ -75,45 +77,32 @@ function stubEnricher() {
75
77
  };
76
78
  }
77
79
 
78
- function stubUsageAccumulator() {
79
- return {
80
- addTurn: vi.fn(),
81
- // hasTurns:false keeps the loop from building a StreamingUsageSummary (which
82
- // would need a real snapshot); usage plumbing is covered by usage-accumulator's
83
- // own tests.
84
- hasTurns: false,
85
- snapshot: vi.fn(() => ({ inputTokens: 0n, outputTokens: 0n })),
86
- };
80
+ /** Prices nothing: hands the counts back as a delta with a stated cost, so the sink sees a priced delta. */
81
+ function stubUsagePricer() {
82
+ return { price: vi.fn((usage: Record<string, number>) => ({ ...usage, estimatedCostUsd: 0.01, model: "m" })) };
87
83
  }
88
84
 
89
85
  interface BuiltDeps {
90
86
  deps: CursorTurnStreamDeps;
91
87
  state: TurnStreamState;
92
- persist: ReturnType<typeof vi.fn>;
88
+ sink: RecordingTurnSink;
93
89
  accumulator: ReturnType<typeof stubAccumulator>;
94
- status: ReturnType<typeof create<typeof AgentExecutionStatusSchema>>;
95
90
  }
96
91
 
97
92
  function buildDeps(overrides: Partial<CursorTurnStreamDeps> = {}): BuiltDeps {
98
93
  const state = overrides.state ?? newTurnStreamState();
99
- const status = overrides.status ?? create(AgentExecutionStatusSchema, {});
100
- const accumulator = (overrides.accumulator as unknown as ReturnType<typeof stubAccumulator>) ??
101
- stubAccumulator();
102
- const persist =
103
- (overrides.persist as ReturnType<typeof vi.fn>) ??
104
- vi.fn(async () => ExecutionControlSignal.UNSPECIFIED);
94
+ const sink = (overrides.sink as RecordingTurnSink | undefined) ?? new RecordingTurnSink();
95
+ const accumulator = (overrides.accumulator as unknown as ReturnType<typeof stubAccumulator>) ?? stubAccumulator();
105
96
 
106
97
  const deps = {
107
98
  // TurnOnDeltaDeps
108
- usageAccumulator: stubUsageAccumulator(),
99
+ sink,
100
+ usagePricer: stubUsagePricer(),
109
101
  deltaEnricher: stubEnricher(),
110
- heartbeat: vi.fn(),
111
102
  promptEstimatedTokens: 100,
112
103
  executionId: "exec-test",
113
104
  state,
114
- maxCostUsd: 0,
115
105
  // CursorTurnStreamDeps
116
- status,
117
106
  accumulator,
118
107
  todoTracker: { processEvent: vi.fn(), markPersisted: vi.fn(), isDirty: false },
119
108
  eventRecorder: undefined,
@@ -124,43 +113,39 @@ function buildDeps(overrides: Partial<CursorTurnStreamDeps> = {}): BuiltDeps {
124
113
  progressState: { lastAtMs: 0 },
125
114
  changeSetId: "exec-test:0",
126
115
  hitlDir: undefined,
127
- stallTimeoutMs: 120_000,
128
- persist,
129
- isCancelled: () => false,
130
116
  ...overrides,
131
117
  } as unknown as CursorTurnStreamDeps;
132
118
 
133
- return { deps, state, persist, accumulator, status };
119
+ return { deps, state, sink, accumulator };
134
120
  }
135
121
 
136
122
  describe("consumeCursorTurnStream", () => {
137
- afterEach(() => {
138
- vi.useRealTimers();
139
- });
123
+ it("processes every event, persists live through the sink, and returns 'completed' on a natural end", async () => {
124
+ const { deps, state, sink, accumulator } = buildDeps();
140
125
 
141
- it("processes every event, persists live, and returns 'completed' on a natural end", async () => {
142
- const { deps, state, persist, accumulator } = buildDeps();
143
-
144
- const reason = await consumeCursorTurnStream(
145
- mockRun([ev({ type: "assistant" }), ev({ type: "assistant" })]),
146
- deps,
147
- );
126
+ const reason = await consumeCursorTurnStream(mockRun([ev({ type: "assistant" }), ev({ type: "assistant" })]), deps);
148
127
 
149
128
  expect(reason).toBe("completed");
150
129
  expect(accumulator.processEvent).toHaveBeenCalledTimes(2);
151
130
  // The #1 retry gap: the bare loop never persisted mid-stream. This proves the
152
- // shared loop persists live.
153
- expect(persist).toHaveBeenCalled();
131
+ // shared loop persists live — through the runtime's chokepoint.
132
+ expect(sink.persistRequests).toBeGreaterThan(0);
154
133
  expect(state.eventCount).toBe(2);
155
134
  });
156
135
 
136
+ it("reports progress per event, naming the tool of a tool_call event (the stall copy's `last tool`)", async () => {
137
+ const { deps, sink } = buildDeps();
138
+
139
+ await consumeCursorTurnStream(mockRun([ev({ type: "assistant" }), ev({ type: "tool_call", name: "shell" })]), deps);
140
+
141
+ const marks = sink.events.filter((e) => e.kind === "activity");
142
+ expect(marks.map((m) => (m.kind === "activity" ? m.detail : undefined))).toEqual([undefined, "shell"]);
143
+ });
144
+
157
145
  it("tracks a sub-agent delegation (the 'task' tool call) — dropped by the old bare retry loop", async () => {
158
146
  const { deps, accumulator } = buildDeps();
159
147
 
160
- await consumeCursorTurnStream(
161
- mockRun([ev({ type: "tool_call", name: "task" })]),
162
- deps,
163
- );
148
+ await consumeCursorTurnStream(mockRun([ev({ type: "tool_call", name: "task" })]), deps);
164
149
 
165
150
  expect(accumulator.trackSubAgentExecution).toHaveBeenCalledTimes(1);
166
151
  });
@@ -168,56 +153,27 @@ describe("consumeCursorTurnStream", () => {
168
153
  it("attaches DD-32/DD-33 mid-run progress when a substrate is present", async () => {
169
154
  const capture = vi.fn(async () => ({ delta: { entries: [] }, changed: true }));
170
155
  const progressSubstrate = { capture } as unknown as ProgressSubstrate;
171
- const { deps, status } = buildDeps({ progressSubstrate });
156
+ const { deps, sink } = buildDeps({ progressSubstrate });
172
157
 
173
158
  await consumeCursorTurnStream(mockRun([ev({ type: "assistant" })]), deps);
174
159
 
175
160
  expect(capture).toHaveBeenCalled();
176
161
  // changed:true → the transient snapshot is attached to status.
177
- expect(status.fileChangeProgress).toBeDefined();
162
+ expect(sink.status.fileChangeProgress).toBeDefined();
178
163
  });
179
164
 
180
165
  it("does not touch progress when no substrate is configured", async () => {
181
- const { deps, status } = buildDeps({ progressSubstrate: undefined });
166
+ const { deps, sink } = buildDeps({ progressSubstrate: undefined });
182
167
 
183
168
  await consumeCursorTurnStream(mockRun([ev({ type: "assistant" })]), deps);
184
169
 
185
- expect(status.fileChangeProgress).toBeUndefined();
186
- });
187
-
188
- it("returns 'paused' and sets pauseDetected when the activity is cancelled", async () => {
189
- const { deps, state, accumulator } = buildDeps({ isCancelled: () => true });
190
-
191
- const reason = await consumeCursorTurnStream(mockRun([ev({ type: "assistant" })]), deps);
192
-
193
- expect(reason).toBe("paused");
194
- expect(state.pauseDetected).toBe(true);
195
- // Cancellation is checked before the event is processed.
196
- expect(accumulator.processEvent).not.toHaveBeenCalled();
197
- });
198
-
199
- it("returns 'platform-stop' when a persist reports the STOP control signal", async () => {
200
- const persist = vi.fn(async () => ExecutionControlSignal.STOP);
201
- const { deps, state } = buildDeps({ persist });
202
-
203
- const reason = await consumeCursorTurnStream(
204
- mockRun([ev({ type: "assistant" }), ev({ type: "assistant" })]),
205
- deps,
206
- );
207
-
208
- expect(reason).toBe("platform-stop");
209
- expect(state.platformStopSignaled).toBe(true);
210
- // Broke after the first persist — the second event never persisted.
211
- expect(persist).toHaveBeenCalledTimes(1);
170
+ expect(sink.status.fileChangeProgress).toBeUndefined();
212
171
  });
213
172
 
214
173
  it("captures a stream ERROR status message onto state.streamErrorMessage", async () => {
215
174
  const { deps, state } = buildDeps();
216
175
 
217
- await consumeCursorTurnStream(
218
- mockRun([ev({ type: "status", status: "ERROR", message: "boom" })]),
219
- deps,
220
- );
176
+ await consumeCursorTurnStream(mockRun([ev({ type: "status", status: "ERROR", message: "boom" })]), deps);
221
177
 
222
178
  expect(state.streamErrorMessage).toBe("boom");
223
179
  });
@@ -227,10 +183,7 @@ describe("consumeCursorTurnStream", () => {
227
183
  // crash classifyText downstream (.toLowerCase() on a non-string).
228
184
  const { deps, state } = buildDeps();
229
185
 
230
- await consumeCursorTurnStream(
231
- mockRun([ev({ type: "status", status: "ERROR", message: { code: 14 } })]),
232
- deps,
233
- );
186
+ await consumeCursorTurnStream(mockRun([ev({ type: "status", status: "ERROR", message: { code: 14 } })]), deps);
234
187
 
235
188
  expect(state.streamErrorMessage).toBeUndefined();
236
189
  });
@@ -244,11 +197,7 @@ describe("consumeCursorTurnStream", () => {
244
197
 
245
198
  it("stops the turn, cancels the run, and arms denialCancelSettled on a ledger entry", async () => {
246
199
  hitlDir = await mkdtemp(join(tmpdir(), "turn-stream-denial-"));
247
- await writeFile(
248
- join(hitlDir, "denials.jsonl"),
249
- JSON.stringify({ toolName: "shell", token: "tok-1" }) + "\n",
250
- "utf-8",
251
- );
200
+ await writeFile(join(hitlDir, "denials.jsonl"), JSON.stringify({ toolName: "shell", token: "tok-1" }) + "\n", "utf-8");
252
201
  const { deps, state } = buildDeps({ hitlDir });
253
202
  const run = mockRun([ev({ type: "tool_call", name: "shell" })]);
254
203
 
@@ -265,72 +214,92 @@ describe("consumeCursorTurnStream", () => {
265
214
  await writeFile(join(hitlDir, "denials.jsonl"), "", "utf-8");
266
215
  const { deps, state } = buildDeps({ hitlDir });
267
216
 
268
- const reason = await consumeCursorTurnStream(
269
- mockRun([ev({ type: "tool_call", name: "shell" })]),
270
- deps,
271
- );
217
+ const reason = await consumeCursorTurnStream(mockRun([ev({ type: "tool_call", name: "shell" })]), deps);
272
218
 
273
219
  expect(reason).toBe("completed");
274
220
  expect(state.firstDenialDetected).toBe(false);
275
221
  });
276
222
  });
277
223
 
278
- describe("cost-cap stop", () => {
279
- it("cancels the run, stops the stream, and returns 'cost-cap' when onDelta flagged the overrun", async () => {
280
- const state = newTurnStreamState();
281
- state.costCapExceeded = true; // onDelta's write, simulated
282
- const { deps, accumulator } = buildDeps({ state });
283
- const run = mockRun([ev({ type: "assistant" }), ev({ type: "assistant" })]);
224
+ describe("the runtime's stop signal", () => {
225
+ it("a signal aborted before the stream starts: the run is cancelled and nothing is processed", async () => {
226
+ const sink = new RecordingTurnSink();
227
+ sink.abort("cost-cap");
228
+ const { deps, accumulator } = buildDeps({ sink });
229
+ const run = mockRun([ev({ type: "assistant" })]);
284
230
 
285
231
  const reason = await consumeCursorTurnStream(run, deps);
286
232
 
287
- expect(reason).toBe("cost-cap");
288
- expect(run.cancel).toHaveBeenCalled();
289
- // The break happens before the event is processed — no post-cap content.
233
+ expect(reason).toBe("interrupted");
234
+ expect(run.cancel).toHaveBeenCalledTimes(1);
290
235
  expect(accumulator.processEvent).not.toHaveBeenCalled();
291
236
  });
292
237
 
293
- it("detects the flag mid-stream when onDelta sets it between events", async () => {
294
- const state = newTurnStreamState();
295
- const { deps, accumulator } = buildDeps({ state });
296
- // Simulate onDelta's write landing between event 1 and event 2 the
297
- // realistic shape: a turn-ended usage delta crosses the cap while the
298
- // loop is between stream events.
238
+ it("an abort between events: the run is cancelled at once and the next event is never processed", async () => {
239
+ const sink = new RecordingTurnSink();
240
+ const { deps, accumulator } = buildDeps({ sink });
241
+ // The realistic shape: the runtime aborts (a turn-ended delta crossed the
242
+ // cap, a STOP came back from a persist) while the loop is between events.
299
243
  async function* gen(): AsyncIterable<SDKMessage> {
300
244
  yield ev({ type: "assistant" });
301
- state.costCapExceeded = true;
245
+ sink.abort("platform-stop");
302
246
  yield ev({ type: "assistant" });
303
247
  }
248
+ const run: MockRun = { stream: () => gen(), supports: () => true, cancel: vi.fn(async () => {}) };
249
+
250
+ const reason = await consumeCursorTurnStream(run, deps);
251
+
252
+ expect(reason).toBe("interrupted");
253
+ expect(run.cancel).toHaveBeenCalledTimes(1);
254
+ // Event 1 was processed; event 2 hit the boundary check.
255
+ expect(accumulator.processEvent).toHaveBeenCalledTimes(1);
256
+ });
257
+
258
+ it("an abort while the pull is blocked (a wedged stream): the cancel is what unblocks it", async () => {
259
+ const sink = new RecordingTurnSink();
260
+ const { deps } = buildDeps({ sink });
261
+ // The generator yields one event, then awaits a promise resolved only by
262
+ // run.cancel() — the loop is suspended inside the pull when the runtime's
263
+ // watchdog aborts.
264
+ let unblock: (() => void) | undefined;
265
+ async function* gen(): AsyncIterable<SDKMessage> {
266
+ yield ev({ type: "assistant" });
267
+ await new Promise<void>((resolve) => {
268
+ unblock = resolve;
269
+ });
270
+ }
304
271
  const run: MockRun = {
305
272
  stream: () => gen(),
306
273
  supports: () => true,
307
- cancel: vi.fn(async () => {}),
274
+ cancel: vi.fn(async () => {
275
+ unblock?.();
276
+ }),
308
277
  };
309
278
 
310
- const reason = await consumeCursorTurnStream(run, deps);
279
+ const pending = consumeCursorTurnStream(run, deps);
280
+ await vi.waitFor(() => expect(unblock).toBeDefined());
281
+ sink.abort("stall");
282
+ const reason = await pending;
311
283
 
312
- expect(reason).toBe("cost-cap");
313
- expect(run.cancel).toHaveBeenCalled();
314
- // Event 1 was processed; event 2 hit the break before processing.
315
- expect(accumulator.processEvent).toHaveBeenCalledTimes(1);
284
+ expect(reason).toBe("interrupted");
285
+ expect(run.cancel).toHaveBeenCalledTimes(1);
316
286
  });
317
287
 
318
288
  it("swallows the cancel-induced teardown rejection (expected, not a failure)", async () => {
319
- const state = newTurnStreamState();
320
- state.costCapExceeded = true;
321
- const { deps } = buildDeps({ state });
289
+ const sink = new RecordingTurnSink();
290
+ const { deps } = buildDeps({ sink });
322
291
  // The break exits the for-await, which invokes the iterator's return();
323
- // a cancelled SDK run can reject there. The catch must exempt cost-cap
324
- // teardown exactly like stall/first-denial teardown.
292
+ // a cancelled SDK run can reject there. The catch must exempt a stopped
293
+ // turn's teardown exactly like the first-denial teardown.
325
294
  const events = [ev({ type: "assistant" })];
326
295
  let i = 0;
327
296
  const run: MockRun = {
328
297
  stream: () => ({
329
298
  [Symbol.asyncIterator]: () => ({
330
- next: async () =>
331
- i < events.length
332
- ? { value: events[i++], done: false as const }
333
- : { value: undefined, done: true as const },
299
+ next: async () => {
300
+ if (i === 1) sink.abort("cost-cap");
301
+ return i < events.length ? { value: events[i++], done: false as const } : { value: undefined, done: true as const };
302
+ },
334
303
  return: async () => {
335
304
  throw new Error("run cancelled");
336
305
  },
@@ -342,139 +311,45 @@ describe("consumeCursorTurnStream", () => {
342
311
 
343
312
  const reason = await consumeCursorTurnStream(run, deps);
344
313
 
345
- expect(reason).toBe("cost-cap");
314
+ expect(reason).toBe("interrupted");
346
315
  expect(run.cancel).toHaveBeenCalled();
347
316
  });
348
- });
349
-
350
- it("returns 'stalled' and cancels the run when the stall watchdog fires", async () => {
351
- vi.useFakeTimers();
352
- // The generator yields one event, then awaits a promise resolved only by
353
- // run.cancel() — so the loop is suspended when the watchdog fires.
354
- let unblock: (() => void) | undefined;
355
- async function* gen(): AsyncIterable<SDKMessage> {
356
- yield ev({ type: "assistant" });
357
- await new Promise<void>((resolve) => {
358
- unblock = resolve;
359
- });
360
- }
361
- const run: MockRun = {
362
- stream: () => gen(),
363
- supports: () => true,
364
- cancel: vi.fn(async () => {
365
- unblock?.();
366
- }),
367
- };
368
- const { deps, state } = buildDeps({ stallTimeoutMs: 40 });
369
-
370
- const pending = consumeCursorTurnStream(run, deps);
371
- // Advance past the stall window (tick = stallMs/4 = 10ms), flushing the
372
- // microtasks between ticks so the loop reaches its await first.
373
- await vi.advanceTimersByTimeAsync(80);
374
- const reason = await pending;
375
-
376
- expect(reason).toBe("stalled");
377
- expect(state.stallDetected).toBe(true);
378
- expect(run.cancel).toHaveBeenCalled();
379
- });
380
- });
381
-
382
- describe("makeCursorTurnOnDelta", () => {
383
- it("flags a pause (not a throw) when the heartbeat reports CancelledFailure", () => {
384
- const state = newTurnStreamState();
385
- const onDelta = makeCursorTurnOnDelta({
386
- usageAccumulator: stubUsageAccumulator() as never,
387
- deltaEnricher: stubEnricher() as never,
388
- heartbeat: () => {
389
- throw new CancelledFailure("paused");
390
- },
391
- promptEstimatedTokens: 10,
392
- executionId: "e",
393
- state,
394
- maxCostUsd: 0,
395
- });
396
317
 
397
- expect(() => onDelta({ update: { type: "text" } as never })).not.toThrow();
398
- expect(state.pauseDetected).toBe(true);
399
- });
318
+ it("rethrows a genuine stream failure when nothing stopped the turn", async () => {
319
+ const { deps } = buildDeps();
320
+ async function* gen(): AsyncIterable<SDKMessage> {
321
+ yield ev({ type: "assistant" });
322
+ throw new Error("transport reset");
323
+ }
324
+ const run: MockRun = { stream: () => gen(), supports: () => true, cancel: vi.fn(async () => {}) };
400
325
 
401
- it("rethrows a non-cancellation heartbeat error", () => {
402
- const state = newTurnStreamState();
403
- const onDelta = makeCursorTurnOnDelta({
404
- usageAccumulator: stubUsageAccumulator() as never,
405
- deltaEnricher: stubEnricher() as never,
406
- heartbeat: () => {
407
- throw new Error("boom");
408
- },
409
- promptEstimatedTokens: 10,
410
- executionId: "e",
411
- state,
412
- maxCostUsd: 0,
326
+ await expect(consumeCursorTurnStream(run, deps)).rejects.toThrow("transport reset");
413
327
  });
414
-
415
- expect(() => onDelta({ update: { type: "text" } as never })).toThrow("boom");
416
- expect(state.pauseDetected).toBe(false);
417
328
  });
329
+ });
418
330
 
419
- it("accumulates turn usage and logs first-turn attribution exactly once", () => {
331
+ describe("makeCursorTurnOnDelta", () => {
332
+ it("reports every delta as progress and every turn-ended usage priced, logging first-turn attribution exactly once", () => {
420
333
  const state = newTurnStreamState();
421
- const usageAccumulator = stubUsageAccumulator();
334
+ const sink = new RecordingTurnSink();
335
+ const usagePricer = stubUsagePricer();
422
336
  const onDelta = makeCursorTurnOnDelta({
423
- usageAccumulator: usageAccumulator as never,
337
+ sink,
338
+ usagePricer: usagePricer as never,
424
339
  deltaEnricher: stubEnricher() as never,
425
- heartbeat: vi.fn(),
426
340
  promptEstimatedTokens: 10,
427
341
  executionId: "e",
428
342
  state,
429
- maxCostUsd: 0,
430
343
  });
431
344
 
345
+ onDelta({ update: { type: "text" } as never });
432
346
  onDelta({ update: { type: "turn-ended", usage: { inputTokens: 100 } } as never });
433
347
  onDelta({ update: { type: "turn-ended", usage: { inputTokens: 50 } } as never });
434
348
 
435
- expect(usageAccumulator.addTurn).toHaveBeenCalledTimes(2);
349
+ expect(sink.activityMarks).toBe(3);
350
+ expect(usagePricer.price).toHaveBeenCalledTimes(2);
351
+ expect(sink.usageDeltas.map((d) => d.inputTokens)).toEqual([100, 50]);
352
+ expect(sink.usageDeltas.every((d) => d.estimatedCostUsd === 0.01), "the delta reaches the sink priced").toBe(true);
436
353
  expect(state.firstTurnAttributionLogged).toBe(true);
437
354
  });
438
-
439
- it("flags costCapExceeded when a turn-ended usage delta pushes the estimate past the cap", () => {
440
- const state = newTurnStreamState();
441
- const usageAccumulator = {
442
- ...stubUsageAccumulator(),
443
- snapshot: vi.fn(() => ({ estimatedCostUsd: 0.51 })),
444
- };
445
- const onDelta = makeCursorTurnOnDelta({
446
- usageAccumulator: usageAccumulator as never,
447
- deltaEnricher: stubEnricher() as never,
448
- heartbeat: vi.fn(),
449
- promptEstimatedTokens: 10,
450
- executionId: "e",
451
- state,
452
- maxCostUsd: 0.5,
453
- });
454
-
455
- onDelta({ update: { type: "turn-ended", usage: { inputTokens: 100 } } as never });
456
-
457
- expect(state.costCapExceeded).toBe(true);
458
- });
459
-
460
- it("never flags costCapExceeded when no cap is configured", () => {
461
- const state = newTurnStreamState();
462
- const usageAccumulator = {
463
- ...stubUsageAccumulator(),
464
- snapshot: vi.fn(() => ({ estimatedCostUsd: 999 })),
465
- };
466
- const onDelta = makeCursorTurnOnDelta({
467
- usageAccumulator: usageAccumulator as never,
468
- deltaEnricher: stubEnricher() as never,
469
- heartbeat: vi.fn(),
470
- promptEstimatedTokens: 10,
471
- executionId: "e",
472
- state,
473
- maxCostUsd: 0,
474
- });
475
-
476
- onDelta({ update: { type: "turn-ended", usage: { inputTokens: 100 } } as never });
477
-
478
- expect(state.costCapExceeded).toBe(false);
479
- });
480
355
  });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The Cursor pricer: one `turn-ended` delta priced at the requested
3
+ * variant's rates and attributed with the basis the runtime records. The
4
+ * accounting half is `harness/__tests__/usage-accumulator.test.ts`.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeAll } from "vitest";
8
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
9
+
10
+ import { CursorUsagePricer } from "../usage-pricing.js";
11
+
12
+ // The pricer reads the worker's pricing table (model-pricing.ts); load it
13
+ // from a stubbed registry so the fast-rate assertions run against known
14
+ // prices (the model-pricing.test.ts pattern).
15
+ beforeAll(async () => {
16
+ const registry = {
17
+ models: [
18
+ {
19
+ id: "composer-2.5",
20
+ displayName: "Composer 2.5",
21
+ provider: "cursor",
22
+ harness: "cursor",
23
+ costTier: "economy",
24
+ pricing: {
25
+ inputPricePerMillion: 0.5,
26
+ outputPricePerMillion: 2.5,
27
+ cacheWritePricePerMillion: 0,
28
+ cacheReadPricePerMillion: 0.2,
29
+ },
30
+ pricingVariants: {
31
+ fast: {
32
+ inputPricePerMillion: 3.0,
33
+ outputPricePerMillion: 15.0,
34
+ cacheWritePricePerMillion: 0,
35
+ cacheReadPricePerMillion: 0.2,
36
+ },
37
+ },
38
+ },
39
+ ],
40
+ };
41
+ vi.stubGlobal(
42
+ "fetch",
43
+ vi.fn(async () => ({ ok: true, status: 200, json: async () => registry })),
44
+ );
45
+ process.env.STIGMER_TOKEN = "test-token";
46
+ await (await import("../model-pricing.js")).ensureLoaded();
47
+ });
48
+
49
+ const MILLION_EACH = { inputTokens: 1_000_000, outputTokens: 1_000_000 };
50
+
51
+ describe("CursorUsagePricer", () => {
52
+ it("prices FAST runs at fast-variant rates, not base rates (#357)", () => {
53
+ // Revert guard for the tier→pricing wiring: a FAST run priced at base
54
+ // rates would understate the display estimate ~6x relative to the
55
+ // authoritative bill. Rates from the stubbed registry: base $0.5/$2.5
56
+ // per M, fast $3/$15 per M.
57
+ const standard = new CursorUsagePricer("composer-2.5", ServiceTier.STANDARD).price(MILLION_EACH);
58
+ const fast = new CursorUsagePricer("composer-2.5", ServiceTier.FAST).price(MILLION_EACH);
59
+ expect(standard.estimatedCostUsd).toBeCloseTo(3.0, 6);
60
+ expect(fast.estimatedCostUsd).toBeCloseTo(18.0, 6);
61
+ });
62
+
63
+ it("prices UNSPECIFIED at base rates (resolves to standard)", () => {
64
+ const unspecified = new CursorUsagePricer("composer-2.5", ServiceTier.UNSPECIFIED).price(MILLION_EACH);
65
+ const standard = new CursorUsagePricer("composer-2.5", ServiceTier.STANDARD).price(MILLION_EACH);
66
+ expect(unspecified.estimatedCostUsd).toBe(standard.estimatedCostUsd);
67
+ });
68
+
69
+ it("names the basis on every priced delta: the model id and the JSON of the params sent", () => {
70
+ const delta = new CursorUsagePricer("composer-2.5", ServiceTier.FAST, [{ id: "fast", value: "true" }]).price({
71
+ inputTokens: 10,
72
+ outputTokens: 5,
73
+ });
74
+ expect(delta.model).toBe("composer-2.5");
75
+ expect(delta.requestedModelParams).toBe('[{"id":"fast","value":"true"}]');
76
+ expect(delta).toMatchObject({ inputTokens: 10, outputTokens: 5, cacheReadTokens: 0, cacheWriteTokens: 0 });
77
+ });
78
+
79
+ it("names an empty params string when the runner sent none, and treats missing counts as zero", () => {
80
+ const delta = new CursorUsagePricer("composer-2.5", ServiceTier.STANDARD, []).price({ inputTokens: 1 });
81
+ expect(delta.requestedModelParams).toBe("");
82
+ expect(delta.outputTokens).toBe(0);
83
+ });
84
+ });