@earendil-works/pi-agent-core 0.84.3 → 0.85.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 (343) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +32 -25
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/harness/agent-harness.d.ts +639 -384
  6. package/dist/harness/agent-harness.d.ts.map +1 -1
  7. package/dist/harness/agent-harness.js +5 -251
  8. package/dist/harness/agent-harness.js.map +1 -1
  9. package/dist/harness/compaction/branch-summarization.d.ts +12 -6
  10. package/dist/harness/compaction/branch-summarization.d.ts.map +1 -1
  11. package/dist/harness/compaction/branch-summarization.js +33 -26
  12. package/dist/harness/compaction/branch-summarization.js.map +1 -1
  13. package/dist/harness/compaction/compaction.d.ts +30 -8
  14. package/dist/harness/compaction/compaction.d.ts.map +1 -1
  15. package/dist/harness/compaction/compaction.js +53 -63
  16. package/dist/harness/compaction/compaction.js.map +1 -1
  17. package/dist/harness/config.d.ts +9 -0
  18. package/dist/harness/config.d.ts.map +1 -0
  19. package/dist/harness/config.js +27 -0
  20. package/dist/harness/config.js.map +1 -0
  21. package/dist/harness/context.d.ts +9 -0
  22. package/dist/harness/context.d.ts.map +1 -0
  23. package/dist/harness/context.js +13 -0
  24. package/dist/harness/context.js.map +1 -0
  25. package/dist/harness/env/nodejs.d.ts +24 -28
  26. package/dist/harness/env/nodejs.d.ts.map +1 -1
  27. package/dist/harness/env/nodejs.js +242 -87
  28. package/dist/harness/env/nodejs.js.map +1 -1
  29. package/dist/harness/events.d.ts +20 -38
  30. package/dist/harness/events.d.ts.map +1 -1
  31. package/dist/harness/events.js +240 -55
  32. package/dist/harness/events.js.map +1 -1
  33. package/dist/harness/execution/assistant.d.ts +42 -0
  34. package/dist/harness/execution/assistant.d.ts.map +1 -0
  35. package/dist/harness/execution/assistant.js +82 -0
  36. package/dist/harness/execution/assistant.js.map +1 -0
  37. package/dist/harness/execution/effect-gate.d.ts +22 -0
  38. package/dist/harness/execution/effect-gate.d.ts.map +1 -0
  39. package/dist/harness/execution/effect-gate.js +49 -0
  40. package/dist/harness/execution/effect-gate.js.map +1 -0
  41. package/dist/harness/execution/tools.d.ts +67 -0
  42. package/dist/harness/execution/tools.d.ts.map +1 -0
  43. package/dist/harness/execution/tools.js +121 -0
  44. package/dist/harness/execution/tools.js.map +1 -0
  45. package/dist/harness/hooks.d.ts +38 -0
  46. package/dist/harness/hooks.d.ts.map +1 -0
  47. package/dist/harness/hooks.js +406 -0
  48. package/dist/harness/hooks.js.map +1 -0
  49. package/dist/harness/messages.d.ts +2 -2
  50. package/dist/harness/messages.d.ts.map +1 -1
  51. package/dist/harness/messages.js.map +1 -1
  52. package/dist/harness/prompt-templates.d.ts +3 -2
  53. package/dist/harness/prompt-templates.d.ts.map +1 -1
  54. package/dist/harness/prompt-templates.js +17 -17
  55. package/dist/harness/prompt-templates.js.map +1 -1
  56. package/dist/harness/result.d.ts +93 -0
  57. package/dist/harness/result.d.ts.map +1 -1
  58. package/dist/harness/result.js +40 -0
  59. package/dist/harness/result.js.map +1 -1
  60. package/dist/harness/runtime/drive/boundary.d.ts +28 -0
  61. package/dist/harness/runtime/drive/boundary.d.ts.map +1 -0
  62. package/dist/harness/runtime/drive/boundary.js +170 -0
  63. package/dist/harness/runtime/drive/boundary.js.map +1 -0
  64. package/dist/harness/runtime/drive/checkpoint.d.ts +8 -0
  65. package/dist/harness/runtime/drive/checkpoint.d.ts.map +1 -0
  66. package/dist/harness/runtime/drive/checkpoint.js +133 -0
  67. package/dist/harness/runtime/drive/checkpoint.js.map +1 -0
  68. package/dist/harness/runtime/drive/deferred.d.ts +15 -0
  69. package/dist/harness/runtime/drive/deferred.d.ts.map +1 -0
  70. package/dist/harness/runtime/drive/deferred.js +179 -0
  71. package/dist/harness/runtime/drive/deferred.js.map +1 -0
  72. package/dist/harness/runtime/drive/generation.d.ts +8 -0
  73. package/dist/harness/runtime/drive/generation.d.ts.map +1 -0
  74. package/dist/harness/runtime/drive/generation.js +205 -0
  75. package/dist/harness/runtime/drive/generation.js.map +1 -0
  76. package/dist/harness/runtime/drive/reconcile.d.ts +5 -0
  77. package/dist/harness/runtime/drive/reconcile.d.ts.map +1 -0
  78. package/dist/harness/runtime/drive/reconcile.js +140 -0
  79. package/dist/harness/runtime/drive/reconcile.js.map +1 -0
  80. package/dist/harness/runtime/drive/recovery.d.ts +8 -0
  81. package/dist/harness/runtime/drive/recovery.d.ts.map +1 -0
  82. package/dist/harness/runtime/drive/recovery.js +85 -0
  83. package/dist/harness/runtime/drive/recovery.js.map +1 -0
  84. package/dist/harness/runtime/drive/response.d.ts +23 -0
  85. package/dist/harness/runtime/drive/response.d.ts.map +1 -0
  86. package/dist/harness/runtime/drive/response.js +394 -0
  87. package/dist/harness/runtime/drive/response.js.map +1 -0
  88. package/dist/harness/runtime/drive/retry.d.ts +4 -0
  89. package/dist/harness/runtime/drive/retry.d.ts.map +1 -0
  90. package/dist/harness/runtime/drive/retry.js +37 -0
  91. package/dist/harness/runtime/drive/retry.js.map +1 -0
  92. package/dist/harness/runtime/drive/structural.d.ts +32 -0
  93. package/dist/harness/runtime/drive/structural.d.ts.map +1 -0
  94. package/dist/harness/runtime/drive/structural.js +929 -0
  95. package/dist/harness/runtime/drive/structural.js.map +1 -0
  96. package/dist/harness/runtime/drive/terminal.d.ts +7 -0
  97. package/dist/harness/runtime/drive/terminal.d.ts.map +1 -0
  98. package/dist/harness/runtime/drive/terminal.js +49 -0
  99. package/dist/harness/runtime/drive/terminal.js.map +1 -0
  100. package/dist/harness/runtime/drive/tool-placement.d.ts +14 -0
  101. package/dist/harness/runtime/drive/tool-placement.d.ts.map +1 -0
  102. package/dist/harness/runtime/drive/tool-placement.js +244 -0
  103. package/dist/harness/runtime/drive/tool-placement.js.map +1 -0
  104. package/dist/harness/runtime/drive/tools.d.ts +6 -0
  105. package/dist/harness/runtime/drive/tools.d.ts.map +1 -0
  106. package/dist/harness/runtime/drive/tools.js +449 -0
  107. package/dist/harness/runtime/drive/tools.js.map +1 -0
  108. package/dist/harness/runtime/drive.d.ts +6 -0
  109. package/dist/harness/runtime/drive.d.ts.map +1 -0
  110. package/dist/harness/runtime/drive.js +93 -0
  111. package/dist/harness/runtime/drive.js.map +1 -0
  112. package/dist/harness/runtime/harness.d.ts +59 -0
  113. package/dist/harness/runtime/harness.d.ts.map +1 -0
  114. package/dist/harness/runtime/harness.js +311 -0
  115. package/dist/harness/runtime/harness.js.map +1 -0
  116. package/dist/harness/runtime/index.d.ts +2 -0
  117. package/dist/harness/runtime/index.d.ts.map +1 -0
  118. package/dist/harness/runtime/index.js +2 -0
  119. package/dist/harness/runtime/index.js.map +1 -0
  120. package/dist/harness/runtime/lane.d.ts +101 -0
  121. package/dist/harness/runtime/lane.d.ts.map +1 -0
  122. package/dist/harness/runtime/lane.js +1575 -0
  123. package/dist/harness/runtime/lane.js.map +1 -0
  124. package/dist/harness/runtime/progress.d.ts +15 -0
  125. package/dist/harness/runtime/progress.d.ts.map +1 -0
  126. package/dist/harness/runtime/progress.js +66 -0
  127. package/dist/harness/runtime/progress.js.map +1 -0
  128. package/dist/harness/runtime/reducer.d.ts +5 -0
  129. package/dist/harness/runtime/reducer.d.ts.map +1 -0
  130. package/dist/harness/runtime/reducer.js +240 -0
  131. package/dist/harness/runtime/reducer.js.map +1 -0
  132. package/dist/harness/runtime/restore.d.ts +24 -0
  133. package/dist/harness/runtime/restore.d.ts.map +1 -0
  134. package/dist/harness/runtime/restore.js +117 -0
  135. package/dist/harness/runtime/restore.js.map +1 -0
  136. package/dist/harness/runtime/transcript.d.ts +21 -0
  137. package/dist/harness/runtime/transcript.d.ts.map +1 -0
  138. package/dist/harness/runtime/transcript.js +72 -0
  139. package/dist/harness/runtime/transcript.js.map +1 -0
  140. package/dist/harness/runtime/types.d.ts +105 -0
  141. package/dist/harness/runtime/types.d.ts.map +1 -0
  142. package/dist/harness/runtime/types.js +61 -0
  143. package/dist/harness/runtime/types.js.map +1 -0
  144. package/dist/harness/session/commit.d.ts +53 -0
  145. package/dist/harness/session/commit.d.ts.map +1 -0
  146. package/dist/harness/session/commit.js +57 -0
  147. package/dist/harness/session/commit.js.map +1 -0
  148. package/dist/harness/session/context.d.ts +6 -18
  149. package/dist/harness/session/context.d.ts.map +1 -1
  150. package/dist/harness/session/context.js +31 -49
  151. package/dist/harness/session/context.js.map +1 -1
  152. package/dist/harness/session/fork-policy.d.ts +13 -0
  153. package/dist/harness/session/fork-policy.d.ts.map +1 -0
  154. package/dist/harness/session/fork-policy.js +22 -0
  155. package/dist/harness/session/fork-policy.js.map +1 -0
  156. package/dist/harness/session/fork.d.ts +18 -0
  157. package/dist/harness/session/fork.d.ts.map +1 -0
  158. package/dist/harness/session/fork.js +127 -0
  159. package/dist/harness/session/fork.js.map +1 -0
  160. package/dist/harness/session/in-memory-storage-state.d.ts +39 -0
  161. package/dist/harness/session/in-memory-storage-state.d.ts.map +1 -0
  162. package/dist/harness/session/in-memory-storage-state.js +216 -0
  163. package/dist/harness/session/in-memory-storage-state.js.map +1 -0
  164. package/dist/harness/session/index.d.ts +9 -5
  165. package/dist/harness/session/index.d.ts.map +1 -1
  166. package/dist/harness/session/index.js +7 -4
  167. package/dist/harness/session/index.js.map +1 -1
  168. package/dist/harness/session/jsonl/codec.d.ts +19 -8
  169. package/dist/harness/session/jsonl/codec.d.ts.map +1 -1
  170. package/dist/harness/session/jsonl/codec.js +34 -218
  171. package/dist/harness/session/jsonl/codec.js.map +1 -1
  172. package/dist/harness/session/jsonl/index.d.ts +4 -0
  173. package/dist/harness/session/jsonl/index.d.ts.map +1 -0
  174. package/dist/harness/session/jsonl/index.js +4 -0
  175. package/dist/harness/session/jsonl/index.js.map +1 -0
  176. package/dist/harness/session/jsonl/legacy-v3.d.ts +18 -0
  177. package/dist/harness/session/jsonl/legacy-v3.d.ts.map +1 -0
  178. package/dist/harness/session/jsonl/legacy-v3.js +383 -0
  179. package/dist/harness/session/jsonl/legacy-v3.js.map +1 -0
  180. package/dist/harness/session/jsonl/repo.d.ts +26 -19
  181. package/dist/harness/session/jsonl/repo.d.ts.map +1 -1
  182. package/dist/harness/session/jsonl/repo.js +266 -156
  183. package/dist/harness/session/jsonl/repo.js.map +1 -1
  184. package/dist/harness/session/jsonl/storage.d.ts +39 -41
  185. package/dist/harness/session/jsonl/storage.d.ts.map +1 -1
  186. package/dist/harness/session/jsonl/storage.js +238 -181
  187. package/dist/harness/session/jsonl/storage.js.map +1 -1
  188. package/dist/harness/session/jsonl/types.d.ts +23 -22
  189. package/dist/harness/session/jsonl/types.d.ts.map +1 -1
  190. package/dist/harness/session/jsonl/types.js +2 -1
  191. package/dist/harness/session/jsonl/types.js.map +1 -1
  192. package/dist/harness/session/memory.d.ts +46 -40
  193. package/dist/harness/session/memory.d.ts.map +1 -1
  194. package/dist/harness/session/memory.js +346 -113
  195. package/dist/harness/session/memory.js.map +1 -1
  196. package/dist/harness/session/mutation-line.d.ts +8 -0
  197. package/dist/harness/session/mutation-line.d.ts.map +1 -0
  198. package/dist/harness/session/mutation-line.js +21 -0
  199. package/dist/harness/session/mutation-line.js.map +1 -0
  200. package/dist/harness/session/session.d.ts +78 -46
  201. package/dist/harness/session/session.d.ts.map +1 -1
  202. package/dist/harness/session/session.js +343 -209
  203. package/dist/harness/session/session.js.map +1 -1
  204. package/dist/harness/session/testing/benchmark/datasets.d.ts +12 -0
  205. package/dist/harness/session/testing/benchmark/datasets.d.ts.map +1 -0
  206. package/dist/harness/session/testing/benchmark/datasets.js +24 -0
  207. package/dist/harness/session/testing/benchmark/datasets.js.map +1 -0
  208. package/dist/harness/session/testing/benchmark/session-repo.d.ts +42 -0
  209. package/dist/harness/session/testing/benchmark/session-repo.d.ts.map +1 -0
  210. package/dist/harness/session/testing/benchmark/session-repo.js +127 -0
  211. package/dist/harness/session/testing/benchmark/session-repo.js.map +1 -0
  212. package/dist/harness/session/testing/benchmark/storage.d.ts +25 -0
  213. package/dist/harness/session/testing/benchmark/storage.d.ts.map +1 -0
  214. package/dist/harness/session/testing/benchmark/storage.js +114 -0
  215. package/dist/harness/session/testing/benchmark/storage.js.map +1 -0
  216. package/dist/harness/session/testing/conformance/session-repo.d.ts +21 -0
  217. package/dist/harness/session/testing/conformance/session-repo.d.ts.map +1 -0
  218. package/dist/harness/session/testing/conformance/session-repo.js +590 -0
  219. package/dist/harness/session/testing/conformance/session-repo.js.map +1 -0
  220. package/dist/harness/session/testing/conformance/storage.d.ts +4 -0
  221. package/dist/harness/session/testing/conformance/storage.d.ts.map +1 -0
  222. package/dist/harness/session/testing/conformance/storage.js +600 -0
  223. package/dist/harness/session/testing/conformance/storage.js.map +1 -0
  224. package/dist/harness/session/testing/gating-storage.d.ts +26 -0
  225. package/dist/harness/session/testing/gating-storage.d.ts.map +1 -0
  226. package/dist/harness/session/testing/gating-storage.js +100 -0
  227. package/dist/harness/session/testing/gating-storage.js.map +1 -0
  228. package/dist/harness/session/testing/index.d.ts +9 -2
  229. package/dist/harness/session/testing/index.d.ts.map +1 -1
  230. package/dist/harness/session/testing/index.js +8 -1
  231. package/dist/harness/session/testing/index.js.map +1 -1
  232. package/dist/harness/session/testing/instrumented-storage.d.ts +11 -0
  233. package/dist/harness/session/testing/instrumented-storage.d.ts.map +1 -0
  234. package/dist/harness/session/testing/instrumented-storage.js +16 -0
  235. package/dist/harness/session/testing/instrumented-storage.js.map +1 -0
  236. package/dist/harness/session/testing/storage-decorator.d.ts +20 -0
  237. package/dist/harness/session/testing/storage-decorator.d.ts.map +1 -0
  238. package/dist/harness/session/testing/storage-decorator.js +41 -0
  239. package/dist/harness/session/testing/storage-decorator.js.map +1 -0
  240. package/dist/harness/session/testing/types.d.ts +5 -7
  241. package/dist/harness/session/testing/types.d.ts.map +1 -1
  242. package/dist/harness/session/testing/types.js.map +1 -1
  243. package/dist/harness/session/types.d.ts +424 -259
  244. package/dist/harness/session/types.d.ts.map +1 -1
  245. package/dist/harness/session/types.js +7 -8
  246. package/dist/harness/session/types.js.map +1 -1
  247. package/dist/harness/session/values.d.ts +95 -0
  248. package/dist/harness/session/values.d.ts.map +1 -0
  249. package/dist/harness/session/values.js +81 -0
  250. package/dist/harness/session/values.js.map +1 -0
  251. package/dist/harness/skills.d.ts +3 -2
  252. package/dist/harness/skills.d.ts.map +1 -1
  253. package/dist/harness/skills.js +29 -26
  254. package/dist/harness/skills.js.map +1 -1
  255. package/dist/harness/telemetry.d.ts +64 -43
  256. package/dist/harness/telemetry.d.ts.map +1 -1
  257. package/dist/harness/telemetry.js +45 -26
  258. package/dist/harness/telemetry.js.map +1 -1
  259. package/dist/harness/tools/bash.d.ts +4 -4
  260. package/dist/harness/tools/bash.d.ts.map +1 -1
  261. package/dist/harness/tools/bash.js +66 -93
  262. package/dist/harness/tools/bash.js.map +1 -1
  263. package/dist/harness/tools/edit.d.ts.map +1 -1
  264. package/dist/harness/tools/edit.js +10 -10
  265. package/dist/harness/tools/edit.js.map +1 -1
  266. package/dist/harness/tools/file-mutation-queue.d.ts +2 -1
  267. package/dist/harness/tools/file-mutation-queue.d.ts.map +1 -1
  268. package/dist/harness/tools/file-mutation-queue.js +5 -5
  269. package/dist/harness/tools/file-mutation-queue.js.map +1 -1
  270. package/dist/harness/tools/path-utils.d.ts +3 -2
  271. package/dist/harness/tools/path-utils.d.ts.map +1 -1
  272. package/dist/harness/tools/path-utils.js +5 -5
  273. package/dist/harness/tools/path-utils.js.map +1 -1
  274. package/dist/harness/tools/read.d.ts +2 -1
  275. package/dist/harness/tools/read.d.ts.map +1 -1
  276. package/dist/harness/tools/read.js +4 -6
  277. package/dist/harness/tools/read.js.map +1 -1
  278. package/dist/harness/tools/write.d.ts.map +1 -1
  279. package/dist/harness/tools/write.js +7 -7
  280. package/dist/harness/tools/write.js.map +1 -1
  281. package/dist/harness/types.d.ts +108 -45
  282. package/dist/harness/types.d.ts.map +1 -1
  283. package/dist/harness/types.js.map +1 -1
  284. package/dist/harness/utils/adaptive-publisher.d.ts +24 -0
  285. package/dist/harness/utils/adaptive-publisher.d.ts.map +1 -0
  286. package/dist/harness/utils/adaptive-publisher.js +79 -0
  287. package/dist/harness/utils/adaptive-publisher.js.map +1 -0
  288. package/dist/harness/utils/output-capture.d.ts +31 -0
  289. package/dist/harness/utils/output-capture.d.ts.map +1 -0
  290. package/dist/harness/utils/output-capture.js +217 -0
  291. package/dist/harness/utils/output-capture.js.map +1 -0
  292. package/dist/harness/utils/shell-output.d.ts +12 -5
  293. package/dist/harness/utils/shell-output.d.ts.map +1 -1
  294. package/dist/harness/utils/shell-output.js +64 -174
  295. package/dist/harness/utils/shell-output.js.map +1 -1
  296. package/dist/harness/utils/truncate.d.ts +1 -0
  297. package/dist/harness/utils/truncate.d.ts.map +1 -1
  298. package/dist/harness/utils/truncate.js +1 -1
  299. package/dist/harness/utils/truncate.js.map +1 -1
  300. package/dist/harness/utils/usage.d.ts +4 -0
  301. package/dist/harness/utils/usage.d.ts.map +1 -0
  302. package/dist/harness/utils/usage.js +33 -0
  303. package/dist/harness/utils/usage.js.map +1 -0
  304. package/dist/index.d.ts +4 -1
  305. package/dist/index.d.ts.map +1 -1
  306. package/dist/index.js +3 -6
  307. package/dist/index.js.map +1 -1
  308. package/dist/proxy.d.ts +2 -0
  309. package/dist/proxy.d.ts.map +1 -1
  310. package/dist/proxy.js +40 -10
  311. package/dist/proxy.js.map +1 -1
  312. package/dist/search/index.d.ts +24 -16
  313. package/dist/search/index.d.ts.map +1 -1
  314. package/dist/search/index.js +1 -1
  315. package/dist/search/index.js.map +1 -1
  316. package/dist/types.d.ts +5 -2
  317. package/dist/types.d.ts.map +1 -1
  318. package/dist/types.js.map +1 -1
  319. package/package.json +25 -4
  320. package/dist/harness/reducer.d.ts +0 -100
  321. package/dist/harness/reducer.d.ts.map +0 -1
  322. package/dist/harness/reducer.js +0 -415
  323. package/dist/harness/reducer.js.map +0 -1
  324. package/dist/harness/session/jsonl/errors.d.ts +0 -9
  325. package/dist/harness/session/jsonl/errors.d.ts.map +0 -1
  326. package/dist/harness/session/jsonl/errors.js +0 -19
  327. package/dist/harness/session/jsonl/errors.js.map +0 -1
  328. package/dist/harness/session/jsonl.d.ts +0 -3
  329. package/dist/harness/session/jsonl.d.ts.map +0 -1
  330. package/dist/harness/session/jsonl.js +0 -2
  331. package/dist/harness/session/jsonl.js.map +0 -1
  332. package/dist/harness/session/state.d.ts +0 -65
  333. package/dist/harness/session/state.d.ts.map +0 -1
  334. package/dist/harness/session/state.js +0 -319
  335. package/dist/harness/session/state.js.map +0 -1
  336. package/dist/harness/session/testing/conformance.d.ts +0 -4
  337. package/dist/harness/session/testing/conformance.d.ts.map +0 -1
  338. package/dist/harness/session/testing/conformance.js +0 -762
  339. package/dist/harness/session/testing/conformance.js.map +0 -1
  340. package/dist/search/scanning.d.ts +0 -29
  341. package/dist/search/scanning.d.ts.map +0 -1
  342. package/dist/search/scanning.js +0 -103
  343. package/dist/search/scanning.js.map +0 -1
@@ -0,0 +1,1575 @@
1
+ import { reduceAssistantMessageFrames, } from "@earendil-works/pi-ai";
2
+ import { prepareBranchEntries } from "../compaction/branch-summarization.js";
3
+ import { prepareCompaction } from "../compaction/compaction.js";
4
+ import { awaitWithContext } from "../context.js";
5
+ import { toolResultFromMessage } from "../execution/tools.js";
6
+ import { formatPromptTemplateInvocation } from "../prompt-templates.js";
7
+ import { Closed, HarnessClosed, HarnessFault, InvalidMessage, InvalidNavigation, LaneBusy, NoActiveOperation, NothingToCompact, NothingToResume, OperationMismatch, Result, UnknownSkill, UnknownTarget, UnknownTemplate, } from "../result.js";
8
+ import { insertEntry, insertUsage } from "../session/commit.js";
9
+ import { SessionInvariantError, SessionPendingAssistantMessageError } from "../session/session.js";
10
+ import { branchTip, deleteValue, laneConfig, laneState as laneStateValue, operationMeta as operationMetaValue, operationPreparation, operationResult as operationResultValue, operationState as operationStateValue, operationToolArgs, pendingEntry, pendingToolOutput, setValue, } from "../session/values.js";
11
+ import { formatSkillInvocation } from "../skills.js";
12
+ import { durableBranchPreparation, durableCompactionPreparation } from "./drive/structural.js";
13
+ import { driveOperation } from "./drive.js";
14
+ import { readAssistantFrames } from "./progress.js";
15
+ import { chainEntries, committedEntryEvents, readLaneQueues } from "./transcript.js";
16
+ import { Drive, } from "./types.js";
17
+ function isPromiseLike(value) {
18
+ if (value === null || (typeof value !== "object" && typeof value !== "function"))
19
+ return false;
20
+ return "then" in value && typeof value.then === "function";
21
+ }
22
+ function inboxItems(inbox, kind) {
23
+ return inbox.filter((item) => item.kind === kind);
24
+ }
25
+ function withoutInboxItems(inbox, removed) {
26
+ const removedIds = new Set(removed.map((item) => item.entryId));
27
+ return inbox.filter((item) => !removedIds.has(item.entryId));
28
+ }
29
+ function selectAcceptedInbox(inbox, steeringMode, followUpMode) {
30
+ let steerTaken = false;
31
+ let followUpTaken = false;
32
+ const selected = [];
33
+ const remainder = [];
34
+ for (const item of inbox) {
35
+ const eligible = item.kind === "write" ||
36
+ item.kind === "nextRun" ||
37
+ (item.kind === "steer" && (steeringMode === "all" || !steerTaken)) ||
38
+ (item.kind === "followUp" && (followUpMode === "all" || !followUpTaken));
39
+ if (eligible) {
40
+ selected.push(item);
41
+ if (item.kind === "steer")
42
+ steerTaken = true;
43
+ if (item.kind === "followUp")
44
+ followUpTaken = true;
45
+ }
46
+ else {
47
+ remainder.push(item);
48
+ }
49
+ }
50
+ return { selected, remainder };
51
+ }
52
+ function capturedSettings(config) {
53
+ return {
54
+ compaction: config.compaction,
55
+ steeringMode: config.steeringMode,
56
+ followUpMode: config.followUpMode,
57
+ toolExecution: config.toolExecution,
58
+ };
59
+ }
60
+ function durableLaneState(state, currentOperationId, inbox = state.inbox, lastOperationId = state.lastOperationId) {
61
+ return { currentOperationId, lastOperationId, inbox };
62
+ }
63
+ function pendingEntryWrite(entryId, pending) {
64
+ return pending.type === "message"
65
+ ? { id: entryId, parentId: null, type: "message", message: pending.payload }
66
+ : {
67
+ id: entryId,
68
+ parentId: null,
69
+ type: "custom",
70
+ customType: pending.customType,
71
+ ...(pending.payload === undefined ? {} : { data: pending.payload }),
72
+ };
73
+ }
74
+ function capturedModel(operation) {
75
+ const { state } = operation;
76
+ switch (state.at) {
77
+ case "assistant.ready":
78
+ case "assistant.effect_pending":
79
+ case "assistant.retry_wait":
80
+ return state.generationContext.configuration.model;
81
+ case "tools":
82
+ return state.batch.configuration.model;
83
+ case "deferred.suspended":
84
+ case "deferred.effect_pending":
85
+ return state.configuration.model;
86
+ case "summary.ready":
87
+ case "summary.effect_pending":
88
+ case "summary.retry_wait":
89
+ return state.summaryContext.configuration.model;
90
+ default:
91
+ return undefined;
92
+ }
93
+ }
94
+ /** Runtime implementation of one configured lane. */
95
+ export class Lane {
96
+ name;
97
+ session;
98
+ models;
99
+ hooks;
100
+ emitBatch;
101
+ onFault;
102
+ installWatch;
103
+ config;
104
+ stateChange;
105
+ resolveStateChange;
106
+ idleOwner;
107
+ /** Package-internal drive owner. Public only because deterministic procedure tests install exact owners directly. */
108
+ activeDrive;
109
+ /** Authoritative live control projection while this harness owns the Session. */
110
+ state;
111
+ closedError;
112
+ constructor(name, session, models, hooks, state, onFault, emitBatch, installWatch, readConfig) {
113
+ this.session = session;
114
+ this.models = models;
115
+ this.hooks = hooks;
116
+ this.name = name;
117
+ this.state = state;
118
+ this.onFault = onFault;
119
+ this.emitBatch = emitBatch;
120
+ this.installWatch = installWatch;
121
+ this.config = readConfig;
122
+ let resolveStateChange;
123
+ this.stateChange = new Promise((resolve) => {
124
+ resolveStateChange = resolve;
125
+ });
126
+ this.resolveStateChange = resolveStateChange;
127
+ }
128
+ async getTipId(_context) {
129
+ this.assertOpen();
130
+ return this.state.tipId;
131
+ }
132
+ async getResult(operationId, context) {
133
+ this.assertOpen();
134
+ return (await this.session.getValue(operationResultValue(operationId), context))?.value;
135
+ }
136
+ readConfig() {
137
+ return this.config();
138
+ }
139
+ mismatch(expected, currentOperationId, lastOperationId) {
140
+ return new OperationMismatch({
141
+ lane: this.name,
142
+ expectedOperationId: expected,
143
+ ...(currentOperationId === null ? {} : { currentOperationId }),
144
+ ...(lastOperationId === null ? {} : { lastOperationId }),
145
+ message: `Operation ${expected} does not own lane ${JSON.stringify(this.name)}`,
146
+ });
147
+ }
148
+ /**
149
+ * Run one effect-free command on this lane's serialized mutation line. Owned `state` is authoritative; the
150
+ * planner receives that state plus a read-only reader for bounded payload lookups. Committed values and owned
151
+ * state are immutable snapshots: update them by replacement, never in place. State-independent input validation
152
+ * belongs before `command()`, while every state-dependent decision belongs inside its planner.
153
+ *
154
+ * A planner may choose exactly one outcome:
155
+ * - `commit` commits once, publishes `next`, then synchronously materializes the caller result from
156
+ * storage-assigned `CommitResult` metadata;
157
+ * - `return` returns without a commit, boxed so a promise value is not awaited while holding the Session line;
158
+ * - `reject` rejects outside the mutation/fault boundary as an expected caller error without a commit.
159
+ *
160
+ * Planner, commit, and materialization errors fault the harness before releasing the Session line. Close/fault gates
161
+ * are checked both before queueing and when the callback starts: close-first rejects, while a callback admitted
162
+ * before close may finish its commit, publish memory, and resolve without another open check. Never invoke providers,
163
+ * tools, hooks, timers, event handlers, or wait for task completion here; perform those after `command()` returns.
164
+ */
165
+ async readLane(read, context) {
166
+ this.assertOpen();
167
+ try {
168
+ return await this.session.mutate(async (reader) => {
169
+ this.assertOpen();
170
+ try {
171
+ return await read(this.state, reader);
172
+ }
173
+ catch (error) {
174
+ if (this.closedError !== undefined)
175
+ throw this.closedError;
176
+ throw this.onFault(error, context);
177
+ }
178
+ }, context);
179
+ }
180
+ catch (error) {
181
+ if (this.closedError !== undefined)
182
+ throw this.closedError;
183
+ throw error;
184
+ }
185
+ }
186
+ async command(plan, context) {
187
+ this.assertOpen();
188
+ while (this.idleOwner !== undefined) {
189
+ await awaitWithContext(Promise.race([this.idleOwner, this.stateChange]), context);
190
+ this.assertOpen();
191
+ }
192
+ let outcome;
193
+ try {
194
+ outcome = await this.session.mutate(async (mutator) => {
195
+ this.assertOpen();
196
+ if (this.idleOwner !== undefined) {
197
+ return { kind: "idle_blocked", owner: this.idleOwner, change: this.stateChange };
198
+ }
199
+ try {
200
+ const decision = await plan(this.state, mutator);
201
+ switch (decision.kind) {
202
+ case "return":
203
+ return { kind: "return", result: decision.result };
204
+ case "reject":
205
+ return { kind: "reject", error: decision.error };
206
+ case "commit": {
207
+ const commit = await mutator.commit(decision.writes, context);
208
+ this.state = decision.next;
209
+ this.signalStateChange();
210
+ const result = decision.materialize(commit);
211
+ if (isPromiseLike(result)) {
212
+ throw new TypeError("Lane command materialize() must be synchronous");
213
+ }
214
+ const events = decision.events?.(commit) ?? [];
215
+ const delivery = events.length === 0 ? undefined : this.emitBatch(events, context);
216
+ return { kind: "return", result, ...(delivery === undefined ? {} : { delivery }) };
217
+ }
218
+ }
219
+ }
220
+ catch (error) {
221
+ if (this.closedError !== undefined)
222
+ throw this.closedError;
223
+ throw this.onFault(error, context);
224
+ }
225
+ }, context);
226
+ }
227
+ catch (error) {
228
+ if (this.closedError !== undefined)
229
+ throw this.closedError;
230
+ throw error;
231
+ }
232
+ if (outcome.kind === "idle_blocked") {
233
+ await awaitWithContext(Promise.race([outcome.owner, outcome.change]), context);
234
+ this.assertOpen();
235
+ return this.command(plan, context);
236
+ }
237
+ if (outcome.kind === "reject")
238
+ throw outcome.error;
239
+ await outcome.delivery;
240
+ return outcome.result;
241
+ }
242
+ /**
243
+ * Run a command against the current operation even after cancellation is requested. Use this to settle admitted
244
+ * effects, finish the operation, or update concurrent child state. The capability narrows the planner's state type;
245
+ * the Drive continuation remains the sole top-level state writer.
246
+ */
247
+ settleOperation(_capability, plan, context) {
248
+ return this.command(async (state, reader) => {
249
+ const operation = state.operation;
250
+ const decision = await plan(state, operation.state, operation.meta, reader);
251
+ if (decision.kind === "commit") {
252
+ return {
253
+ kind: "commit",
254
+ writes: [
255
+ ...decision.writes,
256
+ setValue(operationStateValue(operation.meta.operationId), decision.operationState),
257
+ ...(decision.lane?.inbox === undefined
258
+ ? []
259
+ : [
260
+ setValue(laneStateValue(this.name), durableLaneState(state, operation.meta.operationId, decision.lane.inbox)),
261
+ ]),
262
+ ],
263
+ next: {
264
+ ...state,
265
+ ...decision.lane,
266
+ operation: { meta: operation.meta, state: decision.operationState },
267
+ },
268
+ materialize: decision.materialize,
269
+ ...(decision.events === undefined ? {} : { events: decision.events }),
270
+ };
271
+ }
272
+ if (decision.kind !== "finish")
273
+ return decision;
274
+ const inbox = decision.lane?.inbox ?? state.inbox;
275
+ return {
276
+ kind: "commit",
277
+ writes: [
278
+ ...decision.writes,
279
+ setValue(operationResultValue(operation.meta.operationId), decision.record),
280
+ setValue(laneStateValue(this.name), durableLaneState(state, null, inbox, operation.meta.operationId)),
281
+ ],
282
+ next: {
283
+ ...state,
284
+ ...decision.lane,
285
+ inbox,
286
+ lastOperationId: operation.meta.operationId,
287
+ operation: null,
288
+ },
289
+ materialize: decision.materialize,
290
+ ...(decision.events === undefined ? {} : { events: decision.events }),
291
+ };
292
+ }, context);
293
+ }
294
+ /**
295
+ * Run an ordinary operation command only while durable control is running. Use this before starting new hooks,
296
+ * effects, or forward progress. Returns `cancel_requested` without invoking the planner once cancellation is requested.
297
+ */
298
+ continueOperation(capability, plan, context) {
299
+ return this.settleOperation(capability, async (state, latest, meta, reader) => {
300
+ if (latest.control.status === "cancel_requested") {
301
+ return { kind: "return", result: { kind: "cancel_requested" } };
302
+ }
303
+ const decision = await plan(state, latest, meta, reader);
304
+ if (decision.kind === "return") {
305
+ return { kind: "return", result: { kind: "result", value: decision.result } };
306
+ }
307
+ return {
308
+ ...decision,
309
+ materialize: (commit) => ({ kind: "result", value: decision.materialize(commit) }),
310
+ };
311
+ }, context);
312
+ }
313
+ async accept(request, context) {
314
+ if (this.closedError instanceof HarnessClosed) {
315
+ return Result.err(new Closed({ message: this.closedError.message }));
316
+ }
317
+ this.assertOpen();
318
+ const startedAt = Date.now();
319
+ const operationId = request.operationId ?? this.session.idGenerator.next(startedAt);
320
+ const acceptanceConfig = this.readConfig();
321
+ if (request.kind === "compaction") {
322
+ return this.acceptCompaction(request, operationId, startedAt, acceptanceConfig, context);
323
+ }
324
+ if (request.kind === "navigation") {
325
+ return this.acceptNavigation(request, operationId, startedAt, acceptanceConfig, context);
326
+ }
327
+ return this.acceptRun(request, operationId, startedAt, acceptanceConfig, context);
328
+ }
329
+ async acceptRun(request, operationId, startedAt, acceptanceConfig, context) {
330
+ let messages;
331
+ switch (request.kind) {
332
+ case "prompt":
333
+ if (typeof request.prompt !== "string") {
334
+ messages = Array.isArray(request.prompt) ? request.prompt : [request.prompt];
335
+ }
336
+ else {
337
+ const images = request.images ?? [];
338
+ messages =
339
+ request.prompt.length === 0 && images.length === 0
340
+ ? []
341
+ : [
342
+ {
343
+ role: "user",
344
+ content: [
345
+ ...(request.prompt.length === 0
346
+ ? []
347
+ : [{ type: "text", text: request.prompt }]),
348
+ ...images,
349
+ ],
350
+ timestamp: startedAt,
351
+ },
352
+ ];
353
+ }
354
+ break;
355
+ case "skill": {
356
+ const skill = acceptanceConfig.resources.skills?.find((candidate) => candidate.name === request.name);
357
+ if (skill === undefined) {
358
+ return Result.err(new UnknownSkill({ name: request.name, message: `Unknown skill: ${request.name}` }));
359
+ }
360
+ messages = [
361
+ {
362
+ role: "user",
363
+ content: [{ type: "text", text: formatSkillInvocation(skill, request.additionalInstructions) }],
364
+ timestamp: startedAt,
365
+ },
366
+ ];
367
+ break;
368
+ }
369
+ case "prompt_template": {
370
+ const template = acceptanceConfig.resources.promptTemplates?.find((candidate) => candidate.name === request.name);
371
+ if (template === undefined) {
372
+ return Result.err(new UnknownTemplate({ name: request.name, message: `Unknown prompt template: ${request.name}` }));
373
+ }
374
+ const content = formatPromptTemplateInvocation(template, request.args);
375
+ messages =
376
+ content.length === 0
377
+ ? []
378
+ : [{ role: "user", content: [{ type: "text", text: content }], timestamp: startedAt }];
379
+ break;
380
+ }
381
+ }
382
+ for (const message of messages) {
383
+ if (message.role === "assistant" && message.stopReason === "pending") {
384
+ return Result.err(new InvalidMessage({
385
+ lane: this.name,
386
+ reason: "pending_assistant",
387
+ message: "Cannot accept a pending assistant message",
388
+ }));
389
+ }
390
+ }
391
+ const prompt = messages.map((message) => ({ id: this.session.idGenerator.next(startedAt), message }));
392
+ return this.command(async (state, reader) => {
393
+ if (state.operation !== null) {
394
+ return {
395
+ kind: "return",
396
+ result: Result.err(new LaneBusy({
397
+ lane: this.name,
398
+ operationId: state.operation.meta.operationId,
399
+ operationKind: state.operation.meta.intent.kind,
400
+ message: `Lane ${JSON.stringify(this.name)} already has an active operation`,
401
+ })),
402
+ };
403
+ }
404
+ const { selected: selectedItems, remainder: inbox } = selectAcceptedInbox(state.inbox, acceptanceConfig.steeringMode, acceptanceConfig.followUpMode);
405
+ const captured = await Promise.all(selectedItems.map(async (item) => {
406
+ const stored = await reader.getValue(pendingEntry(item.entryId), context);
407
+ if (stored === undefined) {
408
+ throw new SessionInvariantError(`Pending ${item.kind} entry ${item.entryId} is missing its payload`);
409
+ }
410
+ if (item.kind !== "write" && stored.value.type !== "message") {
411
+ throw new SessionInvariantError(`Pending ${item.kind} entry ${item.entryId} is not a message`);
412
+ }
413
+ if (stored.value.type === "message" &&
414
+ stored.value.payload.role === "assistant" &&
415
+ stored.value.payload.stopReason === "pending") {
416
+ throw new SessionInvariantError(`Pending ${item.kind} entry ${item.entryId} contains a pending assistant`);
417
+ }
418
+ return { item, pending: stored.value };
419
+ }));
420
+ const hasCapturedConversation = selectedItems.some((item) => item.kind !== "write");
421
+ if (prompt.length === 0 && !hasCapturedConversation) {
422
+ return {
423
+ kind: "return",
424
+ result: Result.err(new InvalidMessage({
425
+ lane: this.name,
426
+ reason: "empty",
427
+ message: "Acceptance must append at least one message",
428
+ })),
429
+ };
430
+ }
431
+ const entries = chainEntries(state.tipId, [
432
+ ...captured.map(({ item, pending }) => pendingEntryWrite(item.entryId, pending)),
433
+ ...prompt.map(({ id, message }) => ({ id, parentId: null, type: "message", message })),
434
+ ]);
435
+ const parentId = entries[entries.length - 1].id;
436
+ const meta = {
437
+ operationId,
438
+ lane: this.name,
439
+ sourceTipId: state.tipId,
440
+ startedAt,
441
+ intent: { kind: "run", promptEntryIds: prompt.map(({ id }) => id) },
442
+ };
443
+ const operationState = {
444
+ at: "starting",
445
+ control: { status: "running" },
446
+ settings: capturedSettings(acceptanceConfig),
447
+ latestAssistantEntryId: null,
448
+ };
449
+ const remainingQueues = await readLaneQueues(reader, inbox, context);
450
+ const next = {
451
+ ...state,
452
+ tipId: parentId,
453
+ inbox,
454
+ operation: { meta, state: operationState },
455
+ };
456
+ return {
457
+ kind: "commit",
458
+ writes: [
459
+ ...entries.map((entry) => insertEntry(entry)),
460
+ ...selectedItems.map((item) => deleteValue(pendingEntry(item.entryId))),
461
+ setValue(branchTip(this.name), parentId),
462
+ setValue(operationMetaValue(operationId), meta),
463
+ setValue(operationStateValue(operationId), operationState),
464
+ setValue(laneStateValue(this.name), durableLaneState(state, operationId, inbox)),
465
+ ],
466
+ next,
467
+ materialize: () => Result.ok({ operationId, kind: "run", startedAt }),
468
+ events: (commit) => {
469
+ const events = [
470
+ { type: "run_start", runId: operationId, startedAt, lane: this.name },
471
+ ...committedEntryEvents(entries, commit, this.name, operationId),
472
+ ];
473
+ if (selectedItems.length > 0) {
474
+ events.push({ type: "queue_update", queues: remainingQueues, lane: this.name });
475
+ }
476
+ return events;
477
+ },
478
+ };
479
+ }, context);
480
+ }
481
+ acceptCompaction(request, operationId, startedAt, acceptanceConfig, context) {
482
+ const taskId = this.session.idGenerator.next(startedAt);
483
+ return this.command(async (state, reader) => {
484
+ if (state.operation !== null) {
485
+ return {
486
+ kind: "return",
487
+ result: Result.err(new LaneBusy({
488
+ lane: this.name,
489
+ operationId: state.operation.meta.operationId,
490
+ operationKind: state.operation.meta.intent.kind,
491
+ message: `Lane ${JSON.stringify(this.name)} already has an active operation`,
492
+ })),
493
+ };
494
+ }
495
+ const path = state.tipId === null
496
+ ? []
497
+ : (await reader.scanBranch({ start: state.tipId, stopAtType: "compaction", order: "newestFirst" }, context)).reverse();
498
+ const prepared = prepareCompaction(path, acceptanceConfig.compaction);
499
+ if (!prepared.ok)
500
+ throw prepared.error;
501
+ if (prepared.value === undefined) {
502
+ return {
503
+ kind: "return",
504
+ result: Result.err(new NothingToCompact({
505
+ lane: this.name,
506
+ message: `Lane ${JSON.stringify(this.name)} has nothing to compact`,
507
+ })),
508
+ };
509
+ }
510
+ const meta = {
511
+ operationId,
512
+ lane: this.name,
513
+ sourceTipId: state.tipId,
514
+ startedAt,
515
+ intent: {
516
+ kind: "compaction",
517
+ ...(request.customInstructions === undefined ? {} : { customInstructions: request.customInstructions }),
518
+ },
519
+ };
520
+ const operationState = {
521
+ at: "summary.deciding",
522
+ control: { status: "running" },
523
+ settings: capturedSettings(acceptanceConfig),
524
+ latestAssistantEntryId: null,
525
+ task: {
526
+ taskId,
527
+ reason: "manual",
528
+ ...(request.customInstructions === undefined ? {} : { customInstructions: request.customInstructions }),
529
+ boundary: { kind: "finish" },
530
+ },
531
+ };
532
+ return {
533
+ kind: "commit",
534
+ writes: [
535
+ setValue(operationPreparation(operationId, taskId), durableCompactionPreparation(prepared.value)),
536
+ setValue(operationMetaValue(operationId), meta),
537
+ setValue(operationStateValue(operationId), operationState),
538
+ setValue(laneStateValue(this.name), durableLaneState(state, operationId)),
539
+ ],
540
+ next: { ...state, operation: { meta, state: operationState } },
541
+ materialize: () => Result.ok({ operationId, kind: "compaction", startedAt }),
542
+ events: () => [
543
+ { type: "compaction_start", lane: this.name, runId: operationId, reason: "manual", startedAt },
544
+ ],
545
+ };
546
+ }, context);
547
+ }
548
+ async acceptNavigation(request, operationId, startedAt, acceptanceConfig, context) {
549
+ const taskId = this.session.idGenerator.next(startedAt);
550
+ const targetId = request.targetId;
551
+ const options = request.options ?? {};
552
+ const summarize = options.summarize ?? false;
553
+ for (;;) {
554
+ const observedTipId = this.state.tipId;
555
+ let preparation;
556
+ if (summarize && observedTipId !== null && targetId !== null) {
557
+ const target = await this.session.getEntries([targetId], context);
558
+ if (target.has(targetId)) {
559
+ const [oldPath, targetPath] = await Promise.all([
560
+ this.session.scanBranch({ start: observedTipId, order: "newestFirst" }, context),
561
+ this.session.scanBranch({ start: targetId, order: "newestFirst" }, context),
562
+ ]);
563
+ const oldIds = new Set(oldPath.map((entry) => entry.id));
564
+ const commonAncestorId = targetPath.find((entry) => oldIds.has(entry.id))?.id ?? null;
565
+ preparation = prepareBranchEntries(oldPath
566
+ .slice(0, commonAncestorId === null
567
+ ? oldPath.length
568
+ : oldPath.findIndex((entry) => entry.id === commonAncestorId))
569
+ .reverse());
570
+ }
571
+ }
572
+ const accepted = await this.command(async (state, reader) => {
573
+ if (state.operation !== null) {
574
+ return {
575
+ kind: "return",
576
+ result: Result.err(new LaneBusy({
577
+ lane: this.name,
578
+ operationId: state.operation.meta.operationId,
579
+ operationKind: state.operation.meta.intent.kind,
580
+ message: `Lane ${JSON.stringify(this.name)} already has an active operation`,
581
+ })),
582
+ };
583
+ }
584
+ if (state.tipId !== observedTipId)
585
+ return { kind: "return", result: undefined };
586
+ if (targetId === state.tipId) {
587
+ return {
588
+ kind: "return",
589
+ result: Result.err(new InvalidNavigation({
590
+ lane: this.name,
591
+ reason: "current_tip",
592
+ message: "Navigation target must differ from the current tip",
593
+ })),
594
+ };
595
+ }
596
+ if (targetId === null && options.label !== undefined) {
597
+ return {
598
+ kind: "return",
599
+ result: Result.err(new InvalidNavigation({
600
+ lane: this.name,
601
+ reason: "root_label",
602
+ message: "Root navigation cannot set a label",
603
+ })),
604
+ };
605
+ }
606
+ if (summarize && (state.tipId === null || targetId === null)) {
607
+ return {
608
+ kind: "return",
609
+ result: Result.err(new InvalidNavigation({
610
+ lane: this.name,
611
+ reason: state.tipId === null ? "source_root" : "target_root",
612
+ message: "Summarized navigation requires non-root source and target entries",
613
+ })),
614
+ };
615
+ }
616
+ if (targetId !== null && !(await reader.getEntries([targetId], context)).has(targetId)) {
617
+ return {
618
+ kind: "return",
619
+ result: Result.err(new UnknownTarget({ targetId, message: `Unknown target: ${targetId}` })),
620
+ };
621
+ }
622
+ const intent = {
623
+ kind: "navigation",
624
+ targetId,
625
+ summarize,
626
+ ...(options.label === undefined ? {} : { label: options.label }),
627
+ ...(options.customInstructions === undefined ? {} : { customInstructions: options.customInstructions }),
628
+ };
629
+ const meta = {
630
+ operationId,
631
+ lane: this.name,
632
+ sourceTipId: state.tipId,
633
+ startedAt,
634
+ intent,
635
+ };
636
+ const operationScope = {
637
+ control: { status: "running" },
638
+ settings: capturedSettings(acceptanceConfig),
639
+ latestAssistantEntryId: null,
640
+ };
641
+ let operationState;
642
+ const writes = [];
643
+ if (summarize) {
644
+ if (state.tipId === null || targetId === null || preparation === undefined) {
645
+ throw new SessionInvariantError("Validated summarized navigation is missing its preparation");
646
+ }
647
+ writes.push(setValue(operationPreparation(operationId, taskId), durableBranchPreparation(preparation)));
648
+ operationState = {
649
+ ...operationScope,
650
+ at: "summary.deciding",
651
+ task: {
652
+ taskId,
653
+ ...(options.customInstructions === undefined
654
+ ? {}
655
+ : { customInstructions: options.customInstructions }),
656
+ boundary: {
657
+ kind: "commit_navigation",
658
+ targetId,
659
+ ...(options.label === undefined ? {} : { label: options.label }),
660
+ },
661
+ },
662
+ };
663
+ }
664
+ else {
665
+ operationState = {
666
+ ...operationScope,
667
+ at: "navigation.ready_to_commit",
668
+ targetId,
669
+ ...(options.label === undefined ? {} : { label: options.label }),
670
+ };
671
+ }
672
+ writes.push(setValue(operationMetaValue(operationId), meta), setValue(operationStateValue(operationId), operationState), setValue(laneStateValue(this.name), durableLaneState(state, operationId)));
673
+ return {
674
+ kind: "commit",
675
+ writes,
676
+ next: { ...state, operation: { meta, state: operationState } },
677
+ materialize: () => Result.ok({ operationId, kind: "navigation", startedAt }),
678
+ events: () => [{ type: "navigation_start", lane: this.name, runId: operationId, targetId, startedAt }],
679
+ };
680
+ }, context);
681
+ if (accepted !== undefined)
682
+ return accepted;
683
+ }
684
+ }
685
+ async drive(options, context) {
686
+ if (this.closedError instanceof HarnessClosed) {
687
+ return Result.err(new Closed({ message: this.closedError.message }));
688
+ }
689
+ this.assertOpen();
690
+ for (;;) {
691
+ const claim = await this.command(async (state, reader) => {
692
+ const signal = context.abortSignal;
693
+ if (signal?.aborted) {
694
+ const reason = signal.reason;
695
+ return {
696
+ kind: "reject",
697
+ error: reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError"),
698
+ };
699
+ }
700
+ if (state.operation?.meta.operationId === options.operationId) {
701
+ if (this.activeDrive === undefined) {
702
+ const drive = new Drive(options, context);
703
+ this.activeDrive = drive;
704
+ this.signalStateChange();
705
+ return { kind: "return", result: { kind: "observe", drive, installed: true } };
706
+ }
707
+ return {
708
+ kind: "return",
709
+ result: this.activeDrive.operationId === options.operationId
710
+ ? { kind: "observe", drive: this.activeDrive, installed: false }
711
+ : { kind: "occupied", drive: this.activeDrive },
712
+ };
713
+ }
714
+ const stored = await reader.getValue(operationResultValue(options.operationId), context);
715
+ return stored === undefined
716
+ ? {
717
+ kind: "return",
718
+ result: {
719
+ kind: "mismatch",
720
+ error: this.mismatch(options.operationId, state.operation?.meta.operationId ?? null, state.lastOperationId),
721
+ },
722
+ }
723
+ : { kind: "return", result: { kind: "settled", outcome: stored.value } };
724
+ }, context);
725
+ if (claim.kind === "settled")
726
+ return Result.ok({ kind: "settled", outcome: claim.outcome });
727
+ if (claim.kind === "mismatch")
728
+ return Result.err(claim.error);
729
+ if (claim.kind === "occupied") {
730
+ await awaitWithContext(claim.drive.completion, context);
731
+ continue;
732
+ }
733
+ if (claim.installed) {
734
+ void driveOperation(this, claim.drive).then((outcome) => {
735
+ if (this.activeDrive === claim.drive) {
736
+ this.activeDrive = undefined;
737
+ this.signalStateChange();
738
+ }
739
+ claim.drive.settle(outcome);
740
+ }, (error) => {
741
+ let failure = this.closedError;
742
+ if (failure === undefined) {
743
+ try {
744
+ failure = this.onFault(error, claim.drive.context);
745
+ }
746
+ catch (faultError) {
747
+ failure = faultError;
748
+ }
749
+ }
750
+ if (this.activeDrive === claim.drive) {
751
+ this.activeDrive = undefined;
752
+ this.signalStateChange();
753
+ }
754
+ claim.drive.fail(failure);
755
+ });
756
+ }
757
+ return Result.ok(await awaitWithContext(claim.drive.completion, context));
758
+ }
759
+ }
760
+ /** Package-private durable cancellation primitive. Public exposure remains guarded until M8. */
761
+ async requestOperationAbort(operationId, context) {
762
+ if (this.closedError instanceof HarnessClosed) {
763
+ return Result.err(new Closed({ message: this.closedError.message }));
764
+ }
765
+ this.assertOpen();
766
+ const drive = this.activeDrive?.operationId === operationId ? this.activeDrive : undefined;
767
+ let resolveCancellation;
768
+ let rejectCancellation;
769
+ const cancellation = new Promise((resolve, reject) => {
770
+ resolveCancellation = resolve;
771
+ rejectCancellation = reject;
772
+ });
773
+ void cancellation.catch(() => { });
774
+ drive?.beginAbort(cancellation);
775
+ let gateSettled = false;
776
+ const settleGate = (signal) => {
777
+ if (gateSettled)
778
+ return;
779
+ gateSettled = true;
780
+ resolveCancellation();
781
+ if (signal)
782
+ drive?.signalAbort();
783
+ };
784
+ try {
785
+ const result = await this.command(async (state, reader) => {
786
+ const operation = state.operation;
787
+ if (operation?.meta.operationId !== operationId) {
788
+ return {
789
+ kind: "return",
790
+ result: Result.err(this.mismatch(operationId, operation?.meta.operationId ?? null, state.lastOperationId)),
791
+ };
792
+ }
793
+ if (operation.state.control.status === "cancel_requested") {
794
+ return {
795
+ kind: "return",
796
+ result: Result.ok({ operationId, newlyRequested: false, steer: [], followUp: [] }),
797
+ };
798
+ }
799
+ const removed = state.inbox.filter((item) => item.kind === "steer" || item.kind === "followUp");
800
+ const payloads = await Promise.all(removed.map(async (item) => {
801
+ const stored = await reader.getValue(pendingEntry(item.entryId), context);
802
+ if (stored?.value.type !== "message") {
803
+ throw new SessionInvariantError(`Pending ${item.kind} entry ${item.entryId} is missing its message`);
804
+ }
805
+ return { item, message: stored.value.payload };
806
+ }));
807
+ const steer = payloads.filter(({ item }) => item.kind === "steer").map(({ message }) => message);
808
+ const followUp = payloads.filter(({ item }) => item.kind === "followUp").map(({ message }) => message);
809
+ const removedIds = new Set(removed.map((item) => item.entryId));
810
+ const inbox = state.inbox.filter((item) => !removedIds.has(item.entryId));
811
+ const queues = await readLaneQueues(reader, inbox, context);
812
+ const operationState = {
813
+ ...operation.state,
814
+ control: { status: "cancel_requested", requestedAt: Date.now() },
815
+ };
816
+ return {
817
+ kind: "commit",
818
+ writes: [
819
+ ...removed.map((item) => deleteValue(pendingEntry(item.entryId))),
820
+ setValue(operationStateValue(operationId), operationState),
821
+ setValue(laneStateValue(this.name), durableLaneState(state, operationId, inbox)),
822
+ ],
823
+ next: { ...state, inbox, operation: { meta: operation.meta, state: operationState } },
824
+ materialize: () => {
825
+ settleGate(true);
826
+ return Result.ok({ operationId, newlyRequested: true, steer, followUp });
827
+ },
828
+ events: () => [
829
+ {
830
+ type: "operation_abort",
831
+ operationId,
832
+ steer,
833
+ followUp,
834
+ lane: this.name,
835
+ },
836
+ ...(removed.length === 0 ? [] : [{ type: "queue_update", queues, lane: this.name }]),
837
+ ],
838
+ };
839
+ }, context);
840
+ // A fresh Drive may observe an already-durable marker; pull its gate on the repeat path too. A mismatch
841
+ // can leave only the stale Drive's admission gate in aborting state; its cancellation wait is still released.
842
+ settleGate(result.ok && result.value.newlyRequested === false);
843
+ return result;
844
+ }
845
+ catch (error) {
846
+ if (!gateSettled)
847
+ rejectCancellation(error);
848
+ throw error;
849
+ }
850
+ }
851
+ requestAbort(operationId, context) {
852
+ return this.requestOperationAbort(operationId, context);
853
+ }
854
+ inspectExecution(context) {
855
+ return this.readLane((state) => {
856
+ const operation = state.operation;
857
+ const captured = operation === null ? undefined : capturedModel(operation);
858
+ const current = operation === null
859
+ ? null
860
+ : {
861
+ id: operation.meta.operationId,
862
+ kind: operation.meta.intent.kind,
863
+ status: operation.state.control.status === "cancel_requested"
864
+ ? "aborting"
865
+ : "open",
866
+ startedAt: operation.meta.startedAt,
867
+ ...(captured === undefined ? {} : { capturedModel: captured }),
868
+ };
869
+ return {
870
+ lane: this.name,
871
+ tipId: state.tipId,
872
+ configuredModel: state.configuration.model,
873
+ current,
874
+ lastOperationId: state.lastOperationId,
875
+ };
876
+ }, context);
877
+ }
878
+ async prompt(...args) {
879
+ if (args.length === 3) {
880
+ return this.driveRunRequest({ kind: "prompt", prompt: args[0], ...(args[1] === undefined ? {} : { images: args[1] }) }, args[2]);
881
+ }
882
+ return this.driveRunRequest({ kind: "prompt", prompt: args[0] }, args[1]);
883
+ }
884
+ skill(name, additionalInstructions, context) {
885
+ return this.driveRunRequest({ kind: "skill", name, ...(additionalInstructions === undefined ? {} : { additionalInstructions }) }, context);
886
+ }
887
+ promptFromTemplate(name, args, context) {
888
+ return this.driveRunRequest({ kind: "prompt_template", name, ...(args === undefined ? {} : { args }) }, context);
889
+ }
890
+ async driveRunRequest(request, context) {
891
+ const admission = await this.accept(request, context);
892
+ if (!admission.ok) {
893
+ switch (admission.error._tag) {
894
+ case "LaneBusy":
895
+ case "InvalidMessage":
896
+ case "UnknownSkill":
897
+ case "UnknownTemplate":
898
+ case "Closed":
899
+ return Result.err(admission.error);
900
+ default:
901
+ throw this.onFault(new SessionInvariantError(`Run acceptance returned ${admission.error._tag}`), context);
902
+ }
903
+ }
904
+ const driven = await this.drive({ operationId: admission.value.operationId, waitForRetry: true }, context);
905
+ if (!driven.ok) {
906
+ if (driven.error._tag === "Closed")
907
+ return Result.err(driven.error);
908
+ throw this.onFault(new SessionInvariantError(`Accepted run ${admission.value.operationId} no longer matches its lane`), context);
909
+ }
910
+ if (driven.value.kind === "settled")
911
+ return Result.ok(driven.value.outcome);
912
+ if (driven.value.reason === "deferred") {
913
+ return Result.ok({
914
+ operationId: admission.value.operationId,
915
+ status: "suspended",
916
+ deferred: driven.value.deferred,
917
+ });
918
+ }
919
+ throw this.onFault(new SessionInvariantError(`Run ${admission.value.operationId} returned an unwaited retry`), context);
920
+ }
921
+ async compact(options, context) {
922
+ const admission = await this.accept({
923
+ kind: "compaction",
924
+ ...(options?.customInstructions === undefined ? {} : { customInstructions: options.customInstructions }),
925
+ }, context);
926
+ if (!admission.ok) {
927
+ switch (admission.error._tag) {
928
+ case "LaneBusy":
929
+ case "NothingToCompact":
930
+ case "Closed":
931
+ return Result.err(admission.error);
932
+ default:
933
+ throw this.onFault(new SessionInvariantError(`Compaction acceptance returned ${admission.error._tag}`), context);
934
+ }
935
+ }
936
+ const compacted = await this.driveStructuralAdmission(admission.value, context);
937
+ if (!compacted.ok)
938
+ return compacted;
939
+ const continuation = await this.continueAfterStructural(compacted.value, context);
940
+ return continuation.ok
941
+ ? Result.ok({
942
+ compaction: compacted.value,
943
+ ...(continuation.value === undefined ? {} : { run: continuation.value }),
944
+ })
945
+ : continuation;
946
+ }
947
+ async navigateTree(targetId, options, context) {
948
+ const admission = await this.accept({ kind: "navigation", targetId, ...(options === undefined ? {} : { options }) }, context);
949
+ if (!admission.ok) {
950
+ switch (admission.error._tag) {
951
+ case "LaneBusy":
952
+ case "InvalidNavigation":
953
+ case "UnknownTarget":
954
+ case "Closed":
955
+ return Result.err(admission.error);
956
+ default:
957
+ throw this.onFault(new SessionInvariantError(`Navigation acceptance returned ${admission.error._tag}`), context);
958
+ }
959
+ }
960
+ const navigated = await this.driveStructuralAdmission(admission.value, context);
961
+ if (!navigated.ok)
962
+ return navigated;
963
+ const continuation = await this.continueAfterStructural(navigated.value, context);
964
+ return continuation.ok
965
+ ? Result.ok({
966
+ navigation: navigated.value,
967
+ ...(continuation.value === undefined ? {} : { run: continuation.value }),
968
+ })
969
+ : continuation;
970
+ }
971
+ async driveStructuralAdmission(admission, context) {
972
+ const driven = await this.drive({ operationId: admission.operationId, waitForRetry: true }, context);
973
+ if (!driven.ok) {
974
+ if (driven.error._tag === "Closed")
975
+ return Result.err(driven.error);
976
+ throw this.onFault(new SessionInvariantError(`Accepted ${admission.kind} ${admission.operationId} no longer matches its lane`), context);
977
+ }
978
+ if (driven.value.kind === "settled")
979
+ return Result.ok(driven.value.outcome);
980
+ throw this.onFault(new SessionInvariantError(`${admission.kind} ${admission.operationId} returned ${driven.value.reason}`), context);
981
+ }
982
+ async continueAfterStructural(record, context) {
983
+ if (record.status === "aborted")
984
+ return Result.ok(undefined);
985
+ const admission = await this.accept({ kind: "prompt", prompt: "" }, context);
986
+ if (!admission.ok) {
987
+ switch (admission.error._tag) {
988
+ case "InvalidMessage":
989
+ case "LaneBusy":
990
+ return Result.ok(undefined);
991
+ case "Closed":
992
+ return Result.err(admission.error);
993
+ default:
994
+ throw this.onFault(new SessionInvariantError(`Structural continuation acceptance returned ${admission.error._tag}`), context);
995
+ }
996
+ }
997
+ const driven = await this.drive({ operationId: admission.value.operationId, waitForRetry: true }, context);
998
+ if (!driven.ok) {
999
+ if (driven.error._tag === "Closed")
1000
+ return Result.err(driven.error);
1001
+ throw this.onFault(new SessionInvariantError(`Continuation run ${admission.value.operationId} no longer matches its lane`), context);
1002
+ }
1003
+ if (driven.value.kind === "settled")
1004
+ return Result.ok(driven.value.outcome);
1005
+ if (driven.value.reason === "deferred") {
1006
+ return Result.ok({
1007
+ operationId: admission.value.operationId,
1008
+ status: "suspended",
1009
+ deferred: driven.value.deferred,
1010
+ });
1011
+ }
1012
+ throw this.onFault(new SessionInvariantError(`Continuation run ${admission.value.operationId} returned an unwaited retry`), context);
1013
+ }
1014
+ async resume(context) {
1015
+ if (this.closedError instanceof HarnessClosed) {
1016
+ return Result.err(new Closed({ message: this.closedError.message }));
1017
+ }
1018
+ this.assertOpen();
1019
+ const inspected = await this.command((state) => {
1020
+ const operation = state.operation;
1021
+ return operation === null
1022
+ ? {
1023
+ kind: "return",
1024
+ result: Result.err(new NothingToResume({
1025
+ lane: this.name,
1026
+ message: `Lane ${JSON.stringify(this.name)} has no active operation to resume`,
1027
+ })),
1028
+ }
1029
+ : { kind: "return", result: Result.ok({ operationId: operation.meta.operationId }) };
1030
+ }, context);
1031
+ if (!inspected.ok)
1032
+ return inspected;
1033
+ const driven = await this.drive({ operationId: inspected.value.operationId, pollDeferred: true, waitForRetry: true }, context);
1034
+ if (!driven.ok) {
1035
+ if (driven.error._tag === "Closed")
1036
+ return Result.err(driven.error);
1037
+ throw this.onFault(new SessionInvariantError(`Operation ${inspected.value.operationId} no longer matches its lane`), context);
1038
+ }
1039
+ if (driven.value.kind === "settled")
1040
+ return Result.ok(driven.value.outcome);
1041
+ if (driven.value.reason === "deferred") {
1042
+ return Result.ok({
1043
+ operationId: inspected.value.operationId,
1044
+ status: "suspended",
1045
+ deferred: driven.value.deferred,
1046
+ });
1047
+ }
1048
+ throw this.onFault(new SessionInvariantError(`Operation ${inspected.value.operationId} returned an unwaited retry`), context);
1049
+ }
1050
+ async abort(context) {
1051
+ if (this.closedError instanceof HarnessClosed) {
1052
+ return Result.err(new Closed({ message: this.closedError.message }));
1053
+ }
1054
+ this.assertOpen();
1055
+ const operationId = await this.command((state) => ({
1056
+ kind: "return",
1057
+ result: state.operation?.meta.operationId,
1058
+ }), context);
1059
+ if (operationId === undefined) {
1060
+ return Result.err(new NoActiveOperation({
1061
+ lane: this.name,
1062
+ message: `Lane ${JSON.stringify(this.name)} has no active operation`,
1063
+ }));
1064
+ }
1065
+ const requested = await this.requestAbort(operationId, context);
1066
+ if (!requested.ok) {
1067
+ if (requested.error._tag === "Closed")
1068
+ return Result.err(requested.error);
1069
+ return Result.err(new NoActiveOperation({
1070
+ lane: this.name,
1071
+ message: `Lane ${JSON.stringify(this.name)} no longer has the inspected operation`,
1072
+ }));
1073
+ }
1074
+ const driven = await this.drive({ operationId }, context);
1075
+ if (!driven.ok && driven.error._tag === "Closed")
1076
+ return Result.err(driven.error);
1077
+ if (!driven.ok) {
1078
+ throw this.onFault(new SessionInvariantError(`Cancelled operation ${operationId} no longer matches its lane`), context);
1079
+ }
1080
+ return Result.ok({
1081
+ operationId,
1082
+ steer: requested.value.steer,
1083
+ followUp: requested.value.followUp,
1084
+ });
1085
+ }
1086
+ steer(message, images, context) {
1087
+ return this.enqueue("steer", message, images, context);
1088
+ }
1089
+ followUp(message, images, context) {
1090
+ return this.enqueue("followUp", message, images, context);
1091
+ }
1092
+ nextRun(message, images, context) {
1093
+ return this.enqueue("nextRun", message, images, context);
1094
+ }
1095
+ async enqueue(kind, input, images, context) {
1096
+ if (this.closedError instanceof HarnessClosed) {
1097
+ return Result.err(new Closed({ message: this.closedError.message }));
1098
+ }
1099
+ this.assertOpen();
1100
+ const at = Date.now();
1101
+ let message;
1102
+ if (typeof input === "string") {
1103
+ if (input.length === 0 && (images === undefined || images.length === 0)) {
1104
+ return Result.err(new InvalidMessage({
1105
+ lane: this.name,
1106
+ reason: "empty",
1107
+ message: "Queued input must contain text or an image",
1108
+ }));
1109
+ }
1110
+ message = {
1111
+ role: "user",
1112
+ content: [...(input.length === 0 ? [] : [{ type: "text", text: input }]), ...(images ?? [])],
1113
+ timestamp: at,
1114
+ };
1115
+ }
1116
+ else {
1117
+ if (input.role === "assistant" && input.stopReason === "pending") {
1118
+ return Result.err(new InvalidMessage({
1119
+ lane: this.name,
1120
+ reason: "pending_assistant",
1121
+ message: "Cannot queue a pending assistant message",
1122
+ }));
1123
+ }
1124
+ if (images !== undefined && images.length > 0 && input.role !== "user") {
1125
+ return Result.err(new InvalidMessage({
1126
+ lane: this.name,
1127
+ reason: "images_with_non_user",
1128
+ message: "Images can be added only to queued user messages",
1129
+ }));
1130
+ }
1131
+ message =
1132
+ images === undefined || images.length === 0 || input.role !== "user"
1133
+ ? structuredClone(input)
1134
+ : {
1135
+ ...structuredClone(input),
1136
+ content: [
1137
+ ...(typeof input.content === "string"
1138
+ ? input.content.length === 0
1139
+ ? []
1140
+ : [{ type: "text", text: input.content }]
1141
+ : input.content),
1142
+ ...images,
1143
+ ],
1144
+ };
1145
+ }
1146
+ const entryId = this.session.idGenerator.next(at);
1147
+ return this.command(async (state, reader) => {
1148
+ const inbox = [...state.inbox, { entryId, kind }];
1149
+ const queues = [
1150
+ ...(await readLaneQueues(reader, state.inbox, context)),
1151
+ { entryId, kind, type: "message", message },
1152
+ ];
1153
+ return {
1154
+ kind: "commit",
1155
+ writes: [
1156
+ setValue(pendingEntry(entryId), { type: "message", payload: message }),
1157
+ setValue(laneStateValue(this.name), durableLaneState(state, state.operation?.meta.operationId ?? null, inbox)),
1158
+ ],
1159
+ next: { ...state, inbox },
1160
+ materialize: () => Result.ok({ entryId }),
1161
+ events: () => [{ type: "queue_update", queues, lane: this.name }],
1162
+ };
1163
+ }, context);
1164
+ }
1165
+ async cancelQueued(entryId, context) {
1166
+ if (this.closedError instanceof HarnessClosed) {
1167
+ return Result.err(new Closed({ message: this.closedError.message }));
1168
+ }
1169
+ this.assertOpen();
1170
+ return this.command(async (state, reader) => {
1171
+ const queued = state.inbox.find((item) => item.entryId === entryId);
1172
+ if (queued === undefined) {
1173
+ const consumed = (await reader.getEntries([entryId], context)).has(entryId);
1174
+ return {
1175
+ kind: "return",
1176
+ result: Result.ok({ kind: consumed ? "already_consumed" : "not_found" }),
1177
+ };
1178
+ }
1179
+ if ((await reader.getValue(pendingEntry(entryId), context)) === undefined) {
1180
+ throw new SessionInvariantError(`Queued ${queued.kind} entry ${entryId} is missing its payload`);
1181
+ }
1182
+ const inbox = state.inbox.filter((item) => item.entryId !== entryId);
1183
+ const queues = await readLaneQueues(reader, inbox, context);
1184
+ return {
1185
+ kind: "commit",
1186
+ writes: [
1187
+ deleteValue(pendingEntry(entryId)),
1188
+ setValue(laneStateValue(this.name), durableLaneState(state, state.operation?.meta.operationId ?? null, inbox)),
1189
+ ],
1190
+ next: { ...state, inbox },
1191
+ materialize: () => Result.ok({ kind: "cancelled" }),
1192
+ events: () => [{ type: "queue_update", queues, lane: this.name }],
1193
+ };
1194
+ }, context);
1195
+ }
1196
+ async recordUsage(usage, options, context) {
1197
+ if (this.closedError instanceof HarnessClosed) {
1198
+ return Result.err(new Closed({ message: this.closedError.message }));
1199
+ }
1200
+ this.assertOpen();
1201
+ return this.command((state) => {
1202
+ const usageId = this.session.idGenerator.next();
1203
+ const row = {
1204
+ id: usageId,
1205
+ usage,
1206
+ adjustment: true,
1207
+ ...(options?.entryId === undefined ? {} : { entryId: options.entryId }),
1208
+ ...(options?.details === undefined ? {} : { details: options.details }),
1209
+ };
1210
+ return {
1211
+ kind: "commit",
1212
+ writes: [insertUsage(row)],
1213
+ next: state,
1214
+ materialize: () => Result.ok({ usageId }),
1215
+ events: (commit) => [
1216
+ {
1217
+ type: "usage",
1218
+ lane: this.name,
1219
+ row: { ...row, seq: commit.seqs[0] },
1220
+ totals: commit.stats.usage,
1221
+ },
1222
+ ],
1223
+ };
1224
+ }, context);
1225
+ }
1226
+ async waitForIdle(context) {
1227
+ for (;;) {
1228
+ const observation = await this.command((state) => ({
1229
+ kind: "return",
1230
+ result: state.operation === null && this.activeDrive === undefined
1231
+ ? { kind: "idle" }
1232
+ : { kind: "wait", drive: this.activeDrive, change: this.stateChange },
1233
+ }), context);
1234
+ if (observation.kind === "idle")
1235
+ return;
1236
+ await awaitWithContext(observation.drive === undefined ? observation.change : observation.drive.completion.then(() => undefined), context);
1237
+ }
1238
+ }
1239
+ async runWhenIdle(callback, context) {
1240
+ let owner;
1241
+ let releaseOwner;
1242
+ for (;;) {
1243
+ const observation = await this.command((state) => {
1244
+ if (state.operation !== null || this.activeDrive !== undefined) {
1245
+ return {
1246
+ kind: "return",
1247
+ result: { kind: "wait", drive: this.activeDrive, change: this.stateChange },
1248
+ };
1249
+ }
1250
+ let release;
1251
+ const claimed = new Promise((resolve) => {
1252
+ release = resolve;
1253
+ });
1254
+ owner = claimed;
1255
+ releaseOwner = release;
1256
+ this.idleOwner = claimed;
1257
+ this.signalStateChange();
1258
+ return { kind: "return", result: { kind: "claimed" } };
1259
+ }, context);
1260
+ if (observation.kind === "claimed")
1261
+ break;
1262
+ await awaitWithContext(observation.drive === undefined ? observation.change : observation.drive.completion.then(() => undefined), context);
1263
+ }
1264
+ if (owner === undefined || releaseOwner === undefined) {
1265
+ throw this.onFault(new SessionInvariantError("Idle callback claim was not published"), context);
1266
+ }
1267
+ try {
1268
+ this.assertOpen();
1269
+ await callback(context);
1270
+ }
1271
+ finally {
1272
+ if (this.idleOwner === owner)
1273
+ this.idleOwner = undefined;
1274
+ releaseOwner();
1275
+ this.signalStateChange();
1276
+ }
1277
+ }
1278
+ async getModel(_context) {
1279
+ this.assertOpen();
1280
+ return this.models.getModel(this.state.configuration.model.provider, this.state.configuration.model.modelId);
1281
+ }
1282
+ setModel(model, context) {
1283
+ return this.setConfiguration((configuration) => ({
1284
+ ...configuration,
1285
+ model: { ...model },
1286
+ }), (previous, value) => ({
1287
+ type: "config_update",
1288
+ property: "model",
1289
+ previous: previous.model,
1290
+ value: value.model,
1291
+ }), context);
1292
+ }
1293
+ async getThinkingLevel(_context) {
1294
+ this.assertOpen();
1295
+ return this.state.configuration.thinkingLevel;
1296
+ }
1297
+ setThinkingLevel(thinkingLevel, context) {
1298
+ return this.setConfiguration((configuration) => ({ ...configuration, thinkingLevel }), (previous, value) => ({
1299
+ type: "config_update",
1300
+ property: "thinkingLevel",
1301
+ previous: previous.thinkingLevel,
1302
+ value: value.thinkingLevel,
1303
+ }), context);
1304
+ }
1305
+ async getActiveTools(_context) {
1306
+ this.assertOpen();
1307
+ return this.state.configuration.activeToolNames;
1308
+ }
1309
+ setActiveTools(activeToolNames, context) {
1310
+ return this.setConfiguration((configuration) => ({ ...configuration, activeToolNames }), (previous, value) => ({
1311
+ type: "config_update",
1312
+ property: "activeTools",
1313
+ previous: previous.activeToolNames,
1314
+ value: value.activeToolNames,
1315
+ }), context);
1316
+ }
1317
+ watch(context) {
1318
+ return this.readLane(async (state, reader) => {
1319
+ const watcher = this.installWatch({}, (event) => event.type === "usage" || !("lane" in event) || event.lane === this.name, context, (resnapshotContext, markBoundary) => this.readLane(async (latest, latestReader) => {
1320
+ const snapshot = await this.captureLaneSnapshot(latest, latestReader, resnapshotContext);
1321
+ markBoundary();
1322
+ return snapshot;
1323
+ }, resnapshotContext));
1324
+ try {
1325
+ watcher.snapshot = await this.captureLaneSnapshot(state, reader, context);
1326
+ return watcher;
1327
+ }
1328
+ catch (error) {
1329
+ watcher.unsubscribe();
1330
+ throw error;
1331
+ }
1332
+ }, context);
1333
+ }
1334
+ async captureLaneSnapshot(state, reader, context) {
1335
+ const captured = structuredClone(state);
1336
+ const transcript = captured.tipId === null
1337
+ ? []
1338
+ : (await reader.scanBranch({ start: captured.tipId, stopAtType: "compaction", order: "newestFirst" }, context)).reverse();
1339
+ const queues = await readLaneQueues(reader, captured.inbox, context);
1340
+ let lastResult;
1341
+ if (captured.lastOperationId !== null) {
1342
+ const stored = await reader.getValue(operationResultValue(captured.lastOperationId), context);
1343
+ if (stored === undefined) {
1344
+ throw new SessionInvariantError(`Lane ${JSON.stringify(this.name)} is missing result ${captured.lastOperationId}`);
1345
+ }
1346
+ lastResult = stored.value;
1347
+ }
1348
+ const stats = await reader.getStats(context);
1349
+ const operation = captured.operation;
1350
+ let operationSnapshot = null;
1351
+ if (operation !== null) {
1352
+ const runningTools = [];
1353
+ let streamingMessage;
1354
+ let retry;
1355
+ let deferred;
1356
+ const readStreamingMessage = async (responseEntryId) => reduceAssistantMessageFrames(await readAssistantFrames(reader, operation.meta.operationId, responseEntryId, context));
1357
+ const state = operation.state;
1358
+ switch (state.at) {
1359
+ case "assistant.retry_wait":
1360
+ retry = {
1361
+ attempt: state.nextAttempt,
1362
+ maxAttempts: state.generationContext.retryPolicy.maxAttempts,
1363
+ nextAttemptAt: state.notBefore,
1364
+ };
1365
+ break;
1366
+ case "assistant.effect_pending":
1367
+ streamingMessage = await readStreamingMessage(state.responseEntryId);
1368
+ break;
1369
+ case "deferred.suspended":
1370
+ case "deferred.effect_pending": {
1371
+ const source = (await reader.getEntries([state.sourceEntryId], context)).get(state.sourceEntryId);
1372
+ if (source?.type !== "message" ||
1373
+ source.message.role !== "assistant" ||
1374
+ source.message.deferred === undefined) {
1375
+ throw new SessionInvariantError("Deferred source is missing its assistant handle");
1376
+ }
1377
+ deferred = { handle: source.message.deferred, poll: state.poll };
1378
+ if (state.at === "deferred.effect_pending") {
1379
+ streamingMessage = await readStreamingMessage(state.responseEntryId);
1380
+ }
1381
+ break;
1382
+ }
1383
+ case "tools": {
1384
+ const { batch } = state;
1385
+ const assistant = (await reader.getEntries([batch.assistantEntryId], context)).get(batch.assistantEntryId);
1386
+ if (assistant?.type !== "message" || assistant.message.role !== "assistant") {
1387
+ throw new SessionInvariantError("Tool batch assistant entry is invalid");
1388
+ }
1389
+ for (const call of batch.calls) {
1390
+ if (call.status === "planned" || call.status === "completed")
1391
+ continue;
1392
+ const block = assistant.message.content[call.sourceIndex];
1393
+ if (block?.type !== "toolCall") {
1394
+ throw new SessionInvariantError(`Tool call source index ${call.sourceIndex} does not name a tool-call block`);
1395
+ }
1396
+ const args = await reader.getValue(operationToolArgs(operation.meta.operationId, batch.turnId, call.sourceIndex), context);
1397
+ if (call.status === "effect_pending") {
1398
+ if (args === undefined) {
1399
+ throw new SessionInvariantError(`Tool call ${block.id} is missing persisted arguments`);
1400
+ }
1401
+ const checkpoint = await reader.getValue(pendingToolOutput(operation.meta.operationId, call.resultEntryId), context);
1402
+ runningTools.push({
1403
+ status: "running",
1404
+ toolCallId: block.id,
1405
+ toolName: block.name,
1406
+ args: args.value,
1407
+ ...(checkpoint === undefined ? {} : { result: checkpoint.value }),
1408
+ });
1409
+ continue;
1410
+ }
1411
+ const staged = await reader.getValue(pendingEntry(call.resultEntryId), context);
1412
+ if (staged?.value.type !== "message" || staged.value.payload.role !== "toolResult") {
1413
+ throw new SessionInvariantError(`Tool call ${call.resultEntryId} is missing its staged result`);
1414
+ }
1415
+ if (staged.value.payload.toolCallId !== block.id || staged.value.payload.toolName !== block.name) {
1416
+ throw new SessionInvariantError(`Tool call ${call.resultEntryId} has a mismatched staged result`);
1417
+ }
1418
+ runningTools.push({
1419
+ status: "settled",
1420
+ toolCallId: block.id,
1421
+ toolName: block.name,
1422
+ args: args?.value ?? block.arguments,
1423
+ result: toolResultFromMessage(staged.value.payload, call.terminate),
1424
+ isError: staged.value.payload.isError,
1425
+ });
1426
+ }
1427
+ break;
1428
+ }
1429
+ case "summary.retry_wait":
1430
+ retry = {
1431
+ attempt: state.nextAttempt,
1432
+ maxAttempts: state.summaryContext.retryPolicy.maxAttempts,
1433
+ nextAttemptAt: state.notBefore,
1434
+ };
1435
+ break;
1436
+ default:
1437
+ break;
1438
+ }
1439
+ operationSnapshot = {
1440
+ id: operation.meta.operationId,
1441
+ kind: operation.meta.intent.kind,
1442
+ startedAt: operation.meta.startedAt,
1443
+ fromTipId: operation.meta.sourceTipId,
1444
+ status: operation.state.control.status === "cancel_requested" ? "aborting" : "open",
1445
+ ...(retry === undefined ? {} : { retry }),
1446
+ ...(deferred === undefined ? {} : { deferred }),
1447
+ ...(streamingMessage === undefined ? {} : { streamingMessage }),
1448
+ runningTools,
1449
+ };
1450
+ }
1451
+ return structuredClone({
1452
+ lane: this.name,
1453
+ transcript,
1454
+ tipId: captured.tipId,
1455
+ ...(lastResult === undefined ? {} : { lastResult }),
1456
+ configuration: captured.configuration,
1457
+ stats,
1458
+ operation: operationSnapshot,
1459
+ queues,
1460
+ faulted: this.closedError instanceof HarnessFault,
1461
+ });
1462
+ }
1463
+ async setConfiguration(update, event, context) {
1464
+ await this.command((state) => {
1465
+ const configuration = update(state.configuration);
1466
+ return {
1467
+ kind: "commit",
1468
+ writes: [setValue(laneConfig(this.name), configuration)],
1469
+ next: { ...state, configuration },
1470
+ materialize: () => undefined,
1471
+ events: () => [{ ...event(state.configuration, configuration), lane: this.name }],
1472
+ };
1473
+ }, context);
1474
+ }
1475
+ async findEntries(query, context) {
1476
+ query ??= {};
1477
+ this.assertOpen();
1478
+ const start = query.start ?? this.state.tipId;
1479
+ return start === null
1480
+ ? []
1481
+ : this.session.scanBranch({ ...query, start, order: query.order ?? "newestFirst" }, context);
1482
+ }
1483
+ async findEntry(query, context) {
1484
+ query ??= {};
1485
+ return (await this.findEntries({ ...query, limit: query.limit === undefined ? 1 : Math.min(query.limit, 1) }, context))[0];
1486
+ }
1487
+ appendMessage(message, context) {
1488
+ return this.append({ type: "message", payload: message }, context);
1489
+ }
1490
+ appendCustomEntry(customType, data, context) {
1491
+ return this.append({ type: "custom", customType, ...(data === undefined ? {} : { payload: data }) }, context);
1492
+ }
1493
+ append(pending, context) {
1494
+ this.assertOpen();
1495
+ if (pending.type === "message" &&
1496
+ pending.payload.role === "assistant" &&
1497
+ pending.payload.stopReason === "pending") {
1498
+ return Promise.reject(new SessionPendingAssistantMessageError());
1499
+ }
1500
+ const id = this.session.idGenerator.next();
1501
+ return this.command(async (state, reader) => {
1502
+ if (state.operation === null) {
1503
+ const queued = inboxItems(state.inbox, "write");
1504
+ const captured = await Promise.all(queued.map(async (item) => {
1505
+ const stored = await reader.getValue(pendingEntry(item.entryId), context);
1506
+ if (stored === undefined) {
1507
+ throw new SessionInvariantError(`Pending write ${item.entryId} is missing its payload`);
1508
+ }
1509
+ return pendingEntryWrite(item.entryId, stored.value);
1510
+ }));
1511
+ const inbox = withoutInboxItems(state.inbox, queued);
1512
+ const queues = queued.length === 0 ? undefined : await readLaneQueues(reader, inbox, context);
1513
+ const entries = chainEntries(state.tipId, [...captured, pendingEntryWrite(id, pending)]);
1514
+ return {
1515
+ kind: "commit",
1516
+ writes: [
1517
+ ...entries.map((entry) => insertEntry(entry)),
1518
+ ...queued.map((item) => deleteValue(pendingEntry(item.entryId))),
1519
+ setValue(branchTip(this.name), id),
1520
+ setValue(laneStateValue(this.name), durableLaneState(state, null, inbox)),
1521
+ ],
1522
+ next: { ...state, tipId: id, inbox },
1523
+ materialize: () => id,
1524
+ events: (commit) => [
1525
+ ...committedEntryEvents(entries, commit, this.name),
1526
+ ...(queues === undefined ? [] : [{ type: "queue_update", queues, lane: this.name }]),
1527
+ ],
1528
+ };
1529
+ }
1530
+ const operation = state.operation;
1531
+ const inbox = [...state.inbox, { entryId: id, kind: "write" }];
1532
+ const queues = [
1533
+ ...(await readLaneQueues(reader, state.inbox, context)),
1534
+ pending.type === "message"
1535
+ ? { entryId: id, kind: "write", type: "message", message: pending.payload }
1536
+ : {
1537
+ entryId: id,
1538
+ kind: "write",
1539
+ type: "custom",
1540
+ customType: pending.customType,
1541
+ ...(pending.payload === undefined ? {} : { data: pending.payload }),
1542
+ },
1543
+ ];
1544
+ return {
1545
+ kind: "commit",
1546
+ writes: [
1547
+ setValue(pendingEntry(id), pending),
1548
+ setValue(laneStateValue(this.name), durableLaneState(state, operation.meta.operationId, inbox)),
1549
+ ],
1550
+ next: { ...state, inbox },
1551
+ materialize: () => id,
1552
+ events: () => [{ type: "queue_update", queues, lane: this.name }],
1553
+ };
1554
+ }, context);
1555
+ }
1556
+ seal(error) {
1557
+ this.closedError ??= error;
1558
+ this.activeDrive?.closeGate(error);
1559
+ this.signalStateChange();
1560
+ return this.idleOwner ?? Promise.resolve();
1561
+ }
1562
+ signalStateChange() {
1563
+ this.resolveStateChange();
1564
+ let resolveStateChange;
1565
+ this.stateChange = new Promise((resolve) => {
1566
+ resolveStateChange = resolve;
1567
+ });
1568
+ this.resolveStateChange = resolveStateChange;
1569
+ }
1570
+ assertOpen() {
1571
+ if (this.closedError !== undefined)
1572
+ throw this.closedError;
1573
+ }
1574
+ }
1575
+ //# sourceMappingURL=lane.js.map