@stigmer/runner 3.14.0 → 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 (331) 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 +268 -0
  9. package/dist/__test-utils__/hermetic-activity.js +385 -0
  10. package/dist/__test-utils__/hermetic-activity.js.map +1 -0
  11. package/dist/__test-utils__/mock-client.d.ts +13 -0
  12. package/dist/__test-utils__/mock-client.js +45 -0
  13. package/dist/__test-utils__/mock-client.js.map +1 -0
  14. package/dist/__test-utils__/mock-workspace.d.ts +15 -0
  15. package/dist/__test-utils__/mock-workspace.js +21 -0
  16. package/dist/__test-utils__/mock-workspace.js.map +1 -0
  17. package/dist/__test-utils__/model-registry-fixture.d.ts +56 -0
  18. package/dist/__test-utils__/model-registry-fixture.js +66 -0
  19. package/dist/__test-utils__/model-registry-fixture.js.map +1 -0
  20. package/dist/__test-utils__/turn-input-fixture.d.ts +60 -0
  21. package/dist/__test-utils__/turn-input-fixture.js +118 -0
  22. package/dist/__test-utils__/turn-input-fixture.js.map +1 -0
  23. package/dist/activities/execute-cursor/__test-utils__/contract-subject.d.ts +107 -0
  24. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +344 -0
  25. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -0
  26. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +20 -0
  27. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +58 -0
  28. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  29. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +2 -32
  30. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -1
  31. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +148 -0
  32. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +257 -0
  33. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -0
  34. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +183 -0
  35. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +262 -0
  36. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -0
  37. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +138 -0
  38. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +191 -0
  39. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -0
  40. package/dist/activities/execute-cursor/adapter.d.ts +55 -0
  41. package/dist/activities/execute-cursor/adapter.js +119 -0
  42. package/dist/activities/execute-cursor/adapter.js.map +1 -0
  43. package/dist/activities/execute-cursor/approval-state.d.ts +2 -2
  44. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  45. package/dist/activities/execute-cursor/capture-flow.d.ts +15 -30
  46. package/dist/activities/execute-cursor/capture-flow.js +17 -26
  47. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  48. package/dist/activities/execute-cursor/cursor-capabilities.d.ts +23 -0
  49. package/dist/activities/execute-cursor/cursor-capabilities.js +33 -0
  50. package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -0
  51. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +2 -2
  52. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -1
  53. package/dist/activities/execute-cursor/fetch-interceptor.d.ts +15 -25
  54. package/dist/activities/execute-cursor/fetch-interceptor.js +39 -33
  55. package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
  56. package/dist/activities/execute-cursor/http2-interceptor.d.ts +21 -17
  57. package/dist/activities/execute-cursor/http2-interceptor.js +38 -31
  58. package/dist/activities/execute-cursor/http2-interceptor.js.map +1 -1
  59. package/dist/activities/execute-cursor/message-translator.d.ts +28 -19
  60. package/dist/activities/execute-cursor/message-translator.js +76 -27
  61. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  62. package/dist/activities/execute-cursor/prompt-builder.d.ts +180 -11
  63. package/dist/activities/execute-cursor/prompt-builder.js +171 -0
  64. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  65. package/dist/activities/execute-cursor/rejection-capture.d.ts +3 -6
  66. package/dist/activities/execute-cursor/rejection-capture.js +5 -7
  67. package/dist/activities/execute-cursor/rejection-capture.js.map +1 -1
  68. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -4
  69. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  70. package/dist/activities/execute-cursor/turn-boundary.d.ts +1 -1
  71. package/dist/activities/execute-cursor/turn-settle.d.ts +49 -0
  72. package/dist/activities/execute-cursor/turn-settle.js +507 -0
  73. package/dist/activities/execute-cursor/turn-settle.js.map +1 -0
  74. package/dist/activities/execute-cursor/turn-setup.d.ts +173 -0
  75. package/dist/activities/execute-cursor/turn-setup.js +496 -0
  76. package/dist/activities/execute-cursor/turn-setup.js.map +1 -0
  77. package/dist/activities/execute-cursor/turn-stream.d.ts +41 -71
  78. package/dist/activities/execute-cursor/turn-stream.js +70 -135
  79. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  80. package/dist/activities/execute-cursor/turn.d.ts +43 -0
  81. package/dist/activities/execute-cursor/turn.js +222 -0
  82. package/dist/activities/execute-cursor/turn.js.map +1 -0
  83. package/dist/activities/execute-cursor/usage-pricing.d.ts +38 -0
  84. package/dist/activities/execute-cursor/usage-pricing.js +50 -0
  85. package/dist/activities/execute-cursor/usage-pricing.js.map +1 -0
  86. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +1 -1
  87. package/dist/activities/execute-deep-agent/approval-file-change.js +1 -1
  88. package/dist/activities/execute-deep-agent/environment.js +1 -1
  89. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  90. package/dist/activities/execute-deep-agent/setup.js +3 -2
  91. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  92. package/dist/activities/hydrate-workflow-execution.js +1 -1
  93. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  94. package/dist/config.d.ts +15 -0
  95. package/dist/config.js.map +1 -1
  96. package/dist/harness/capabilities.d.ts +87 -0
  97. package/dist/harness/capabilities.js +37 -0
  98. package/dist/harness/capabilities.js.map +1 -0
  99. package/dist/harness/persist-chokepoint.d.ts +74 -0
  100. package/dist/harness/persist-chokepoint.js +88 -0
  101. package/dist/harness/persist-chokepoint.js.map +1 -0
  102. package/dist/harness/registry.d.ts +106 -0
  103. package/dist/harness/registry.js +168 -0
  104. package/dist/harness/registry.js.map +1 -0
  105. package/dist/harness/run-turn.d.ts +53 -0
  106. package/dist/harness/run-turn.js +528 -0
  107. package/dist/harness/run-turn.js.map +1 -0
  108. package/dist/harness/stop-controller.d.ts +50 -0
  109. package/dist/harness/stop-controller.js +62 -0
  110. package/dist/harness/stop-controller.js.map +1 -0
  111. package/dist/harness/terminal-table.d.ts +141 -0
  112. package/dist/harness/terminal-table.js +239 -0
  113. package/dist/harness/terminal-table.js.map +1 -0
  114. package/dist/harness/turn-context.d.ts +523 -0
  115. package/dist/harness/turn-context.js +739 -0
  116. package/dist/harness/turn-context.js.map +1 -0
  117. package/dist/harness/types.d.ts +448 -0
  118. package/dist/harness/types.js +64 -0
  119. package/dist/harness/types.js.map +1 -0
  120. package/dist/harness/usage-accumulator.d.ts +68 -0
  121. package/dist/harness/usage-accumulator.js +101 -0
  122. package/dist/harness/usage-accumulator.js.map +1 -0
  123. package/dist/harness-adapters.d.ts +27 -0
  124. package/dist/harness-adapters.js +28 -0
  125. package/dist/harness-adapters.js.map +1 -0
  126. package/dist/main.js.map +1 -1
  127. package/dist/runner-manager.js +64 -48
  128. package/dist/runner-manager.js.map +1 -1
  129. package/dist/runner.d.ts +5 -4
  130. package/dist/runner.js +65 -64
  131. package/dist/runner.js.map +1 -1
  132. package/dist/shared/approval-policy.d.ts +1 -1
  133. package/dist/shared/approval-policy.js.map +1 -1
  134. package/dist/{activities/execute-cursor → shared}/attachment-resolver.d.ts +2 -2
  135. package/dist/{activities/execute-cursor → shared}/attachment-resolver.js +5 -5
  136. package/dist/shared/attachment-resolver.js.map +1 -0
  137. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.d.ts +7 -5
  138. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.js +3 -63
  139. package/dist/shared/blueprint-resolver.js.map +1 -0
  140. package/dist/shared/channel-attachment.d.ts +1 -1
  141. package/dist/shared/channel-attachment.js.map +1 -1
  142. package/dist/{activities/execute-cursor → shared}/cost-guard.d.ts +5 -1
  143. package/dist/{activities/execute-cursor → shared}/cost-guard.js +5 -1
  144. package/dist/shared/cost-guard.js.map +1 -0
  145. package/dist/{activities/execute-cursor → shared}/env-resolver.d.ts +1 -1
  146. package/dist/shared/env-resolver.js.map +1 -0
  147. package/dist/{activities/execute-cursor → shared}/exact-apply.d.ts +1 -1
  148. package/dist/{activities/execute-cursor → shared}/exact-apply.js +6 -6
  149. package/dist/shared/exact-apply.js.map +1 -0
  150. package/dist/shared/execution-context.d.ts +35 -0
  151. package/dist/shared/execution-context.js +38 -0
  152. package/dist/shared/execution-context.js.map +1 -0
  153. package/dist/{activities/execute-cursor → shared}/extract-structured-output.d.ts +5 -2
  154. package/dist/{activities/execute-cursor → shared}/extract-structured-output.js +9 -6
  155. package/dist/shared/extract-structured-output.js.map +1 -0
  156. package/dist/shared/filereview/capture.d.ts +1 -1
  157. package/dist/shared/filereview/capture.js +1 -1
  158. package/dist/shared/heartbeat.d.ts +13 -11
  159. package/dist/shared/heartbeat.js +14 -29
  160. package/dist/shared/heartbeat.js.map +1 -1
  161. package/dist/{activities/execute-cursor → shared}/persist-decision.d.ts +5 -2
  162. package/dist/{activities/execute-cursor → shared}/persist-decision.js +4 -1
  163. package/dist/shared/persist-decision.js.map +1 -0
  164. package/dist/{activities/execute-cursor → shared}/skill-resolver.d.ts +16 -3
  165. package/dist/{activities/execute-cursor → shared}/skill-resolver.js +12 -4
  166. package/dist/shared/skill-resolver.js.map +1 -0
  167. package/dist/shared/subagent-rows.d.ts +21 -0
  168. package/dist/shared/subagent-rows.js +34 -0
  169. package/dist/shared/subagent-rows.js.map +1 -0
  170. package/dist/shared/worker-shutdown.d.ts +43 -16
  171. package/dist/shared/worker-shutdown.js +28 -13
  172. package/dist/shared/worker-shutdown.js.map +1 -1
  173. package/dist/shared/workspace/platform-dir.d.ts +1 -1
  174. package/dist/shared/workspace/platform-dir.js +1 -1
  175. package/dist/shared/workspace/session-provision.d.ts +54 -0
  176. package/dist/{activities/execute-cursor/workspace-provision.js → shared/workspace/session-provision.js} +15 -12
  177. package/dist/shared/workspace/session-provision.js.map +1 -0
  178. package/package.json +4 -4
  179. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +325 -0
  180. package/src/__test-utils__/config-fixture.ts +63 -0
  181. package/src/__test-utils__/execution-record-fixture.ts +104 -0
  182. package/src/__test-utils__/harness-boot-order-child.ts +58 -0
  183. package/src/__test-utils__/harness-contract/contract.ts +581 -0
  184. package/src/__test-utils__/harness-contract/recording-sink.ts +107 -0
  185. package/src/__test-utils__/harness-contract/runtime-contract.ts +666 -0
  186. package/src/__test-utils__/harness-contract/scripted-adapter.ts +349 -0
  187. package/src/__test-utils__/harness-contract/types.ts +176 -0
  188. package/src/__test-utils__/hermetic-activity.ts +502 -0
  189. package/src/__test-utils__/model-registry-fixture.ts +72 -0
  190. package/src/__test-utils__/module-specifiers.ts +82 -0
  191. package/src/__test-utils__/proto-helpers.ts +25 -0
  192. package/src/__test-utils__/turn-input-fixture.ts +155 -0
  193. package/src/__tests__/harness-boot-order.test.ts +77 -0
  194. package/src/__tests__/harness-contract.test.ts +27 -0
  195. package/src/__tests__/runner-manager.test.ts +18 -0
  196. package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +102 -0
  197. package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +396 -0
  198. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +66 -0
  199. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +2 -43
  200. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +342 -0
  201. package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +368 -0
  202. package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +254 -0
  203. package/src/activities/execute-cursor/__tests__/adapter-is-temporal-free.test.ts +45 -0
  204. package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +90 -0
  205. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -8
  206. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +28 -1
  207. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +1 -1
  208. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +64 -6
  209. package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +228 -0
  210. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +180 -0
  211. package/src/activities/execute-cursor/__tests__/hermetic/goldens/cost-cap.status.json +53 -0
  212. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn1.status.json +55 -0
  213. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn2.status.json +77 -0
  214. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +126 -0
  215. package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +40 -0
  216. package/src/activities/execute-cursor/__tests__/hermetic/goldens/plain-turn.status.json +48 -0
  217. package/src/activities/execute-cursor/__tests__/hermetic/goldens/platform-stop.status.json +56 -0
  218. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-fresh-agent.status.json +53 -0
  219. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-transport-timeout.status.json +48 -0
  220. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
  221. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-cancelled.status.json +36 -0
  222. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-error-non-retryable.status.json +37 -0
  223. package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +38 -0
  224. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +59 -0
  225. package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-call.status.json +68 -0
  226. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +79 -0
  227. package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +42 -0
  228. package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +170 -0
  229. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +205 -0
  230. package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +171 -0
  231. package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +156 -0
  232. package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +286 -0
  233. package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +332 -0
  234. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +193 -0
  235. package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +137 -0
  236. package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +185 -0
  237. package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +151 -0
  238. package/src/activities/execute-cursor/__tests__/http2-interceptor.test.ts +65 -35
  239. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +0 -40
  240. package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +171 -0
  241. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +118 -243
  242. package/src/activities/execute-cursor/__tests__/usage-pricing.test.ts +84 -0
  243. package/src/activities/execute-cursor/adapter.ts +138 -0
  244. package/src/activities/execute-cursor/approval-state.ts +2 -2
  245. package/src/activities/execute-cursor/capture-flow.ts +18 -41
  246. package/src/activities/execute-cursor/cursor-capabilities.ts +35 -0
  247. package/src/activities/execute-cursor/cursor-mcp-config.ts +2 -2
  248. package/src/activities/execute-cursor/fetch-interceptor.ts +48 -45
  249. package/src/activities/execute-cursor/http2-interceptor.ts +47 -34
  250. package/src/activities/execute-cursor/message-translator.ts +90 -45
  251. package/src/activities/execute-cursor/prompt-builder.ts +327 -12
  252. package/src/activities/execute-cursor/rejection-capture.ts +5 -12
  253. package/src/activities/execute-cursor/session-lifecycle.ts +1 -5
  254. package/src/activities/execute-cursor/turn-boundary.ts +1 -1
  255. package/src/activities/execute-cursor/turn-settle.ts +601 -0
  256. package/src/activities/execute-cursor/turn-setup.ts +645 -0
  257. package/src/activities/execute-cursor/turn-stream.ts +84 -210
  258. package/src/activities/execute-cursor/turn.ts +240 -0
  259. package/src/activities/execute-cursor/usage-pricing.ts +67 -0
  260. package/src/activities/execute-deep-agent/approval-file-change.ts +1 -1
  261. package/src/activities/execute-deep-agent/environment.ts +1 -1
  262. package/src/activities/execute-deep-agent/setup.ts +3 -2
  263. package/src/activities/hydrate-workflow-execution.ts +1 -1
  264. package/src/config.ts +13 -0
  265. package/src/harness/__tests__/import-direction.test.ts +168 -0
  266. package/src/harness/__tests__/persist-chokepoint.test.ts +146 -0
  267. package/src/harness/__tests__/registry.test.ts +168 -0
  268. package/src/harness/__tests__/run-turn.test.ts +165 -0
  269. package/src/harness/__tests__/stop-controller.test.ts +49 -0
  270. package/src/harness/__tests__/turn-context.test.ts +178 -0
  271. package/src/harness/__tests__/usage-accumulator.test.ts +83 -0
  272. package/src/harness/capabilities.ts +92 -0
  273. package/src/harness/persist-chokepoint.ts +112 -0
  274. package/src/harness/registry.ts +197 -0
  275. package/src/harness/run-turn.ts +618 -0
  276. package/src/harness/stop-controller.ts +83 -0
  277. package/src/harness/terminal-table.ts +276 -0
  278. package/src/harness/turn-context.ts +1028 -0
  279. package/src/harness/types.ts +473 -0
  280. package/src/harness/usage-accumulator.ts +122 -0
  281. package/src/harness-adapters.ts +30 -0
  282. package/src/main.ts +1 -1
  283. package/src/runner-manager.ts +68 -61
  284. package/src/runner.ts +67 -78
  285. package/src/shared/__tests__/attachment-naming.test.ts +1 -1
  286. package/src/{activities/execute-cursor → shared}/__tests__/attachment-resolver.test.ts +6 -4
  287. package/src/{activities/execute-cursor → shared}/__tests__/cost-guard.test.ts +2 -1
  288. package/src/{activities/execute-cursor → shared}/__tests__/exact-apply.test.ts +3 -3
  289. package/src/{activities/execute-cursor → shared}/__tests__/extract-structured-output.test.ts +8 -8
  290. package/src/{activities/execute-cursor → shared}/__tests__/persist-decision.test.ts +4 -2
  291. package/src/{activities/execute-cursor → shared}/__tests__/skill-resolver.test.ts +1 -1
  292. package/src/shared/__tests__/subagent-rows.test.ts +34 -0
  293. package/src/shared/__tests__/worker-shutdown.test.ts +44 -39
  294. package/src/shared/approval-policy.ts +1 -1
  295. package/src/{activities/execute-cursor → shared}/attachment-resolver.ts +6 -6
  296. package/src/{activities/execute-cursor → shared}/blueprint-resolver.ts +7 -78
  297. package/src/shared/channel-attachment.ts +1 -1
  298. package/src/{activities/execute-cursor → shared}/cost-guard.ts +5 -1
  299. package/src/{activities/execute-cursor → shared}/env-resolver.ts +1 -1
  300. package/src/{activities/execute-cursor → shared}/exact-apply.ts +7 -7
  301. package/src/shared/execution-context.ts +45 -0
  302. package/src/{activities/execute-cursor → shared}/extract-structured-output.ts +10 -7
  303. package/src/shared/filereview/capture.ts +1 -1
  304. package/src/shared/heartbeat.ts +13 -33
  305. package/src/{activities/execute-cursor → shared}/persist-decision.ts +5 -2
  306. package/src/{activities/execute-cursor → shared}/skill-resolver.ts +20 -6
  307. package/src/shared/subagent-rows.ts +39 -0
  308. package/src/shared/worker-shutdown.ts +55 -25
  309. package/src/{activities/execute-cursor/__tests__/workspace-provision.test.ts → shared/workspace/__tests__/session-provision.test.ts} +25 -28
  310. package/src/shared/workspace/platform-dir.ts +1 -1
  311. package/src/{activities/execute-cursor/workspace-provision.ts → shared/workspace/session-provision.ts} +29 -16
  312. package/dist/activities/execute-cursor/attachment-resolver.js.map +0 -1
  313. package/dist/activities/execute-cursor/blueprint-resolver.js.map +0 -1
  314. package/dist/activities/execute-cursor/cost-guard.js.map +0 -1
  315. package/dist/activities/execute-cursor/env-resolver.js.map +0 -1
  316. package/dist/activities/execute-cursor/exact-apply.js.map +0 -1
  317. package/dist/activities/execute-cursor/extract-structured-output.js.map +0 -1
  318. package/dist/activities/execute-cursor/index.d.ts +0 -209
  319. package/dist/activities/execute-cursor/index.js +0 -2345
  320. package/dist/activities/execute-cursor/index.js.map +0 -1
  321. package/dist/activities/execute-cursor/persist-decision.js.map +0 -1
  322. package/dist/activities/execute-cursor/skill-resolver.js.map +0 -1
  323. package/dist/activities/execute-cursor/usage-accumulator.d.ts +0 -89
  324. package/dist/activities/execute-cursor/usage-accumulator.js +0 -121
  325. package/dist/activities/execute-cursor/usage-accumulator.js.map +0 -1
  326. package/dist/activities/execute-cursor/workspace-provision.d.ts +0 -42
  327. package/dist/activities/execute-cursor/workspace-provision.js.map +0 -1
  328. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +0 -164
  329. package/src/activities/execute-cursor/index.ts +0 -2876
  330. package/src/activities/execute-cursor/usage-accumulator.ts +0 -169
  331. /package/dist/{activities/execute-cursor → shared}/env-resolver.js +0 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The child entry `__tests__/harness-boot-order.test.ts` spawns: one FRESH
3
+ * Node process that does exactly what the composition roots do before
4
+ * bootstrap — import `harness-adapters.js` and `harness/registry.js`, then
5
+ * `bootHarnesses` on a proxy-mode config — and reports how it went.
6
+ *
7
+ * Why a child process: the `node:http2` ESM facade is snapshotted once per
8
+ * process at its first import, so the positive load-order contract ("the
9
+ * patch reached the facade") cannot be observed inside a shared vitest
10
+ * worker that has already imported half the runner. A fresh process is the
11
+ * only honest instrument (`http2-interceptor.test.ts` says as much).
12
+ *
13
+ * Arms (argv[2]):
14
+ * - `boot`: import the two pre-boot modules, boot with a proxy endpoint and
15
+ * a bound token ref. Success means the pre-boot graph was connect-free,
16
+ * both interceptors installed, `assertHttp2ConnectPatched` passed, and the
17
+ * SDK slice loaded from inside `boot`.
18
+ * - `boot-after-connect-node`: the same, after importing the Stigmer client
19
+ * (connect-node) FIRST. Must fail with the facade sentence: proves the
20
+ * guard is live inside `boot`, so a future regression is loud.
21
+ *
22
+ * Output: one line, `harness-boot-order: ok` or `harness-boot-order: <message>`,
23
+ * then exit 0 on success and 1 on a rejection. The proxy endpoint is an inert
24
+ * loopback nothing listens on; boot dials nothing.
25
+ */
26
+
27
+ import { testConfig } from "./config-fixture.js";
28
+
29
+ const arm = process.argv[2];
30
+
31
+ async function main(): Promise<void> {
32
+ if (arm === "boot-after-connect-node") {
33
+ await import("../client/stigmer-client.js");
34
+ } else if (arm !== "boot") {
35
+ throw new Error(`harness-boot-order-child: unknown arm '${String(arm)}' (expected 'boot' or 'boot-after-connect-node')`);
36
+ }
37
+
38
+ const [{ HARNESS_ADAPTERS }, { adaptersOf, bootHarnesses }] = await Promise.all([
39
+ import("../harness-adapters.js"),
40
+ import("../harness/registry.js"),
41
+ ]);
42
+ const config = testConfig({
43
+ proxyEndpoint: "http://127.0.0.1:1",
44
+ proxyTokenRef: { current: "boot-order-test-token" },
45
+ });
46
+ await bootHarnesses(adaptersOf(HARNESS_ADAPTERS), config);
47
+ }
48
+
49
+ main().then(
50
+ () => {
51
+ process.stdout.write("harness-boot-order: ok\n");
52
+ process.exit(0);
53
+ },
54
+ (err: unknown) => {
55
+ process.stdout.write(`harness-boot-order: ${err instanceof Error ? err.message : String(err)}\n`);
56
+ process.exit(1);
57
+ },
58
+ );
@@ -0,0 +1,581 @@
1
+ /**
2
+ * The harness adapter contract kit — the single authoritative statement of
3
+ * "these are THE things every `HarnessAdapter` must do," runnable against any
4
+ * {@link HarnessContractSubject}.
5
+ *
6
+ * Two halves exist in the program's design; this file is the ADAPTER-SIDE
7
+ * half: what an adapter owes the runtime through `runTurn`, `boot`,
8
+ * `shutdown` and `releaseSession`. The runtime-side half (the throw-vs-return
9
+ * table end to end, the whole-activity heartbeat, the single persist
10
+ * chokepoint, the byte-pinned copy) is `runtime-contract.ts`, proven against
11
+ * the real runtime under `MockActivityEnvironment` with the same subjects.
12
+ *
13
+ * The kit IS the runtime stand-in. {@link ExecutionDriver} does what the
14
+ * runtime and the server do around a turn: builds the `TurnInput`, threads
15
+ * the engine's state id (empty on an engine-minted harness's first turn, then
16
+ * the id the adapter bound; one fixed id for a deterministic harness),
17
+ * advances `turnSeq`, records a user's decision the way `SubmitApproval` does
18
+ * (on the persisted row's `approvalAction`, the one copy) and derives the
19
+ * decisions map from those rows on the next invocation with the runtime's
20
+ * own reader, hands each reinvocation a CLONE of the previous status on BOTH
21
+ * the sink and the record (`execution.status` — the runtime seeds the one
22
+ * from the other, and an adapter may read the record's copy for its facts;
23
+ * nothing survives by object identity), and owns the sink. The subject owns
24
+ * the engine, and tells the kit when that engine is parked on a hang, which
25
+ * is the only moment the kit stops a turn from the outside: a real engine
26
+ * reaches its hang after real setup, and a stop delivered earlier lands in
27
+ * the setup and proves nothing. The kit also owns the adapter's lifetime —
28
+ * booted before the first turn, shut down after the last — because the
29
+ * registry does, and a real adapter refuses a turn before boot.
30
+ *
31
+ * ── Invariant catalog ───────────────────────────────────────────────────────
32
+ * 1. Every exit is a `TurnOutcome`. `runTurn` resolves, never rejects, under
33
+ * every scenario kind and under a pre-aborted signal; a `CancelledFailure`
34
+ * never escapes (the runtime, not the adapter, throws it).
35
+ * 2. A proposal is a WAITING_APPROVAL row on `sink.status` when
36
+ * `awaiting_approval` resolves, and never executes in that turn.
37
+ * 3. Reinvoked with APPROVE for that id → executes exactly once and the row
38
+ * is carried to COMPLETED; with REJECT or SKIP → never executes and the
39
+ * turn continues (the row's terminal status is the runtime's to write,
40
+ * not the adapter's — Q-M4-1); reinvoked again after the approval → still
41
+ * once, never re-gated.
42
+ * 4. Aborting `stopSignal` mid-hang settles `runTurn` as `interrupted` within
43
+ * {@link INTERRUPT_SETTLE_BOUND_MS} and nothing after the stop executes; a
44
+ * signal aborted BEFORE `runTurn` yields `interrupted` with nothing done.
45
+ * 5. Usage reaches the sink as non-negative deltas summing to what the engine
46
+ * emitted.
47
+ * 6. Capability and behaviour agree on the state id: an `engine-minted`
48
+ * adapter binds before its first persist and resumes by the bound id, and
49
+ * surfaces a rejected bind as `failed` with nothing done after it; a
50
+ * `deterministic` adapter never binds.
51
+ * 7. Lifetimes: `releaseSession` for a served session and for an unknown one
52
+ * both resolve; `shutdown` after a release resolves; `boot` after
53
+ * `shutdown` resolves (a worker restart re-boots the same module state).
54
+ * 8. One adapter object serves concurrent turns independently: a hanging turn
55
+ * and a completing turn on the same adapter never cross outcomes or
56
+ * execution counts.
57
+ *
58
+ * Every invariant is an exported plain async function first and an `it`
59
+ * block second, so `__tests__/harness-contract-self-check.test.ts` can run
60
+ * each one against a deliberately broken adapter and prove it fires — a kit
61
+ * that cannot fail proves nothing. Every message names the subject. Each
62
+ * function requires a booted adapter; {@link describeHarnessContract} boots.
63
+ *
64
+ * Above the contract line the pause primitives are indistinguishable: both
65
+ * end a turn `awaiting_approval`, both take the decisions on reinvocation.
66
+ * The kit therefore never branches on `capabilities.pausePrimitive`, and the
67
+ * runner test runs the fake under both to prove it.
68
+ */
69
+
70
+ import { afterAll, beforeAll, describe, it, expect } from "vitest";
71
+ import { CancelledFailure } from "@temporalio/activity";
72
+ import { clone } from "@bufbuild/protobuf";
73
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
74
+ import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
75
+ import { ApprovalAction, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
76
+
77
+ import { approvalDecisionsOf } from "../../harness/turn-context.js";
78
+ import type { TurnInput, TurnOutcome, UsageDelta } from "../../harness/types.js";
79
+ import type { ProposedAction } from "../approval-contract/types.js";
80
+ import { emptyStatus, findToolCallRow } from "../proto-helpers.js";
81
+ import { turnInputFixture } from "../turn-input-fixture.js";
82
+ import { RecordingTurnSink } from "./recording-sink.js";
83
+ import { scenario } from "./types.js";
84
+ import type { EngineView, HarnessContractSubject, TurnScenario } from "./types.js";
85
+
86
+ /**
87
+ * How long a turn may take to settle `interrupted` after its signal aborts.
88
+ * Generous against a saturated CI runner (the fake settles in microtasks; a
89
+ * real adapter cancels an SDK run), tight enough that an adapter parked on
90
+ * anything but the signal fails here rather than at Temporal's heartbeat
91
+ * timeout in production. A timeout race, never a sleep.
92
+ */
93
+ export const INTERRUPT_SETTLE_BOUND_MS = 5_000;
94
+
95
+ const OUTCOME_KINDS = ["completed", "cancelled", "awaiting_approval", "failed", "interrupted"] as const satisfies readonly TurnOutcome["kind"][];
96
+
97
+ /** Representative gated action shared by the invariants. */
98
+ const WRITE_ALPHA: ProposedAction = { kind: "write", resource: "/work/alpha.txt" };
99
+
100
+ // ── The runtime (and server) stand-in ───────────────────────────────────────
101
+
102
+ export interface TurnRun {
103
+ readonly input: TurnInput;
104
+ readonly sink: RecordingTurnSink;
105
+ readonly outcome: TurnOutcome;
106
+ }
107
+
108
+ export interface TurnOptions {
109
+ /** A prepared sink (e.g. one whose bind rejects); defaults to a fresh one over the threaded status. */
110
+ readonly sink?: RecordingTurnSink;
111
+ /**
112
+ * Abort the sink's signal with this reason the moment the engine reports
113
+ * it is parked on a `hang` — the one way the kit stops a turn from the
114
+ * outside (see the header on why not sooner).
115
+ */
116
+ readonly stopWhenHanging?: string;
117
+ }
118
+
119
+ export interface TurnInFlight {
120
+ readonly input: TurnInput;
121
+ readonly sink: RecordingTurnSink;
122
+ readonly settled: Promise<TurnOutcome>;
123
+ }
124
+
125
+ /**
126
+ * One execution's worth of turns against one subject, doing the runtime's
127
+ * and the server's bookkeeping between them. A new driver per test: it
128
+ * carries the threaded state id and the persisted status across
129
+ * reinvocations.
130
+ */
131
+ export class ExecutionDriver {
132
+ readonly executionId: string;
133
+ readonly sessionId: string;
134
+ private threadId: string;
135
+ private turnSeq = 0;
136
+ private persisted: AgentExecutionStatus | undefined;
137
+
138
+ constructor(private readonly subject: HarnessContractSubject, label: string) {
139
+ this.executionId = `exec-${label}`;
140
+ this.sessionId = `ses-${label}`;
141
+ // A deterministic harness's id is the runtime's, known before any engine
142
+ // exists (native's `thread-{sessionId}`); an engine-minted one has nothing
143
+ // until the adapter binds.
144
+ this.threadId = subject.adapter.capabilities.stateIdSource === "deterministic" ? `thread-${this.sessionId}` : "";
145
+ }
146
+
147
+ /** Start a turn without awaiting it, for invariants about turns in flight. */
148
+ begin(turn: TurnScenario, options: TurnOptions = {}): TurnInFlight {
149
+ const sink = options.sink ?? new RecordingTurnSink({ status: this.seedStatus() });
150
+ // The engine's view and the record's copy both derive from the status
151
+ // this turn folds into, so a prepared sink's status is honoured too.
152
+ const view: EngineView = {
153
+ executionId: this.executionId,
154
+ sessionId: this.sessionId,
155
+ approvalDecisions: approvalDecisionsOf(sink.status),
156
+ };
157
+ this.subject.arrange(turn, view);
158
+ // Registered BEFORE the turn is entered so a hang reached synchronously
159
+ // is still observed; the abort itself lands when the engine reports.
160
+ const parked = options.stopWhenHanging !== undefined ? this.subject.whenHanging() : undefined;
161
+ // The whole resolved record: the subject's needs under the driver's five
162
+ // facts and the persisted status (`turn-input-fixture.ts`).
163
+ const input: TurnInput = turnInputFixture({
164
+ ...this.subject.inputOverrides?.(view),
165
+ executionId: this.executionId,
166
+ threadId: this.threadId,
167
+ turnSeq: this.turnSeq,
168
+ sessionId: this.sessionId,
169
+ approvalDecisions: view.approvalDecisions,
170
+ persistedStatus: clone(AgentExecutionStatusSchema, sink.status),
171
+ });
172
+ const settled = this.subject.adapter.runTurn(input, sink);
173
+ if (parked && options.stopWhenHanging !== undefined) {
174
+ const reason = options.stopWhenHanging;
175
+ void parked.then(() => sink.abort(reason));
176
+ }
177
+ return { input, sink, settled };
178
+ }
179
+
180
+ /** Run a turn to its outcome and do the runtime's bookkeeping after it. */
181
+ async turn(turn: TurnScenario, options: TurnOptions = {}): Promise<TurnRun> {
182
+ const { input, sink, settled } = this.begin(turn, options);
183
+ const outcome = await settled;
184
+ this.recordTurn(sink);
185
+ return { input, sink, outcome };
186
+ }
187
+
188
+ /** The runtime's post-turn bookkeeping: persist the status, adopt a bound id, advance the cycle. */
189
+ recordTurn(sink: RecordingTurnSink): void {
190
+ this.persisted = sink.status;
191
+ const bound = sink.boundStateIds.at(-1);
192
+ if (bound !== undefined) this.threadId = bound;
193
+ this.turnSeq += 1;
194
+ }
195
+
196
+ /**
197
+ * The server's act between invocations: `SubmitApproval` writes the user's
198
+ * verdict onto the WAITING row it owns. A decision on a row that does not
199
+ * exist or is not waiting is a test bug, not a contract case.
200
+ */
201
+ decide(toolCallId: string, action: ApprovalAction): void {
202
+ const row = this.persisted ? findToolCallRow(this.persisted, toolCallId) : undefined;
203
+ if (!row || row.status !== ToolCallStatus.TOOL_CALL_WAITING_APPROVAL) {
204
+ const rows = (this.persisted?.messages ?? [])
205
+ .flatMap((m) => m.toolCalls)
206
+ .map((tc) => `${tc.id}=${ToolCallStatus[tc.status]}`)
207
+ .join(", ");
208
+ throw new Error(`${this.subject.name}: kit bug — decide('${toolCallId}') but no WAITING row was persisted (rows: ${rows || "none"})`);
209
+ }
210
+ row.approvalAction = action;
211
+ }
212
+
213
+ /** The status a reinvocation is seeded with: a clone of what was persisted. */
214
+ seedStatus(): AgentExecutionStatus {
215
+ return this.persisted ? clone(AgentExecutionStatusSchema, this.persisted) : emptyStatus();
216
+ }
217
+ }
218
+
219
+ // ── Shared assertion helpers ────────────────────────────────────────────────
220
+
221
+ /**
222
+ * Await a turn, translating a rejection into a contract violation that names
223
+ * the subject and the kind of throw. A `CancelledFailure` gets named
224
+ * specially because throwing it is the one thing an adapter is most tempted
225
+ * to do and must never do.
226
+ */
227
+ async function settleAsOutcome(subject: HarnessContractSubject, settled: Promise<TurnOutcome>, when: string): Promise<TurnOutcome> {
228
+ let outcome: TurnOutcome;
229
+ try {
230
+ outcome = await settled;
231
+ } catch (err) {
232
+ const kind = err instanceof CancelledFailure ? "a CancelledFailure" : `an exception (${err instanceof Error ? err.message : String(err)})`;
233
+ throw new Error(`${subject.name}: runTurn rejected with ${kind} ${when}; every exit must be a TurnOutcome`);
234
+ }
235
+ expect(OUTCOME_KINDS, `${subject.name}: runTurn settled with an unknown outcome kind ${when}`).toContain(outcome.kind);
236
+ return outcome;
237
+ }
238
+
239
+ /** Race a settlement against the interrupt bound; the timer is cleared on settle so a passing test holds nothing. */
240
+ async function settleWithinBound<T>(subject: HarnessContractSubject, settled: Promise<T>, when: string, boundMs: number): Promise<T> {
241
+ let timer: NodeJS.Timeout | undefined;
242
+ const bound = new Promise<never>((_, reject) => {
243
+ timer = setTimeout(
244
+ () => reject(new Error(`${subject.name}: runTurn did not settle within ${boundMs}ms ${when}; every adapter call must be bounded by stopSignal`)),
245
+ boundMs,
246
+ );
247
+ });
248
+ try {
249
+ return await Promise.race([settled, bound]);
250
+ } finally {
251
+ if (timer) clearTimeout(timer);
252
+ }
253
+ }
254
+
255
+ async function expectResolves(subject: HarnessContractSubject, call: Promise<void>, what: string): Promise<void> {
256
+ try {
257
+ await call;
258
+ } catch (err) {
259
+ throw new Error(`${subject.name}: ${what} rejected (${err instanceof Error ? err.message : String(err)}); it must resolve`);
260
+ }
261
+ }
262
+
263
+ // ── Invariant 1 ─────────────────────────────────────────────────────────────
264
+
265
+ /**
266
+ * Invariant 1: every exit is a `TurnOutcome`. Each scenario kind is played
267
+ * once, the hanging one is stopped from the outside, and one turn is entered
268
+ * with an already-aborted signal; none may reject, none may settle with an
269
+ * unknown kind.
270
+ */
271
+ export async function assertEveryExitIsAnOutcome(subject: HarnessContractSubject, boundMs = INTERRUPT_SETTLE_BOUND_MS): Promise<void> {
272
+ const driver = new ExecutionDriver(subject, "inv1");
273
+ const plays: ReadonlyArray<{ readonly when: string; readonly turn: TurnScenario; readonly options?: TurnOptions }> = [
274
+ { when: "on a plain text turn", turn: [scenario.say("hello")] },
275
+ { when: "on a usage-only turn", turn: [scenario.usage({ inputTokens: 1, outputTokens: 1 })] },
276
+ { when: "on an undecided proposal", turn: [scenario.propose("inv1-call", WRITE_ALPHA)] },
277
+ { when: "on an engine failure", turn: [scenario.fail("engine exploded")] },
278
+ { when: "on a hang stopped from the outside", turn: [scenario.hang()], options: { stopWhenHanging: "kit: stop" } },
279
+ ];
280
+ for (const play of plays) {
281
+ const { sink, settled } = driver.begin(play.turn, play.options);
282
+ await settleAsOutcome(subject, settleWithinBound(subject, settled, play.when, boundMs), play.when);
283
+ driver.recordTurn(sink);
284
+ }
285
+
286
+ const preAborted = new RecordingTurnSink({ status: driver.seedStatus() });
287
+ preAborted.abort("kit: aborted before the turn");
288
+ const { settled } = driver.begin([scenario.say("never")], { sink: preAborted });
289
+ await settleAsOutcome(subject, settleWithinBound(subject, settled, "under a pre-aborted signal", boundMs), "under a pre-aborted signal");
290
+ }
291
+
292
+ // ── Invariant 2 ─────────────────────────────────────────────────────────────
293
+
294
+ /**
295
+ * Invariant 2: a proposal is a WAITING_APPROVAL row and never executes in its
296
+ * own turn. The row must carry `requiresApproval` and an UNSPECIFIED
297
+ * `approvalAction` (the server's field, untouched by the adapter), and the
298
+ * turn must end `awaiting_approval` — not `completed`, which would tell the
299
+ * workflow nothing is pending.
300
+ */
301
+ export async function assertProposalIsWaitingAndUnexecuted(subject: HarnessContractSubject): Promise<void> {
302
+ const driver = new ExecutionDriver(subject, "inv2");
303
+ const id = "inv2-write";
304
+ const { outcome, sink } = await driver.turn([scenario.say("about to write"), scenario.propose(id, WRITE_ALPHA), scenario.say("after")]);
305
+
306
+ expect(outcome.kind, `${subject.name}: a turn that proposes an undecided action must end awaiting_approval`).toBe("awaiting_approval");
307
+ const row = findToolCallRow(sink.status, id);
308
+ expect(row, `${subject.name}: the proposal must be a tool-call row on sink.status when awaiting_approval resolves`).toBeDefined();
309
+ expect(row?.status, `${subject.name}: the proposal's row must be WAITING_APPROVAL`).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
310
+ expect(row?.requiresApproval, `${subject.name}: the proposal's row must carry requiresApproval`).toBe(true);
311
+ expect(row?.approvalAction, `${subject.name}: the adapter must never write the server's approvalAction`).toBe(ApprovalAction.UNSPECIFIED);
312
+ expect(subject.executionCount(id), `${subject.name}: an undecided proposal must not execute in its own turn`).toBe(0);
313
+ }
314
+
315
+ // ── Invariant 3 ─────────────────────────────────────────────────────────────
316
+
317
+ /**
318
+ * Invariant 3: the decision is honoured exactly. APPROVE executes once and
319
+ * only once, however many times the same decided row is seen again, and the
320
+ * engine's completion reaches the row; REJECT and SKIP never execute and the
321
+ * turn continues — the row's terminal status is NOT asserted here because
322
+ * it is not the adapter's to write (the decision is the server's field and
323
+ * the WAITING → SKIPPED transition follows from it with no engine knowledge,
324
+ * so it is the runtime's; S2 M4 Q-M4-1, the runtime's arm lands in S3). The
325
+ * reinvocation sees a CLONE of the persisted status, as it would from the
326
+ * server.
327
+ */
328
+ export async function assertDecisionsExecuteExactlyOnce(subject: HarnessContractSubject): Promise<void> {
329
+ const driver = new ExecutionDriver(subject, "inv3");
330
+
331
+ const approved = "inv3-approve";
332
+ const first = await driver.turn([scenario.propose(approved, WRITE_ALPHA)]);
333
+ expect(first.outcome.kind, `${subject.name}: proposal must end awaiting_approval`).toBe("awaiting_approval");
334
+ driver.decide(approved, ApprovalAction.APPROVE);
335
+
336
+ const resumed = await driver.turn([scenario.propose(approved, WRITE_ALPHA), scenario.say("done")]);
337
+ expect(resumed.outcome.kind, `${subject.name}: after APPROVE the turn must run to completion`).toBe("completed");
338
+ expect(subject.executionCount(approved), `${subject.name}: an approved action must execute exactly once`).toBe(1);
339
+ expect(findToolCallRow(resumed.sink.status, approved)?.status, `${subject.name}: the approved row must be carried to COMPLETED`).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
340
+
341
+ const again = await driver.turn([scenario.propose(approved, WRITE_ALPHA), scenario.say("again")]);
342
+ expect(again.outcome.kind, `${subject.name}: a settled proposal seen again must not re-gate`).toBe("completed");
343
+ expect(subject.executionCount(approved), `${subject.name}: reinvoked again after the approval, the action must still have executed exactly once`).toBe(1);
344
+
345
+ for (const [label, action] of [["REJECT", ApprovalAction.REJECT], ["SKIP", ApprovalAction.SKIP]] as const) {
346
+ const id = `inv3-${label.toLowerCase()}`;
347
+ const proposed = await driver.turn([scenario.propose(id, WRITE_ALPHA)]);
348
+ expect(proposed.outcome.kind, `${subject.name}: proposal must end awaiting_approval`).toBe("awaiting_approval");
349
+ driver.decide(id, action);
350
+ const decided = await driver.turn([scenario.propose(id, WRITE_ALPHA), scenario.say("moving on")]);
351
+ expect(decided.outcome.kind, `${subject.name}: after ${label} the turn must continue to completion`).toBe("completed");
352
+ expect(subject.executionCount(id), `${subject.name}: a ${label}-ed action must never execute`).toBe(0);
353
+ expect(findToolCallRow(decided.sink.status, id)?.status, `${subject.name}: a ${label}-ed action must never be reported as executed`).not.toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
354
+ }
355
+ }
356
+
357
+ // ── Invariant 4 ─────────────────────────────────────────────────────────────
358
+
359
+ /**
360
+ * Invariant 4: the stop signal is honoured promptly and completely. A turn
361
+ * stopped WHILE HANGING (the engine has reported it is parked, so the stop
362
+ * lands in the hang and not in the adapter's setup) settles `interrupted`
363
+ * within the bound and executes nothing that came after the hang, even an
364
+ * already-approved action; a turn entered under an aborted signal does
365
+ * nothing at all.
366
+ */
367
+ export async function assertStopSignalInterrupts(subject: HarnessContractSubject, boundMs = INTERRUPT_SETTLE_BOUND_MS): Promise<void> {
368
+ const driver = new ExecutionDriver(subject, "inv4");
369
+ const id = "inv4-approved-after-hang";
370
+
371
+ const proposed = await driver.turn([scenario.propose(id, WRITE_ALPHA)]);
372
+ expect(proposed.outcome.kind, `${subject.name}: proposal must end awaiting_approval`).toBe("awaiting_approval");
373
+ driver.decide(id, ApprovalAction.APPROVE);
374
+
375
+ const hanging = driver.begin([scenario.say("working"), scenario.hang(), scenario.propose(id, WRITE_ALPHA)], { stopWhenHanging: "kit: user pause" });
376
+ const outcome = await settleWithinBound(subject, hanging.settled, "after stopSignal aborted mid-hang", boundMs);
377
+ expect(outcome.kind, `${subject.name}: a turn stopped mid-hang must settle interrupted`).toBe("interrupted");
378
+ expect(subject.executionCount(id), `${subject.name}: nothing after the stop may execute, even an approved action`).toBe(0);
379
+ driver.recordTurn(hanging.sink);
380
+
381
+ const preAborted = new RecordingTurnSink({ status: driver.seedStatus() });
382
+ preAborted.abort("kit: aborted before the turn");
383
+ const early = driver.begin([scenario.propose(id, WRITE_ALPHA)], { sink: preAborted });
384
+ const earlyOutcome = await settleWithinBound(subject, early.settled, "under a pre-aborted signal", boundMs);
385
+ expect(earlyOutcome.kind, `${subject.name}: a turn entered under an aborted signal must settle interrupted`).toBe("interrupted");
386
+ expect(subject.executionCount(id), `${subject.name}: a turn entered under an aborted signal must do no work`).toBe(0);
387
+ expect(preAborted.persistRequests, `${subject.name}: a turn entered under an aborted signal must not ask to persist`).toBe(0);
388
+ }
389
+
390
+ // ── Invariant 5 ─────────────────────────────────────────────────────────────
391
+
392
+ /** The four counts summed; the price and its basis are the adapter's per delta and are not summed here. */
393
+ interface UsageTotals {
394
+ inputTokens: number;
395
+ outputTokens: number;
396
+ cacheReadTokens: number;
397
+ cacheWriteTokens: number;
398
+ }
399
+
400
+ const TOKEN_COUNT_FIELDS = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"] as const satisfies readonly (keyof UsageTotals)[];
401
+
402
+ function sumUsage(deltas: readonly UsageDelta[]): UsageTotals {
403
+ const total: UsageTotals = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
404
+ for (const d of deltas) {
405
+ for (const field of TOKEN_COUNT_FIELDS) total[field] += d[field] ?? 0;
406
+ }
407
+ return total;
408
+ }
409
+
410
+ /**
411
+ * Invariant 5: usage reaches the sink as deltas — the token counts and the
412
+ * price non-negative, and the counts summing to what the engine emitted. An
413
+ * adapter that reports cumulative totals instead of deltas would
414
+ * double-count in the runtime's accumulator and trip the cost cap early; one
415
+ * that reports a negative number would credit it. The basis a delta names
416
+ * (`model`, `requestedModelParams`) is the adapter's own and not judged here.
417
+ */
418
+ export async function assertUsageReachesSinkAsDeltas(subject: HarnessContractSubject): Promise<void> {
419
+ const driver = new ExecutionDriver(subject, "inv5");
420
+ const emitted: readonly UsageDelta[] = [
421
+ { inputTokens: 10, outputTokens: 5 },
422
+ { inputTokens: 3, cacheReadTokens: 2 },
423
+ { outputTokens: 4, cacheWriteTokens: 1 },
424
+ ];
425
+ const { outcome, sink } = await driver.turn([...emitted.map((d) => scenario.usage(d)), scenario.say("done")]);
426
+ expect(outcome.kind, `${subject.name}: a usage-reporting turn must complete`).toBe("completed");
427
+
428
+ for (const delta of sink.usageDeltas) {
429
+ for (const field of [...TOKEN_COUNT_FIELDS, "estimatedCostUsd"] as const) {
430
+ const value = delta[field];
431
+ if (value === undefined) continue;
432
+ expect(value, `${subject.name}: usage delta field ${field} must be non-negative`).toBeGreaterThanOrEqual(0);
433
+ }
434
+ }
435
+ expect(sumUsage(sink.usageDeltas), `${subject.name}: the usage deltas must sum to what the engine emitted`).toEqual(sumUsage(emitted));
436
+ }
437
+
438
+ // ── Invariant 6 ─────────────────────────────────────────────────────────────
439
+
440
+ /**
441
+ * Invariant 6: capability and behaviour agree on the state id. Engine-minted:
442
+ * the bind precedes the first persist (so a crash mid-turn still resumes),
443
+ * the id threaded back on the next invocation is accepted, and a rejected
444
+ * bind ends the turn `failed` with nothing done after it. Deterministic: the
445
+ * adapter never binds and accepts the runtime's id on every turn.
446
+ */
447
+ export async function assertStateIdCapabilityAgrees(subject: HarnessContractSubject): Promise<void> {
448
+ const driver = new ExecutionDriver(subject, "inv6");
449
+ const first = await driver.turn([scenario.say("first turn")]);
450
+ expect(first.outcome.kind, `${subject.name}: a plain turn must complete`).toBe("completed");
451
+
452
+ if (subject.adapter.capabilities.stateIdSource === "engine-minted") {
453
+ expect(first.input.threadId, `${subject.name}: an engine-minted harness has no state id before its first turn`).toBe("");
454
+ const events = first.sink.events;
455
+ const firstBind = events.findIndex((e) => e.kind === "bind");
456
+ const firstPersist = events.findIndex((e) => e.kind === "persist");
457
+ expect(firstBind, `${subject.name}: an engine-minted adapter must bind its state id on the first turn`).toBeGreaterThanOrEqual(0);
458
+ expect(first.sink.boundStateIds[0], `${subject.name}: the bound state id must be non-empty`).toBeTruthy();
459
+ if (firstPersist >= 0) {
460
+ expect(firstBind, `${subject.name}: the state id must be bound BEFORE the first persist request, or a crash mid-turn cannot resume`).toBeLessThan(firstPersist);
461
+ }
462
+
463
+ const second = await driver.turn([scenario.say("second turn")]);
464
+ expect(second.input.threadId, `${subject.name}: the kit threads the bound id back as threadId`).toBe(first.sink.boundStateIds[0]);
465
+ expect(second.outcome.kind, `${subject.name}: resuming by the id the adapter bound must complete`).toBe("completed");
466
+ expect(second.sink.boundStateIds, `${subject.name}: a resumed turn must not bind a new state id`).toHaveLength(0);
467
+
468
+ const failing = new ExecutionDriver(subject, "inv6-bind-rejects");
469
+ const rejecting = new RecordingTurnSink({ bindRejectsWith: new Error("session write failed") });
470
+ const rejected = await failing.turn([scenario.say("never persisted")], { sink: rejecting });
471
+ expect(rejected.outcome.kind, `${subject.name}: a rejected bind must end the turn failed, not reject runTurn`).toBe("failed");
472
+ expect(rejecting.persistRequests, `${subject.name}: nothing may be persisted after a rejected bind`).toBe(0);
473
+ expect(rejecting.status.messages, `${subject.name}: nothing may be folded into the status after a rejected bind`).toHaveLength(0);
474
+ } else {
475
+ expect(first.input.threadId, `${subject.name}: a deterministic harness is handed the runtime's id on its first turn`).not.toBe("");
476
+ expect(first.sink.boundStateIds, `${subject.name}: a deterministic adapter must never bind a state id`).toHaveLength(0);
477
+ const second = await driver.turn([scenario.say("second turn")]);
478
+ expect(second.input.threadId, `${subject.name}: a deterministic id is the same on every turn`).toBe(first.input.threadId);
479
+ expect(second.outcome.kind, `${subject.name}: a deterministic resume must complete`).toBe("completed");
480
+ expect(second.sink.boundStateIds, `${subject.name}: a deterministic adapter must never bind a state id`).toHaveLength(0);
481
+ }
482
+ }
483
+
484
+ // ── Invariant 7 ─────────────────────────────────────────────────────────────
485
+
486
+ /**
487
+ * Invariant 7: the lifetimes resolve. On the booted adapter: serve a turn,
488
+ * release the served session and an unknown one, shut down, and boot again
489
+ * (a worker restart re-boots the same module state). Each must resolve — a
490
+ * rejection in any would leak a session's resources, hang a drain, or fail
491
+ * the next worker's boot. Boot itself is proven by every turn before this
492
+ * one having run.
493
+ */
494
+ export async function assertLifetimesResolve(subject: HarnessContractSubject): Promise<void> {
495
+ const { adapter } = subject;
496
+ const driver = new ExecutionDriver(subject, "inv7");
497
+ const served = await driver.turn([scenario.say("served")]);
498
+ expect(served.outcome.kind, `${subject.name}: a plain turn must complete`).toBe("completed");
499
+
500
+ await expectResolves(subject, adapter.releaseSession(served.input.sessionId), `releaseSession('${served.input.sessionId}') for a served session`);
501
+ await expectResolves(subject, adapter.releaseSession("ses-never-served"), "releaseSession for an unknown session");
502
+ await expectResolves(subject, adapter.shutdown(), "shutdown() after release");
503
+ await expectResolves(subject, adapter.boot(subject.config), "boot(config) after shutdown (a worker restart)");
504
+ }
505
+
506
+ // ── Invariant 8 ─────────────────────────────────────────────────────────────
507
+
508
+ /**
509
+ * Invariant 8: one adapter object, many turns. While one execution's turn
510
+ * hangs (reported parked before the other execution starts, so the two are
511
+ * truly concurrent and not merely queued), another execution's turns
512
+ * propose, get decided and execute on the SAME adapter; stopping the hanging
513
+ * turn settles only it; nothing crosses. This is "the adapter holds no
514
+ * per-turn state" made observable — the property `maxConcurrentActivities`
515
+ * relies on.
516
+ */
517
+ export async function assertConcurrentTurnsAreIndependent(subject: HarnessContractSubject, boundMs = INTERRUPT_SETTLE_BOUND_MS): Promise<void> {
518
+ const hangingExecution = new ExecutionDriver(subject, "inv8-hanging");
519
+ const busyExecution = new ExecutionDriver(subject, "inv8-busy");
520
+ const id = "inv8-busy-write";
521
+
522
+ const parked = subject.whenHanging();
523
+ const hanging = hangingExecution.begin([scenario.say("parked"), scenario.hang()]);
524
+ await settleWithinBound(subject, parked, "waiting for the first execution to park on its hang", boundMs);
525
+
526
+ const proposed = await settleWithinBound(subject, busyExecution.turn([scenario.propose(id, WRITE_ALPHA)]), "on a second execution while another turn hangs", boundMs);
527
+ expect(proposed.outcome.kind, `${subject.name}: a second execution's proposal must settle while another turn hangs`).toBe("awaiting_approval");
528
+ busyExecution.decide(id, ApprovalAction.APPROVE);
529
+ const resumed = await settleWithinBound(subject, busyExecution.turn([scenario.propose(id, WRITE_ALPHA)]), "on a second execution's reinvocation while another turn hangs", boundMs);
530
+ expect(resumed.outcome.kind, `${subject.name}: a second execution's approved turn must complete while another turn hangs`).toBe("completed");
531
+ expect(subject.executionCount(id), `${subject.name}: the busy execution's action must execute exactly once`).toBe(1);
532
+
533
+ hanging.sink.abort("kit: stop the parked turn");
534
+ const stopped = await settleWithinBound(subject, hanging.settled, "after the hanging turn was stopped", boundMs);
535
+ expect(stopped.kind, `${subject.name}: the hanging turn must settle interrupted, and only it`).toBe("interrupted");
536
+ expect(subject.executionCount(id), `${subject.name}: stopping one execution must not touch another's execution count`).toBe(1);
537
+ expect(findToolCallRow(hanging.sink.status, id), `${subject.name}: one execution's rows must never appear on another's status`).toBeUndefined();
538
+ }
539
+
540
+ // ── The runnable suite ──────────────────────────────────────────────────────
541
+
542
+ /**
543
+ * Register the adapter-side contract against one subject. Boots the adapter
544
+ * once before the first invariant and shuts it down after the last, as the
545
+ * registry does around a worker's life (`bootHarnesses` / `shutdownHarnesses`).
546
+ */
547
+ export function describeHarnessContract(subject: HarnessContractSubject): void {
548
+ describe(`harness contract — ${subject.name}`, () => {
549
+ beforeAll(async () => {
550
+ await subject.adapter.boot(subject.config);
551
+ });
552
+ afterAll(async () => {
553
+ await subject.adapter.shutdown();
554
+ });
555
+
556
+ it("settles every turn with a TurnOutcome and never rejects (invariant 1)", async () => {
557
+ await assertEveryExitIsAnOutcome(subject);
558
+ });
559
+ it("surfaces a proposal as a WAITING row and never executes it in its turn (invariant 2)", async () => {
560
+ await assertProposalIsWaitingAndUnexecuted(subject);
561
+ });
562
+ it("executes an approval exactly once and a rejection or skip never (invariant 3)", async () => {
563
+ await assertDecisionsExecuteExactlyOnce(subject);
564
+ });
565
+ it("settles interrupted promptly when stopSignal aborts and does nothing after it (invariant 4)", async () => {
566
+ await assertStopSignalInterrupts(subject);
567
+ });
568
+ it("reports usage as non-negative deltas that sum to what the engine emitted (invariant 5)", async () => {
569
+ await assertUsageReachesSinkAsDeltas(subject);
570
+ });
571
+ it(`binds its state id as its ${subject.adapter.capabilities.stateIdSource} capability declares (invariant 6)`, async () => {
572
+ await assertStateIdCapabilityAgrees(subject);
573
+ });
574
+ it("serves, releases, shuts down and re-boots without a rejection (invariant 7)", async () => {
575
+ await assertLifetimesResolve(subject);
576
+ });
577
+ it("serves concurrent turns on one adapter object independently (invariant 8)", async () => {
578
+ await assertConcurrentTurnsAreIndependent(subject);
579
+ });
580
+ });
581
+ }