@inhuman.tools/turn 0.0.0-stage → 0.2.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 (346) hide show
  1. package/LICENSE.md +7 -0
  2. package/dist/agent-calls/acceptance.d.ts +12 -0
  3. package/dist/agent-calls/acceptance.d.ts.map +1 -0
  4. package/dist/agent-calls/acceptance.js +64 -0
  5. package/dist/agent-calls/acceptance.js.map +1 -0
  6. package/dist/agent-calls/callbacks.d.ts +14 -0
  7. package/dist/agent-calls/callbacks.d.ts.map +1 -0
  8. package/dist/agent-calls/callbacks.js +59 -0
  9. package/dist/agent-calls/callbacks.js.map +1 -0
  10. package/dist/agent-calls/delivery.d.ts +7 -0
  11. package/dist/agent-calls/delivery.d.ts.map +1 -0
  12. package/dist/agent-calls/delivery.js +16 -0
  13. package/dist/agent-calls/delivery.js.map +1 -0
  14. package/dist/agent-calls/index.d.ts +7 -0
  15. package/dist/agent-calls/index.d.ts.map +1 -0
  16. package/dist/agent-calls/index.js +6 -0
  17. package/dist/agent-calls/index.js.map +1 -0
  18. package/dist/agent-calls/operator.d.ts +21 -0
  19. package/dist/agent-calls/operator.d.ts.map +1 -0
  20. package/dist/agent-calls/operator.js +78 -0
  21. package/dist/agent-calls/operator.js.map +1 -0
  22. package/dist/agent-calls/runtime.d.ts +99 -0
  23. package/dist/agent-calls/runtime.d.ts.map +1 -0
  24. package/dist/agent-calls/runtime.js +245 -0
  25. package/dist/agent-calls/runtime.js.map +1 -0
  26. package/dist/agent-calls/types.d.ts +31 -0
  27. package/dist/agent-calls/types.d.ts.map +1 -0
  28. package/dist/agent-calls/types.js +2 -0
  29. package/dist/agent-calls/types.js.map +1 -0
  30. package/dist/context/compact-floors.d.ts +43 -0
  31. package/dist/context/compact-floors.d.ts.map +1 -0
  32. package/dist/context/compact-floors.js +52 -0
  33. package/dist/context/compact-floors.js.map +1 -0
  34. package/dist/context/compact.d.ts +42 -0
  35. package/dist/context/compact.d.ts.map +1 -0
  36. package/dist/context/compact.js +155 -0
  37. package/dist/context/compact.js.map +1 -0
  38. package/dist/context/delivery.d.ts +13 -0
  39. package/dist/context/delivery.d.ts.map +1 -0
  40. package/dist/context/delivery.js +6 -0
  41. package/dist/context/delivery.js.map +1 -0
  42. package/dist/context/history.d.ts +66 -0
  43. package/dist/context/history.d.ts.map +1 -0
  44. package/dist/context/history.js +573 -0
  45. package/dist/context/history.js.map +1 -0
  46. package/dist/context/index.d.ts +10 -0
  47. package/dist/context/index.d.ts.map +1 -0
  48. package/dist/context/index.js +10 -0
  49. package/dist/context/index.js.map +1 -0
  50. package/dist/context/observations.d.ts +7 -0
  51. package/dist/context/observations.d.ts.map +1 -0
  52. package/dist/context/observations.js +11 -0
  53. package/dist/context/observations.js.map +1 -0
  54. package/dist/context/retention/defaults.d.ts +10 -0
  55. package/dist/context/retention/defaults.d.ts.map +1 -0
  56. package/dist/context/retention/defaults.js +47 -0
  57. package/dist/context/retention/defaults.js.map +1 -0
  58. package/dist/context/retention/index.d.ts +14 -0
  59. package/dist/context/retention/index.d.ts.map +1 -0
  60. package/dist/context/retention/index.js +21 -0
  61. package/dist/context/retention/index.js.map +1 -0
  62. package/dist/context/retention/narrow.d.ts +22 -0
  63. package/dist/context/retention/narrow.d.ts.map +1 -0
  64. package/dist/context/retention/narrow.js +140 -0
  65. package/dist/context/retention/narrow.js.map +1 -0
  66. package/dist/context/retention/render.d.ts +37 -0
  67. package/dist/context/retention/render.d.ts.map +1 -0
  68. package/dist/context/retention/render.js +149 -0
  69. package/dist/context/retention/render.js.map +1 -0
  70. package/dist/context/retention/resolve.d.ts +51 -0
  71. package/dist/context/retention/resolve.d.ts.map +1 -0
  72. package/dist/context/retention/resolve.js +254 -0
  73. package/dist/context/retention/resolve.js.map +1 -0
  74. package/dist/context/retention/schema.d.ts +345 -0
  75. package/dist/context/retention/schema.d.ts.map +1 -0
  76. package/dist/context/retention/schema.js +149 -0
  77. package/dist/context/retention/schema.js.map +1 -0
  78. package/dist/context/retention/stamp.d.ts +25 -0
  79. package/dist/context/retention/stamp.d.ts.map +1 -0
  80. package/dist/context/retention/stamp.js +103 -0
  81. package/dist/context/retention/stamp.js.map +1 -0
  82. package/dist/context/retention/step.d.ts +33 -0
  83. package/dist/context/retention/step.d.ts.map +1 -0
  84. package/dist/context/retention/step.js +74 -0
  85. package/dist/context/retention/step.js.map +1 -0
  86. package/dist/context/retention/view.d.ts +57 -0
  87. package/dist/context/retention/view.d.ts.map +1 -0
  88. package/dist/context/retention/view.js +121 -0
  89. package/dist/context/retention/view.js.map +1 -0
  90. package/dist/context/squeeze.d.ts +45 -0
  91. package/dist/context/squeeze.d.ts.map +1 -0
  92. package/dist/context/squeeze.js +182 -0
  93. package/dist/context/squeeze.js.map +1 -0
  94. package/dist/context/tools.d.ts +16 -0
  95. package/dist/context/tools.d.ts.map +1 -0
  96. package/dist/context/tools.js +73 -0
  97. package/dist/context/tools.js.map +1 -0
  98. package/dist/context/transcript.d.ts +151 -0
  99. package/dist/context/transcript.d.ts.map +1 -0
  100. package/dist/context/transcript.js +659 -0
  101. package/dist/context/transcript.js.map +1 -0
  102. package/dist/policy-processor/bypass.d.ts +95 -0
  103. package/dist/policy-processor/bypass.d.ts.map +1 -0
  104. package/dist/policy-processor/bypass.js +188 -0
  105. package/dist/policy-processor/bypass.js.map +1 -0
  106. package/dist/policy-processor/classifier.d.ts +47 -0
  107. package/dist/policy-processor/classifier.d.ts.map +1 -0
  108. package/dist/policy-processor/classifier.js +671 -0
  109. package/dist/policy-processor/classifier.js.map +1 -0
  110. package/dist/policy-processor/cluster.d.ts +7 -0
  111. package/dist/policy-processor/cluster.d.ts.map +1 -0
  112. package/dist/policy-processor/cluster.js +32 -0
  113. package/dist/policy-processor/cluster.js.map +1 -0
  114. package/dist/policy-processor/evidence.d.ts +26 -0
  115. package/dist/policy-processor/evidence.d.ts.map +1 -0
  116. package/dist/policy-processor/evidence.js +204 -0
  117. package/dist/policy-processor/evidence.js.map +1 -0
  118. package/dist/policy-processor/explain.d.ts +59 -0
  119. package/dist/policy-processor/explain.d.ts.map +1 -0
  120. package/dist/policy-processor/explain.js +224 -0
  121. package/dist/policy-processor/explain.js.map +1 -0
  122. package/dist/policy-processor/index.d.ts +6 -0
  123. package/dist/policy-processor/index.d.ts.map +1 -0
  124. package/dist/policy-processor/index.js +6 -0
  125. package/dist/policy-processor/index.js.map +1 -0
  126. package/dist/policy-processor/license.d.ts +23 -0
  127. package/dist/policy-processor/license.d.ts.map +1 -0
  128. package/dist/policy-processor/license.js +47 -0
  129. package/dist/policy-processor/license.js.map +1 -0
  130. package/dist/policy-processor/peel-cwd.d.ts +2 -0
  131. package/dist/policy-processor/peel-cwd.d.ts.map +1 -0
  132. package/dist/policy-processor/peel-cwd.js +2 -0
  133. package/dist/policy-processor/peel-cwd.js.map +1 -0
  134. package/dist/policy-processor/real-path.d.ts +7 -0
  135. package/dist/policy-processor/real-path.d.ts.map +1 -0
  136. package/dist/policy-processor/real-path.js +42 -0
  137. package/dist/policy-processor/real-path.js.map +1 -0
  138. package/dist/policy-processor/scope.d.ts +111 -0
  139. package/dist/policy-processor/scope.d.ts.map +1 -0
  140. package/dist/policy-processor/scope.js +629 -0
  141. package/dist/policy-processor/scope.js.map +1 -0
  142. package/dist/policy-processor/self-protect.d.ts +10 -0
  143. package/dist/policy-processor/self-protect.d.ts.map +1 -0
  144. package/dist/policy-processor/self-protect.js +48 -0
  145. package/dist/policy-processor/self-protect.js.map +1 -0
  146. package/dist/policy-processor/soft-deny.d.ts +75 -0
  147. package/dist/policy-processor/soft-deny.d.ts.map +1 -0
  148. package/dist/policy-processor/soft-deny.js +185 -0
  149. package/dist/policy-processor/soft-deny.js.map +1 -0
  150. package/dist/policy-processor/unattended-guard.d.ts +28 -0
  151. package/dist/policy-processor/unattended-guard.d.ts.map +1 -0
  152. package/dist/policy-processor/unattended-guard.js +145 -0
  153. package/dist/policy-processor/unattended-guard.js.map +1 -0
  154. package/dist/session/cancel.d.ts +14 -0
  155. package/dist/session/cancel.d.ts.map +1 -0
  156. package/dist/session/cancel.js +32 -0
  157. package/dist/session/cancel.js.map +1 -0
  158. package/dist/session/commands.d.ts +41 -0
  159. package/dist/session/commands.d.ts.map +1 -0
  160. package/dist/session/commands.js +241 -0
  161. package/dist/session/commands.js.map +1 -0
  162. package/dist/session/compact-actor.d.ts +23 -0
  163. package/dist/session/compact-actor.d.ts.map +1 -0
  164. package/dist/session/compact-actor.js +91 -0
  165. package/dist/session/compact-actor.js.map +1 -0
  166. package/dist/session/compact-env.d.ts +54 -0
  167. package/dist/session/compact-env.d.ts.map +1 -0
  168. package/dist/session/compact-env.js +345 -0
  169. package/dist/session/compact-env.js.map +1 -0
  170. package/dist/session/compact.d.ts +42 -0
  171. package/dist/session/compact.d.ts.map +1 -0
  172. package/dist/session/compact.js +68 -0
  173. package/dist/session/compact.js.map +1 -0
  174. package/dist/session/index.d.ts +9 -0
  175. package/dist/session/index.d.ts.map +1 -0
  176. package/dist/session/index.js +8 -0
  177. package/dist/session/index.js.map +1 -0
  178. package/dist/session/park.d.ts +16 -0
  179. package/dist/session/park.d.ts.map +1 -0
  180. package/dist/session/park.js +73 -0
  181. package/dist/session/park.js.map +1 -0
  182. package/dist/session/side-channel-reminder.d.ts +41 -0
  183. package/dist/session/side-channel-reminder.d.ts.map +1 -0
  184. package/dist/session/side-channel-reminder.js +132 -0
  185. package/dist/session/side-channel-reminder.js.map +1 -0
  186. package/dist/session/types.d.ts +44 -0
  187. package/dist/session/types.d.ts.map +1 -0
  188. package/dist/session/types.js +2 -0
  189. package/dist/session/types.js.map +1 -0
  190. package/dist/tool-catalog/catalog.d.ts +77 -0
  191. package/dist/tool-catalog/catalog.d.ts.map +1 -0
  192. package/dist/tool-catalog/catalog.js +380 -0
  193. package/dist/tool-catalog/catalog.js.map +1 -0
  194. package/dist/tool-catalog/index.d.ts +4 -0
  195. package/dist/tool-catalog/index.d.ts.map +1 -0
  196. package/dist/tool-catalog/index.js +3 -0
  197. package/dist/tool-catalog/index.js.map +1 -0
  198. package/dist/tool-catalog/recipes.d.ts +36 -0
  199. package/dist/tool-catalog/recipes.d.ts.map +1 -0
  200. package/dist/tool-catalog/recipes.js +121 -0
  201. package/dist/tool-catalog/recipes.js.map +1 -0
  202. package/dist/tool-catalog/types.d.ts +103 -0
  203. package/dist/tool-catalog/types.d.ts.map +1 -0
  204. package/dist/tool-catalog/types.js +5 -0
  205. package/dist/tool-catalog/types.js.map +1 -0
  206. package/dist/tool-runtime/approval/approved.d.ts +13 -0
  207. package/dist/tool-runtime/approval/approved.d.ts.map +1 -0
  208. package/dist/tool-runtime/approval/approved.js +20 -0
  209. package/dist/tool-runtime/approval/approved.js.map +1 -0
  210. package/dist/tool-runtime/approval/dist.d.ts +6 -0
  211. package/dist/tool-runtime/approval/dist.d.ts.map +1 -0
  212. package/dist/tool-runtime/approval/dist.js +28 -0
  213. package/dist/tool-runtime/approval/dist.js.map +1 -0
  214. package/dist/tool-runtime/approval/inbound.d.ts +7 -0
  215. package/dist/tool-runtime/approval/inbound.d.ts.map +1 -0
  216. package/dist/tool-runtime/approval/inbound.js +48 -0
  217. package/dist/tool-runtime/approval/inbound.js.map +1 -0
  218. package/dist/tool-runtime/approval/unattended.d.ts +8 -0
  219. package/dist/tool-runtime/approval/unattended.d.ts.map +1 -0
  220. package/dist/tool-runtime/approval/unattended.js +23 -0
  221. package/dist/tool-runtime/approval/unattended.js.map +1 -0
  222. package/dist/tool-runtime/blob-port.d.ts +46 -0
  223. package/dist/tool-runtime/blob-port.d.ts.map +1 -0
  224. package/dist/tool-runtime/blob-port.js +47 -0
  225. package/dist/tool-runtime/blob-port.js.map +1 -0
  226. package/dist/tool-runtime/extra-root.d.ts +3 -0
  227. package/dist/tool-runtime/extra-root.d.ts.map +1 -0
  228. package/dist/tool-runtime/extra-root.js +14 -0
  229. package/dist/tool-runtime/extra-root.js.map +1 -0
  230. package/dist/tool-runtime/handler.d.ts +29 -0
  231. package/dist/tool-runtime/handler.d.ts.map +1 -0
  232. package/dist/tool-runtime/handler.js +460 -0
  233. package/dist/tool-runtime/handler.js.map +1 -0
  234. package/dist/tool-runtime/index.d.ts +14 -0
  235. package/dist/tool-runtime/index.d.ts.map +1 -0
  236. package/dist/tool-runtime/index.js +14 -0
  237. package/dist/tool-runtime/index.js.map +1 -0
  238. package/dist/tool-runtime/peel.d.ts +19 -0
  239. package/dist/tool-runtime/peel.d.ts.map +1 -0
  240. package/dist/tool-runtime/peel.js +74 -0
  241. package/dist/tool-runtime/peel.js.map +1 -0
  242. package/dist/tool-runtime/plugin-context.d.ts +13 -0
  243. package/dist/tool-runtime/plugin-context.d.ts.map +1 -0
  244. package/dist/tool-runtime/plugin-context.js +55 -0
  245. package/dist/tool-runtime/plugin-context.js.map +1 -0
  246. package/dist/tool-runtime/recall.d.ts +18 -0
  247. package/dist/tool-runtime/recall.d.ts.map +1 -0
  248. package/dist/tool-runtime/recall.js +100 -0
  249. package/dist/tool-runtime/recall.js.map +1 -0
  250. package/dist/tool-runtime/run-system-tool.d.ts +13 -0
  251. package/dist/tool-runtime/run-system-tool.d.ts.map +1 -0
  252. package/dist/tool-runtime/run-system-tool.js +230 -0
  253. package/dist/tool-runtime/run-system-tool.js.map +1 -0
  254. package/dist/tool-runtime/tool-error.d.ts +6 -0
  255. package/dist/tool-runtime/tool-error.d.ts.map +1 -0
  256. package/dist/tool-runtime/tool-error.js +6 -0
  257. package/dist/tool-runtime/tool-error.js.map +1 -0
  258. package/dist/tool-runtime/worker-tools.d.ts +8 -0
  259. package/dist/tool-runtime/worker-tools.d.ts.map +1 -0
  260. package/dist/tool-runtime/worker-tools.js +65 -0
  261. package/dist/tool-runtime/worker-tools.js.map +1 -0
  262. package/dist/tool-runtime/write-admit.d.ts +21 -0
  263. package/dist/tool-runtime/write-admit.d.ts.map +1 -0
  264. package/dist/tool-runtime/write-admit.js +89 -0
  265. package/dist/tool-runtime/write-admit.js.map +1 -0
  266. package/package.json +114 -3
  267. package/policy/auto-mode-ask.scm +201 -0
  268. package/policy/bypass-detect.prompt +55 -0
  269. package/policy/classifier-bash-incomplete.prompt +46 -0
  270. package/policy/classifier-bash.prompt +45 -0
  271. package/policy/classifier-mcp.prompt +45 -0
  272. package/policy/classifier-read.prompt +44 -0
  273. package/policy/classifier-write-outside.prompt +46 -0
  274. package/policy/classifier-write-risky.prompt +45 -0
  275. package/policy/classifier-write-workspace.prompt +46 -0
  276. package/policy/classifier-write.prompt +45 -0
  277. package/policy/soft-deny-round1.prompt +51 -0
  278. package/policy/soft-deny-round2.prompt +60 -0
  279. package/src/agent-calls/acceptance.ts +71 -0
  280. package/src/agent-calls/callbacks.ts +75 -0
  281. package/src/agent-calls/delivery.ts +15 -0
  282. package/src/agent-calls/index.ts +16 -0
  283. package/src/agent-calls/operator.ts +99 -0
  284. package/src/agent-calls/runtime.ts +310 -0
  285. package/src/agent-calls/types.ts +33 -0
  286. package/src/context/compact-floors.ts +76 -0
  287. package/src/context/compact.ts +176 -0
  288. package/src/context/delivery.ts +15 -0
  289. package/src/context/history.ts +725 -0
  290. package/src/context/index.ts +35 -0
  291. package/src/context/observations.ts +13 -0
  292. package/src/context/retention/defaults.ts +58 -0
  293. package/src/context/retention/index.ts +45 -0
  294. package/src/context/retention/narrow.ts +123 -0
  295. package/src/context/retention/render.ts +177 -0
  296. package/src/context/retention/resolve.ts +306 -0
  297. package/src/context/retention/schema.ts +173 -0
  298. package/src/context/retention/stamp.ts +121 -0
  299. package/src/context/retention/step.ts +85 -0
  300. package/src/context/retention/view.ts +161 -0
  301. package/src/context/squeeze.ts +218 -0
  302. package/src/context/tools.ts +92 -0
  303. package/src/context/transcript.ts +705 -0
  304. package/src/policy-processor/README.md +43 -0
  305. package/src/policy-processor/bypass.ts +246 -0
  306. package/src/policy-processor/classifier.ts +711 -0
  307. package/src/policy-processor/cluster.ts +31 -0
  308. package/src/policy-processor/evidence.ts +214 -0
  309. package/src/policy-processor/explain.ts +310 -0
  310. package/src/policy-processor/index.ts +6 -0
  311. package/src/policy-processor/license.ts +45 -0
  312. package/src/policy-processor/peel-cwd.ts +1 -0
  313. package/src/policy-processor/real-path.ts +41 -0
  314. package/src/policy-processor/scope.ts +655 -0
  315. package/src/policy-processor/self-protect.ts +46 -0
  316. package/src/policy-processor/soft-deny.ts +225 -0
  317. package/src/policy-processor/unattended-guard.ts +158 -0
  318. package/src/session/cancel.ts +34 -0
  319. package/src/session/commands.ts +292 -0
  320. package/src/session/compact-actor.ts +132 -0
  321. package/src/session/compact-env.ts +539 -0
  322. package/src/session/compact.ts +95 -0
  323. package/src/session/index.ts +18 -0
  324. package/src/session/park.ts +73 -0
  325. package/src/session/side-channel-reminder.ts +148 -0
  326. package/src/session/types.ts +50 -0
  327. package/src/tool-catalog/catalog.ts +282 -0
  328. package/src/tool-catalog/index.ts +26 -0
  329. package/src/tool-catalog/recipes.ts +154 -0
  330. package/src/tool-catalog/types.ts +110 -0
  331. package/src/tool-runtime/approval/approved.ts +20 -0
  332. package/src/tool-runtime/approval/dist.ts +29 -0
  333. package/src/tool-runtime/approval/inbound.ts +50 -0
  334. package/src/tool-runtime/approval/unattended.ts +24 -0
  335. package/src/tool-runtime/blob-port.ts +64 -0
  336. package/src/tool-runtime/extra-root.ts +16 -0
  337. package/src/tool-runtime/handler.ts +576 -0
  338. package/src/tool-runtime/index.ts +21 -0
  339. package/src/tool-runtime/peel.ts +91 -0
  340. package/src/tool-runtime/plugin-context.ts +68 -0
  341. package/src/tool-runtime/recall.ts +104 -0
  342. package/src/tool-runtime/run-system-tool.ts +273 -0
  343. package/src/tool-runtime/tool-error.ts +6 -0
  344. package/src/tool-runtime/worker-tools.ts +76 -0
  345. package/src/tool-runtime/write-admit.ts +90 -0
  346. package/README.md +0 -3
@@ -0,0 +1,725 @@
1
+ /**
2
+ * Completion input projection: history, compact lead, notifications, tool results.
3
+ * Vendor Chat Completions / Responses encoding stays in the protocol actor.
4
+ *
5
+ * Model input is a pure projection of the graph; nothing is deleted. What an
6
+ * earlier item leaves behind is decided by the recorded retention facts
7
+ * (R1 turn policy, R2 job rule, R3 clock) — see `./retention`.
8
+ */
9
+ import {
10
+ CallJob,
11
+ CompactJob,
12
+ CompletionExpectation,
13
+ type CompletionMessage,
14
+ ContextNeedToolJob,
15
+ latestCompactCutoff,
16
+ owningTurn,
17
+ ProgrammaticJob,
18
+ PromptMessage,
19
+ ReACTChunk,
20
+ SessionRoot,
21
+ type StoredText,
22
+ systemPartsOf,
23
+ tapeCompletion,
24
+ ToolCallJob,
25
+ type ToolTextPart,
26
+ TurnApprovalNoteChunk,
27
+ TurnNotificationChunk,
28
+ TurnPromptChunk,
29
+ TurnSteerChunk,
30
+ type UserVisualIdentity,
31
+ } from "@inhuman.tools/harness-model";
32
+ import invariant from "tiny-invariant";
33
+
34
+ import { usesGenericSilentProjection } from "./delivery.js";
35
+ import { BACKGROUND_TOOL, IN_FLIGHT_TOOL, NO_TOOLS_SYSTEM } from "./observations.js";
36
+ import { elideArgs, refOfNode, textRemainder, toolRemainder, visualRef, type StubFacts } from "./retention/render.js";
37
+ import { effectiveRule, kindRule } from "./retention/resolve.js";
38
+ import { parseJobStamp } from "./retention/stamp.js";
39
+ import { buildView, horizonExpired, newestOf, type RetentionView, type ViewOptions } from "./retention/view.js";
40
+ import {
41
+ liftToolImages,
42
+ paintObservation,
43
+ squeezeProgrammaticText,
44
+ squeezeToolCall,
45
+ toolImageLine,
46
+ type ToolImage,
47
+ } from "./squeeze.js";
48
+ import { liveCatalogOf, selectCompletionTools, type ContextTool } from "./tools.js";
49
+ import { compactLeadRows } from "./transcript.js";
50
+ import type { KernelToolCatalog } from "../tool-catalog/index.js";
51
+
52
+ export type HistoryToolCall = {
53
+ readonly id: string;
54
+ readonly name: string;
55
+ readonly argsJson: string;
56
+ };
57
+
58
+ export type HistoryUserPart =
59
+ | { readonly type: "text"; readonly text: string }
60
+ | {
61
+ readonly type: "image";
62
+ readonly id: string;
63
+ readonly mime: string;
64
+ readonly width: number;
65
+ readonly height: number;
66
+ readonly resizedFrom: { readonly width: number; readonly height: number } | null;
67
+ readonly sourcePath: string | null;
68
+ readonly bytes: Uint8Array | null;
69
+ };
70
+
71
+ export type HistoryMessage = {
72
+ readonly role: "system" | "user" | "assistant" | "tool";
73
+ readonly content: string | null | readonly ToolTextPart[] | readonly HistoryUserPart[];
74
+ readonly toolCallId?: string;
75
+ readonly toolCalls?: readonly HistoryToolCall[];
76
+ readonly thought?: string;
77
+ readonly thoughtEncrypted?: string;
78
+ };
79
+
80
+ export type ProjectHistoryOptions = {
81
+ readonly catalog?: KernelToolCatalog;
82
+ /** Emit an empty assistant shell when the dialect echoes empty thought. */
83
+ readonly echoEmptyThought?: boolean;
84
+ /** Bytes for a seated picture. Missing reader or missing blob omits that picture by name. */
85
+ readonly readVisual?: (id: string) => Uint8Array | null;
86
+ /**
87
+ * Bytes of a body stored beside the document, by content hash (an isolated completion's prompt message, a
88
+ * session round's stored line). A missing reader or a missing blob falls back to the head the document kept.
89
+ */
90
+ readonly readBlob?: (sha256: string) => Uint8Array | null;
91
+ /** Key into `contextRetention.dialects` (e.g. `anthropic`). Defaults from the tip's model id. */
92
+ readonly dialect?: string;
93
+ } & Pick<ViewOptions, "clockAt">;
94
+
95
+ /** Context shared by every completion emitted in one projection. */
96
+ type Projection = {
97
+ readonly catalog: KernelToolCatalog | undefined;
98
+ readonly echoEmptyThought: boolean;
99
+ readonly readVisual: ProjectHistoryOptions["readVisual"];
100
+ readonly readBlob: ProjectHistoryOptions["readBlob"];
101
+ readonly view: RetentionView;
102
+ readonly images: ImagePlan;
103
+ };
104
+
105
+ // --- stored bodies --------------------------------------------------------------------------
106
+
107
+ /** Keeps a leading byte-order mark: the text is what was stored, character for character. */
108
+ const UTF8 = new TextDecoder("utf-8", { ignoreBOM: true });
109
+
110
+ /**
111
+ * The text a stored body stands for: the inline text, the blob's bytes read as UTF-8, or the head the document
112
+ * kept when the blob cannot be read. The model sees a truncated prompt rather than a failed round; the daemon
113
+ * that binds the reader is the one that can say a blob is missing.
114
+ */
115
+ function bodyText(body: StoredText, readBlob: ProjectHistoryOptions["readBlob"]): string {
116
+ if (body.kind === "inline") return body.text;
117
+ const bytes = readBlob?.(body.blob.sha256) ?? null;
118
+ return bytes === null ? body.blob.head : UTF8.decode(bytes);
119
+ }
120
+
121
+ /**
122
+ * {@link CompletionExpectation.userPrompt} with every body read in full: the same rule (authored user messages
123
+ * win; with none, the stored line), but a body stored beside the document comes back through `readBlob`
124
+ * instead of as its head.
125
+ */
126
+ function userPromptOf(expectation: CompletionExpectation, readBlob: ProjectHistoryOptions["readBlob"]): string {
127
+ const users = expectation.promptMessages.filter((row) => row.role === "user");
128
+ if (users.length === 0) return bodyText(expectation.promptTextBody, readBlob);
129
+ return users.map((row) => bodyText(row.body, readBlob)).join("\n\n");
130
+ }
131
+
132
+ // --- prelude --------------------------------------------------------------------------------
133
+
134
+ function completionOfCell(cell: unknown): CompletionExpectation | null {
135
+ return tapeCompletion(cell) ?? (cell instanceof CompletionExpectation ? cell : null);
136
+ }
137
+
138
+ /**
139
+ * A round's prelude: the cells in its `messages` after the last completion
140
+ * ref. `messages` is cumulative (every earlier round's cells ride in it), so
141
+ * anything before that ref belongs to an earlier round and was emitted there.
142
+ */
143
+ export function preludeCells(expectation: CompletionExpectation): CompletionMessage[] {
144
+ const messages = expectation.messages;
145
+ let start = 0;
146
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
147
+ if (completionOfCell(messages[index]) != null) {
148
+ start = index + 1;
149
+ break;
150
+ }
151
+ }
152
+ return messages.slice(start);
153
+ }
154
+
155
+ export function notificationMessagesOf(expectation: CompletionExpectation): HistoryMessage[] {
156
+ const out: HistoryMessage[] = [];
157
+ for (const cell of preludeCells(expectation)) {
158
+ if (!(cell instanceof TurnNotificationChunk)) continue;
159
+ const content = cell.notificationText();
160
+ if (content !== "") out.push({ role: "system", content });
161
+ }
162
+ return out;
163
+ }
164
+
165
+ export function approvalNoteMessagesOf(expectation: CompletionExpectation): HistoryMessage[] {
166
+ const out: HistoryMessage[] = [];
167
+ for (const cell of preludeCells(expectation)) {
168
+ if (!(cell instanceof TurnApprovalNoteChunk)) continue;
169
+ const content = cell.approvalNoteText();
170
+ if (content !== "") out.push({ role: "user", content });
171
+ }
172
+ return out;
173
+ }
174
+
175
+ export function steerMessagesOf(expectation: CompletionExpectation): HistoryMessage[] {
176
+ const out: HistoryMessage[] = [];
177
+ for (const cell of preludeCells(expectation)) {
178
+ if (!(cell instanceof TurnSteerChunk)) continue;
179
+ const content = cell.completionMessage;
180
+ if (content !== "") out.push({ role: "user", content });
181
+ }
182
+ return out;
183
+ }
184
+
185
+ /** A prelude cell's text, or what it leaves once its horizon has passed. `null` leaves nothing. */
186
+ function retainedText(kind: string, text: string, completion: CompletionExpectation, ctx: Projection): string | null {
187
+ const rule = kindRule(ctx.view.policyOf(completion), kind);
188
+ if (text.length < rule.minChars) return text;
189
+ if (!horizonExpired(rule.keep, ctx.view, { consumer: completion, owner: completion })) return text;
190
+ return textRemainder(rule.leave, kind, text);
191
+ }
192
+
193
+ // --- tool results ---------------------------------------------------------------------------
194
+
195
+ type Round = {
196
+ readonly tools: Array<ToolCallJob | ProgrammaticJob>;
197
+ readonly suppressed: Set<string>;
198
+ readonly silent: Set<string>;
199
+ };
200
+
201
+ /** The tool jobs on the tape between `completion` and the next completion or cutoff. */
202
+ function roundOf(completion: CompletionExpectation, catalog: KernelToolCatalog | undefined): Round {
203
+ const turn = owningTurn(completion);
204
+ const tape = turn == null ? [] : [...turn.doneWork, ...turn.queue];
205
+ const start = tapeIndexOf(tape, completion);
206
+ const round: Round = { tools: [], suppressed: new Set(), silent: new Set() };
207
+ if (start === -1) return round;
208
+ for (let index = start + 1; index < tape.length; index += 1) {
209
+ const cell = tape[index];
210
+ if (tapeCompletion(cell) != null || cell instanceof CompactJob) break;
211
+ if (!(cell instanceof ToolCallJob || cell instanceof ProgrammaticJob)) continue;
212
+ if (cell instanceof ContextNeedToolJob) {
213
+ round.suppressed.add(cell.toolCallId);
214
+ continue;
215
+ }
216
+ if (cell instanceof CallJob && catalog != null && usesGenericSilentProjection(cell, catalog)) {
217
+ round.silent.add(cell.toolCallId);
218
+ }
219
+ round.tools.push(cell);
220
+ }
221
+ return round;
222
+ }
223
+
224
+ function sealedText(job: ToolCallJob | ProgrammaticJob): string {
225
+ if (job instanceof ToolCallJob) return paintObservation(liftToolImages(job.observationPayload()).payload);
226
+ const text = job.resultText();
227
+ return text !== "" ? text : (job.receiptJson ?? "");
228
+ }
229
+
230
+ function stubFacts(
231
+ job: ToolCallJob | ProgrammaticJob,
232
+ kind: string,
233
+ snap: { readonly ok: boolean; readonly value?: unknown; readonly code?: string } | undefined,
234
+ ): StubFacts {
235
+ return {
236
+ tool: job instanceof ToolCallJob ? job.name : job.toolName,
237
+ kind,
238
+ ref: refOfNode(job) ?? "job:?",
239
+ args: job.args,
240
+ full: sealedText(job),
241
+ ok: snap?.ok !== false,
242
+ value: job instanceof ToolCallJob ? job.observationPayload() : snap?.value,
243
+ code: snap?.code,
244
+ };
245
+ }
246
+
247
+ function toolContent(
248
+ job: ToolCallJob | ProgrammaticJob,
249
+ silentResult: boolean,
250
+ producer: CompletionExpectation,
251
+ ctx: Projection,
252
+ ): string | readonly ToolTextPart[] {
253
+ if (silentResult) return "";
254
+ if (!job.completed) return job.background ? BACKGROUND_TOOL : IN_FLIGHT_TOOL;
255
+ const stamp = parseJobStamp(job.retentionJson);
256
+ const consumer = ctx.view.nextOf(producer);
257
+ const snap = job.outcomeSnap;
258
+ const failed = snap?.ok === false;
259
+ if (stamp !== null && consumer !== null) {
260
+ const rule = effectiveRule(stamp.rule, failed);
261
+ const text = sealedText(job);
262
+ if (text.length >= rule.minChars && horizonExpired(rule.keep, ctx.view, { consumer, owner: producer })) {
263
+ return toolRemainder(rule.leave, stubFacts(job, stamp.kind, snap), failed);
264
+ }
265
+ }
266
+ if (job instanceof ToolCallJob) {
267
+ const visible = imageVisible(ctx.images, producer, job.toolCallId);
268
+ return squeezeToolCall(job, "compact", (image) =>
269
+ visible.has(image.index)
270
+ ? toolImageLine(image, "shown")
271
+ : toolImageLine(image, "elided", refOfNode(job) ?? "job:?"),
272
+ );
273
+ }
274
+ const parts = job.resultParts();
275
+ if (parts !== undefined)
276
+ return parts.length === 0 ? "" : parts.map((part) => ({ ...part, text: squeezeProgrammaticText(job, part.text) }));
277
+ const text = job.resultText();
278
+ if (text !== "") return squeezeProgrammaticText(job, text);
279
+ const receipt = job.receiptJson;
280
+ return receipt == null ? IN_FLIGHT_TOOL : squeezeProgrammaticText(job, receipt);
281
+ }
282
+
283
+ // --- pictures -------------------------------------------------------------------------------
284
+
285
+ function tapeIndexOf(tape: readonly unknown[], pew: CompletionExpectation): number {
286
+ for (let i = 0; i < tape.length; i += 1) {
287
+ const cell = tape[i];
288
+ if (cell === pew || tapeCompletion(cell) === pew) return i;
289
+ }
290
+ return -1;
291
+ }
292
+
293
+ function pictureLine(index: number, visual: UserVisualIdentity, omitted: boolean): string {
294
+ if (omitted) return `[Image #${index} omitted: ${visual.mime}, ${visual.width}x${visual.height}]`;
295
+ const resized =
296
+ visual.resizedFrom == null ? "" : `, resized from ${visual.resizedFrom.width}x${visual.resizedFrom.height}`;
297
+ const path = visual.sourcePath == null || visual.sourcePath === "" ? "" : `, ${visual.sourcePath}`;
298
+ return `[Image #${index}: ${visual.mime}, ${visual.width}x${visual.height}${resized}${path}]`;
299
+ }
300
+
301
+ function elidedPictureLine(index: number, visual: UserVisualIdentity): string {
302
+ const path = visual.sourcePath == null || visual.sourcePath === "" ? "" : `, ${visual.sourcePath}`;
303
+ return `[Image #${index} elided: ${visual.mime} ${visual.width}x${visual.height}${path} — recall ${visualRef(visual.id)}]`;
304
+ }
305
+
306
+ function picturedPrompt(expectation: CompletionExpectation): boolean {
307
+ const turn = owningTurn(expectation);
308
+ if (turn == null || turn.visuals.length === 0) return false;
309
+ const tape = [...turn.doneWork, ...turn.queue];
310
+ const index = tapeIndexOf(tape, expectation);
311
+ if (index < 0) return false;
312
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
313
+ const cell = tape[cursor];
314
+ if (cell instanceof TurnPromptChunk) return true;
315
+ if (cell instanceof ReACTChunk || tapeCompletion(cell) != null) return false;
316
+ }
317
+ return false;
318
+ }
319
+
320
+ /**
321
+ * Which pictures the model sees in this projection. Pictures live only within
322
+ * their own turn (`turn` evaluated against the tip, not the clock) and at most
323
+ * `newest` per kind survive, counted newest first across the whole projection.
324
+ */
325
+ type ImagePlan = {
326
+ /** `${completion uuid}` → visibility per user picture index. */
327
+ readonly user: ReadonlyMap<CompletionExpectation, readonly boolean[]>;
328
+ /** Producer completion → tool call id → visible 1-based image indexes. */
329
+ readonly tool: ReadonlyMap<CompletionExpectation, ReadonlyMap<string, ReadonlySet<number>>>;
330
+ };
331
+
332
+ type ImageItem =
333
+ | { readonly kind: "image.user"; readonly completion: CompletionExpectation; readonly index: number }
334
+ | {
335
+ readonly kind: "image.tool";
336
+ readonly completion: CompletionExpectation;
337
+ readonly callId: string;
338
+ readonly index: number;
339
+ };
340
+
341
+ function planImages(
342
+ view: RetentionView,
343
+ catalog: KernelToolCatalog | undefined,
344
+ readVisual: ProjectHistoryOptions["readVisual"],
345
+ ): ImagePlan {
346
+ const items: ImageItem[] = [];
347
+ for (const completion of view.order) {
348
+ if (completion === view.tip && !picturedPrompt(completion)) continue;
349
+ const turn = owningTurn(completion);
350
+ if (turn != null && picturedPrompt(completion)) {
351
+ for (let index = 0; index < turn.visuals.length; index += 1) {
352
+ items.push({ kind: "image.user", completion, index });
353
+ }
354
+ }
355
+ if (completion === view.tip) continue;
356
+ for (const job of roundOf(completion, catalog).tools) {
357
+ if (!(job instanceof ToolCallJob) || !job.completed) continue;
358
+ for (const image of liftToolImages(job.observationPayload()).images) {
359
+ if (image.bytes !== null) {
360
+ items.push({ kind: "image.tool", completion, callId: job.toolCallId, index: image.index });
361
+ }
362
+ }
363
+ }
364
+ }
365
+ const survivors = new Map<string, number>();
366
+ const user = new Map<CompletionExpectation, boolean[]>();
367
+ const tool = new Map<CompletionExpectation, Map<string, Set<number>>>();
368
+ for (let cursor = items.length - 1; cursor >= 0; cursor -= 1) {
369
+ const item = items[cursor]!;
370
+ const rule = kindRule(view.policyOf(item.completion), item.kind);
371
+ const consumer = item.kind === "image.user" ? item.completion : view.nextOf(item.completion);
372
+ let visible =
373
+ consumer === null || !horizonExpired(rule.keep, view, { consumer, owner: item.completion, againstTip: true });
374
+ if (visible && item.kind === "image.user" && readVisual == null) visible = true;
375
+ const cap = newestOf(rule.keep);
376
+ if (visible && cap !== null) {
377
+ const seen = survivors.get(item.kind) ?? 0;
378
+ survivors.set(item.kind, seen + 1);
379
+ if (seen >= cap) visible = false;
380
+ }
381
+ if (item.kind === "image.user") {
382
+ const flags = user.get(item.completion) ?? [];
383
+ flags[item.index] = visible;
384
+ user.set(item.completion, flags);
385
+ } else if (visible) {
386
+ const calls = tool.get(item.completion) ?? new Map<string, Set<number>>();
387
+ const set = calls.get(item.callId) ?? new Set<number>();
388
+ set.add(item.index);
389
+ calls.set(item.callId, set);
390
+ tool.set(item.completion, calls);
391
+ }
392
+ }
393
+ return { user, tool };
394
+ }
395
+
396
+ const NO_INDEXES: ReadonlySet<number> = new Set();
397
+
398
+ function imageVisible(plan: ImagePlan, producer: CompletionExpectation, callId: string): ReadonlySet<number> {
399
+ return plan.tool.get(producer)?.get(callId) ?? NO_INDEXES;
400
+ }
401
+
402
+ function userPictureParts(
403
+ text: string,
404
+ visuals: readonly UserVisualIdentity[],
405
+ visible: readonly boolean[],
406
+ readVisual: ProjectHistoryOptions["readVisual"],
407
+ ): readonly HistoryUserPart[] {
408
+ const parts: HistoryUserPart[] = [];
409
+ if (text !== "") parts.push({ type: "text", text });
410
+ for (let index = 0; index < visuals.length; index += 1) {
411
+ const visual = visuals[index];
412
+ if (visual == null) continue;
413
+ const number = index + 1;
414
+ if (visible[index] === false) {
415
+ parts.push({ type: "text", text: elidedPictureLine(number, visual) });
416
+ continue;
417
+ }
418
+ const bytes = readVisual == null ? null : readVisual(visual.id);
419
+ // One line per picture: the descriptive line when the bytes ride, the omitted line when they cannot.
420
+ if (bytes == null) {
421
+ parts.push({ type: "text", text: pictureLine(number, visual, true) });
422
+ continue;
423
+ }
424
+ parts.push(
425
+ { type: "text", text: pictureLine(number, visual, false) },
426
+ {
427
+ type: "image",
428
+ id: visual.id,
429
+ mime: visual.mime,
430
+ width: visual.width,
431
+ height: visual.height,
432
+ resizedFrom: visual.resizedFrom,
433
+ sourcePath: visual.sourcePath,
434
+ bytes,
435
+ },
436
+ );
437
+ }
438
+ return parts;
439
+ }
440
+
441
+ function toolImageRow(
442
+ job: ToolCallJob,
443
+ images: readonly ToolImage[],
444
+ visible: ReadonlySet<number>,
445
+ ): HistoryMessage | null {
446
+ const parts: HistoryUserPart[] = [];
447
+ for (const image of images) {
448
+ if (!visible.has(image.index) || image.bytes === null) continue;
449
+ parts.push({ type: "text", text: `[Image ${image.index} from ${job.name} ${refOfNode(job) ?? "job:?"}]` });
450
+ parts.push({
451
+ type: "image",
452
+ id: `${refOfNode(job) ?? "job:?"}:${image.index}`,
453
+ mime: image.mime,
454
+ width: 0,
455
+ height: 0,
456
+ resizedFrom: null,
457
+ sourcePath: null,
458
+ bytes: image.bytes,
459
+ });
460
+ }
461
+ return parts.length === 0 ? null : { role: "user", content: parts };
462
+ }
463
+
464
+ /** A `context_recall visual:<id>` result: the bytes ride in a user row (Chat tool rows are text-only). */
465
+ function recalledVisual(job: ToolCallJob, ctx: Projection): HistoryMessage | null {
466
+ if (job.name !== "context_recall" || ctx.readVisual == null) return null;
467
+ const json = job.outcomeSnap?.json;
468
+ const id = json != null && typeof json === "object" ? (json as { visualId?: unknown }).visualId : undefined;
469
+ if (typeof id !== "string") return null;
470
+ const bytes = ctx.readVisual(id);
471
+ if (bytes == null) return null;
472
+ const visual = owningTurnVisuals(ctx, id);
473
+ return {
474
+ role: "user",
475
+ content: [
476
+ { type: "text", text: `[Image recalled: ${visualRef(id)}]` },
477
+ {
478
+ type: "image",
479
+ id,
480
+ mime: visual?.mime ?? "image/png",
481
+ width: visual?.width ?? 0,
482
+ height: visual?.height ?? 0,
483
+ resizedFrom: visual?.resizedFrom ?? null,
484
+ sourcePath: visual?.sourcePath ?? null,
485
+ bytes,
486
+ },
487
+ ],
488
+ };
489
+ }
490
+
491
+ function owningTurnVisuals(ctx: Projection, id: string): UserVisualIdentity | undefined {
492
+ for (const c of ctx.view.order) {
493
+ const found = owningTurn(c)?.visuals.find((v) => v.id === id);
494
+ if (found !== undefined) return found;
495
+ }
496
+ return undefined;
497
+ }
498
+
499
+ // --- one completion -------------------------------------------------------------------------
500
+
501
+ function promptMessages(expectation: CompletionExpectation, ctx: Projection): HistoryMessage[] {
502
+ const messages: HistoryMessage[] = [];
503
+ const note = expectation.systemNote?.trim() ?? "";
504
+ if (note !== "") {
505
+ const kept = retainedText("system-note", note, expectation, ctx);
506
+ if (kept !== null) messages.push({ role: "system", content: kept });
507
+ }
508
+ const rules: Array<[kind: string, rows: HistoryMessage[]]> = [
509
+ ["notification", notificationMessagesOf(expectation)],
510
+ ["approval-note", approvalNoteMessagesOf(expectation)],
511
+ ["steer", steerMessagesOf(expectation)],
512
+ ];
513
+ for (const [kind, rows] of rules) {
514
+ for (const row of rows) {
515
+ const text = typeof row.content === "string" ? row.content : "";
516
+ const kept = retainedText(kind, text, expectation, ctx);
517
+ if (kept !== null) messages.push({ ...row, content: kept });
518
+ }
519
+ }
520
+ const text = userPromptOf(expectation, ctx.readBlob);
521
+ const turn = owningTurn(expectation);
522
+ const visuals = turn != null && picturedPrompt(expectation) ? turn.visuals : [];
523
+ if (text === "" && visuals.length === 0) return messages;
524
+ messages.push({
525
+ role: "user",
526
+ content:
527
+ visuals.length === 0
528
+ ? text
529
+ : userPictureParts(text, visuals, ctx.images.user.get(expectation) ?? [], ctx.readVisual),
530
+ });
531
+ return messages;
532
+ }
533
+
534
+ function historyToolCall(
535
+ call: {
536
+ readonly toolCallId: string;
537
+ readonly name: string;
538
+ readonly args: Readonly<Record<string, unknown>> | null;
539
+ },
540
+ job: ToolCallJob | ProgrammaticJob | undefined,
541
+ producer: CompletionExpectation,
542
+ ctx: Projection,
543
+ ): HistoryToolCall {
544
+ const base = { id: call.toolCallId, name: call.name };
545
+ const args = call.args ?? {};
546
+ const stamp = job === undefined ? null : parseJobStamp(job.retentionJson);
547
+ const consumer = ctx.view.nextOf(producer);
548
+ if (
549
+ job !== undefined &&
550
+ stamp?.args !== undefined &&
551
+ consumer !== null &&
552
+ horizonExpired(stamp.args.keep, ctx.view, { consumer, owner: producer })
553
+ ) {
554
+ const elided = elideArgs(args, `${refOfNode(job) ?? "job:?"}#args`, stamp.args.minChars);
555
+ if (elided.elided) return { ...base, argsJson: elided.json };
556
+ }
557
+ return { ...base, argsJson: JSON.stringify(args) };
558
+ }
559
+
560
+ function thinkingExpired(completion: CompletionExpectation, ctx: Projection): boolean {
561
+ const rule = kindRule(ctx.view.policyOf(completion), "thinking", ctx.view.dialect);
562
+ return horizonExpired(rule.keep, ctx.view, { consumer: completion, owner: completion });
563
+ }
564
+
565
+ function* emitCompletionOnTape(completion: CompletionExpectation, ctx: Projection): Generator<HistoryMessage> {
566
+ const round = roundOf(completion, ctx.catalog);
567
+ const prior = promptMessages(completion, ctx);
568
+ const byId = new Map(round.tools.filter((job) => job.toolCallId !== "").map((job) => [job.toolCallId, job]));
569
+ const stored = completion.responseCalls.map((call) =>
570
+ historyToolCall(
571
+ { toolCallId: call.toolCallId, name: call.name, args: call.args },
572
+ byId.get(call.toolCallId),
573
+ completion,
574
+ ctx,
575
+ ),
576
+ );
577
+ const calls = stored.filter((call) => !round.suppressed.has(call.id));
578
+ const allSuppressed = stored.length > 0 && calls.length === 0;
579
+ const dropThinking = thinkingExpired(completion, ctx);
580
+ const thought = dropThinking ? "" : completion.responseThought;
581
+ const encrypted = dropThinking ? "" : (completion.responseReasoningEncrypted?.trim() ?? "");
582
+ if (
583
+ completion.responseText !== "" ||
584
+ calls.length > 0 ||
585
+ thought !== "" ||
586
+ encrypted !== "" ||
587
+ allSuppressed ||
588
+ ctx.echoEmptyThought
589
+ ) {
590
+ prior.push({
591
+ role: "assistant",
592
+ // Strict OpenAI-compatible providers reject null content without tool calls.
593
+ content: completion.responseText === "" ? (calls.length > 0 ? null : "") : completion.responseText,
594
+ ...(calls.length > 0 ? { toolCalls: calls } : {}),
595
+ thought,
596
+ ...(encrypted === "" ? {} : { thoughtEncrypted: encrypted }),
597
+ });
598
+ }
599
+ // A tool row answers a call in this same projection, or it is not emitted: no orphan results.
600
+ const imageRows: HistoryMessage[] = [];
601
+ for (const call of calls) {
602
+ const job = byId.get(call.id);
603
+ prior.push({
604
+ role: "tool",
605
+ toolCallId: call.id,
606
+ content: job === undefined ? IN_FLIGHT_TOOL : toolContent(job, round.silent.has(call.id), completion, ctx),
607
+ });
608
+ if (job instanceof ToolCallJob && job.completed) {
609
+ const row = toolImageRow(
610
+ job,
611
+ liftToolImages(job.observationPayload()).images,
612
+ imageVisible(ctx.images, completion, call.id),
613
+ );
614
+ if (row !== null) imageRows.push(row);
615
+ const recalled = recalledVisual(job, ctx);
616
+ if (recalled !== null) imageRows.push(recalled);
617
+ }
618
+ }
619
+ prior.push(...imageRows);
620
+ for (const row of prior) yield row;
621
+ }
622
+
623
+ function dialectOf(tip: CompletionExpectation, given: string | undefined): string | undefined {
624
+ if (given !== undefined) return given;
625
+ return /claude|anthropic/i.test(tip.model) ? "anthropic" : undefined;
626
+ }
627
+
628
+ /** History before vendor wire encoding. */
629
+ export function* projectHistory(
630
+ tip: CompletionExpectation,
631
+ options: ProjectHistoryOptions = {},
632
+ ): Generator<HistoryMessage> {
633
+ if (tip.completionMode === "isolated") {
634
+ for (const message of tip.promptMessages) {
635
+ invariant(message instanceof PromptMessage, "invalid isolated prompt message");
636
+ yield {
637
+ role: message.role,
638
+ content: bodyText(message.body, options.readBlob),
639
+ ...(message.role === "assistant" ? { thought: "" } : {}),
640
+ };
641
+ }
642
+ return;
643
+ }
644
+ const catalog = liveCatalogOf(tip, options.catalog);
645
+ const tools: readonly ContextTool[] = selectCompletionTools(tip, catalog);
646
+ const echoEmptyThought = options.echoEmptyThought === true;
647
+ const tipTurn = owningTurn(tip);
648
+ const parts = tipTurn === null ? null : systemPartsOf(tipTurn);
649
+ // The addendum frozen with the turn's tool set wins over the live catalog: a catalog edit
650
+ // between rounds must not rewrite the cached prefix.
651
+ const frozenAddendum = tipTurn?.tools.addendum ?? "";
652
+ yield {
653
+ role: "system",
654
+ content: [
655
+ parts?.constitution ?? "",
656
+ tools.length === 0
657
+ ? NO_TOOLS_SYSTEM
658
+ : frozenAddendum !== ""
659
+ ? frozenAddendum
660
+ : (catalog?.TOOLS_SYSTEM_ADDENDUM ?? NO_TOOLS_SYSTEM),
661
+ ]
662
+ .filter((part) => part !== "")
663
+ .join("\n\n"),
664
+ };
665
+ const root = tip.plexus?.root;
666
+ const env = parts?.env ?? {};
667
+ const envLines: string[] = [];
668
+ if (root instanceof SessionRoot && root.path !== "") envLines.push(`Workspace: ${root.path}`);
669
+ for (const key of Object.keys(env).toSorted()) {
670
+ const value = env[key] ?? "";
671
+ if (value !== "") envLines.push(`${key}: ${value}`);
672
+ }
673
+ if (envLines.length > 0) yield { role: "system", content: envLines.join("\n") };
674
+ const note = tipTurn?.derivation?.note.trim() ?? "";
675
+ if (note !== "") yield { role: "system", content: note };
676
+ const cutoff = tipTurn == null ? null : latestCompactCutoff(tipTurn);
677
+ const leadRows = [];
678
+ if (cutoff != null) {
679
+ const kv = root instanceof SessionRoot ? root.kvs : {};
680
+ for (const row of compactLeadRows(cutoff.turn, cutoff.job, {
681
+ kv,
682
+ configFloor: cutoff.job.keepTurns ?? undefined,
683
+ })) {
684
+ leadRows.push(row);
685
+ }
686
+ }
687
+ const lead: CompletionExpectation[] = [];
688
+ for (const row of leadRows) {
689
+ if (row.kind === "user" || row.kind === "assistant") continue;
690
+ const kept = completionOfCell(row.cell);
691
+ if (kept != null && kept !== tip) lead.push(kept);
692
+ }
693
+ const view = buildView(tip, [...lead, ...tip.spine()], {
694
+ ...(options.clockAt === undefined ? {} : { clockAt: options.clockAt }),
695
+ dialect: dialectOf(tip, options.dialect),
696
+ });
697
+ const ctx: Projection = {
698
+ catalog,
699
+ echoEmptyThought,
700
+ readVisual: options.readVisual,
701
+ readBlob: options.readBlob,
702
+ view,
703
+ images: planImages(view, catalog, options.readVisual),
704
+ };
705
+ for (const row of leadRows) {
706
+ if (row.kind === "user") {
707
+ yield { role: "user", content: row.text };
708
+ continue;
709
+ }
710
+ if (row.kind === "assistant") {
711
+ yield { role: "assistant", content: row.text };
712
+ continue;
713
+ }
714
+ const kept = completionOfCell(row.cell);
715
+ if (kept == null || kept === tip) continue;
716
+ yield* emitCompletionOnTape(kept, ctx);
717
+ }
718
+ for (const completion of tip.spine()) {
719
+ if (completion === tip) {
720
+ yield* promptMessages(completion, ctx);
721
+ return;
722
+ }
723
+ yield* emitCompletionOnTape(completion, ctx);
724
+ }
725
+ }