@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,161 @@
1
+ /**
2
+ * The positions a retention decision is made against. Everything here is read
3
+ * from recorded facts: the completion order, each turn's R1 stamp, and the
4
+ * tip's R3 clock. Nothing reads config, kv, the catalog or the wall clock, so
5
+ * a restart re-projects byte-identically.
6
+ */
7
+ import { CompletionExpectation, owningTurn, SessionTurn } from "@inhuman.tools/harness-model";
8
+
9
+ import { LEGACY_POLICY, parsePolicy, type ResolvedPolicy } from "./resolve.js";
10
+ import type { Horizon } from "./schema.js";
11
+
12
+ export type ClockReason = "turn" | "every" | "freed" | "cold" | "compaction" | "hold";
13
+
14
+ export type RetentionClock = {
15
+ /** Uuid of the completion the deep horizons are evaluated against. */
16
+ readonly at: string;
17
+ readonly reason: ClockReason;
18
+ readonly coldByMs?: number;
19
+ readonly freedTokens?: number;
20
+ };
21
+
22
+ const REASONS: ReadonlySet<string> = new Set(["turn", "every", "freed", "cold", "compaction", "hold"]);
23
+
24
+ export function parseClock(json: string): RetentionClock | null {
25
+ if (json === "") return null;
26
+ try {
27
+ const raw = JSON.parse(json) as Record<string, unknown>;
28
+ if (typeof raw.at !== "string" || raw.at === "" || typeof raw.reason !== "string" || !REASONS.has(raw.reason)) {
29
+ return null;
30
+ }
31
+ return {
32
+ at: raw.at,
33
+ reason: raw.reason as ClockReason,
34
+ ...(typeof raw.coldByMs === "number" ? { coldByMs: raw.coldByMs } : {}),
35
+ ...(typeof raw.freedTokens === "number" ? { freedTokens: raw.freedTokens } : {}),
36
+ };
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /** A turn's policy: its own R1 stamp, else the nearest tail's, else the legacy rule set. */
43
+ export function policyOfTurn(turn: SessionTurn | null): ResolvedPolicy {
44
+ const seen = new Set<SessionTurn>();
45
+ for (let cell: unknown = turn; cell instanceof SessionTurn && !seen.has(cell); cell = cell.turnTail) {
46
+ seen.add(cell);
47
+ const parsed = parsePolicy(cell.retentionJson);
48
+ if (parsed !== null) return parsed;
49
+ }
50
+ return LEGACY_POLICY;
51
+ }
52
+
53
+ export type RetentionView = {
54
+ readonly tip: CompletionExpectation;
55
+ readonly order: readonly CompletionExpectation[];
56
+ readonly tipPos: number;
57
+ readonly tipTurn: number;
58
+ /** Null: no recorded clock, so deep horizons never expire anything. */
59
+ readonly clockPos: number | null;
60
+ readonly clockTurn: number | null;
61
+ readonly dialect: string | undefined;
62
+ posOf(c: CompletionExpectation): number;
63
+ turnOf(c: CompletionExpectation): number;
64
+ policyOf(c: CompletionExpectation): ResolvedPolicy;
65
+ /** The completion after `c`, which consumes `c`'s tool results. */
66
+ nextOf(c: CompletionExpectation): CompletionExpectation | null;
67
+ };
68
+
69
+ export type ViewOptions = {
70
+ /** Evaluate the deep horizons here instead of at the recorded clock. Used to estimate a step. */
71
+ readonly clockAt?: CompletionExpectation | null;
72
+ readonly dialect?: string;
73
+ };
74
+
75
+ /** `order` is the lead (kept zone) completions, then the spine, ending at the tip. */
76
+ export function buildView(
77
+ tip: CompletionExpectation,
78
+ order: readonly CompletionExpectation[],
79
+ options: ViewOptions = {},
80
+ ): RetentionView {
81
+ const pos = new Map<CompletionExpectation, number>();
82
+ const turnOrd = new Map<CompletionExpectation, number>();
83
+ const turns = new Map<SessionTurn | null, number>();
84
+ for (const [index, c] of order.entries()) {
85
+ pos.set(c, index);
86
+ const turn = owningTurn(c);
87
+ if (!turns.has(turn)) turns.set(turn, turns.size);
88
+ turnOrd.set(c, turns.get(turn) ?? 0);
89
+ }
90
+ const posOf = (c: CompletionExpectation): number => pos.get(c) ?? -1;
91
+ const turnOf = (c: CompletionExpectation): number => turnOrd.get(c) ?? 0;
92
+ let clockCompletion: CompletionExpectation | null;
93
+ if (options.clockAt !== undefined) clockCompletion = options.clockAt;
94
+ else {
95
+ const clock = parseClock(tip.retentionClockJson);
96
+ clockCompletion = clock === null ? null : (order.find((c) => c.uuid === clock.at) ?? null);
97
+ }
98
+ const clockPos = clockCompletion === null ? null : posOf(clockCompletion);
99
+ return {
100
+ tip,
101
+ order,
102
+ tipPos: posOf(tip),
103
+ tipTurn: turnOf(tip),
104
+ clockPos: clockPos === null || clockPos < 0 ? null : clockPos,
105
+ clockTurn: clockPos === null || clockPos < 0 || clockCompletion === null ? null : turnOf(clockCompletion),
106
+ dialect: options.dialect,
107
+ posOf,
108
+ turnOf,
109
+ policyOf: (c) => policyOfTurn(owningTurn(c)),
110
+ nextOf: (c) => order[posOf(c) + 1] ?? null,
111
+ };
112
+ }
113
+
114
+ export type ItemPlace = {
115
+ /** The completion whose input first contains the item. */
116
+ readonly consumer: CompletionExpectation;
117
+ /** The completion that produced or claimed the item; its turn is the item's turn. */
118
+ readonly owner: CompletionExpectation;
119
+ /** Evaluate against the tip instead of the clock. Pictures live only within their own turn. */
120
+ readonly againstTip?: boolean;
121
+ };
122
+
123
+ /**
124
+ * Any condition met expires the item. `newest` is decided by the caller,
125
+ * which ranks items of one kind; it is not a span.
126
+ */
127
+ export function horizonExpired(keep: readonly Horizon[], view: RetentionView, place: ItemPlace): boolean {
128
+ const consumerPos = view.posOf(place.consumer);
129
+ const ownerTurn = view.turnOf(place.owner);
130
+ if (consumerPos < 0) return false;
131
+ for (const h of keep) {
132
+ if (h === "round") {
133
+ if (view.tipPos - consumerPos >= 1) return true;
134
+ continue;
135
+ }
136
+ if (h === "turn") {
137
+ const ref = place.againstTip === true ? view.tipTurn : view.clockTurn;
138
+ if (ref !== null && ref > ownerTurn) return true;
139
+ continue;
140
+ }
141
+ if (h === "compaction" || h === "forever") continue;
142
+ if ("rounds" in h) {
143
+ const ref = h.rounds === 1 || place.againstTip === true ? view.tipPos : view.clockPos;
144
+ if (ref !== null && ref - consumerPos >= h.rounds) return true;
145
+ continue;
146
+ }
147
+ if ("turns" in h) {
148
+ const ref = place.againstTip === true ? view.tipTurn : view.clockTurn;
149
+ if (ref !== null && ref - ownerTurn >= h.turns) return true;
150
+ }
151
+ }
152
+ return false;
153
+ }
154
+
155
+ export function newestOf(keep: readonly Horizon[]): number | null {
156
+ let out: number | null = null;
157
+ for (const h of keep) {
158
+ if (typeof h === "object" && "newest" in h) out = out === null ? h.newest : Math.min(out, h.newest);
159
+ }
160
+ return out;
161
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Squeeze — tool-call observation paint. Not compact. Not settlement.
3
+ *
4
+ * Stored `outcomeJson` stays the source. This module projects it for the wire.
5
+ * Compact.scm does not inspect payload size.
6
+ *
7
+ * A reduced observation points at `context_recall`: the sealed receipt is read
8
+ * back from the graph, paged, instead of from a `/tmp` copy. There is no second
9
+ * world on disk. Regenerateable results (`read` | `net`) get no pointer:
10
+ * re-invoking is the address.
11
+ *
12
+ * Top-level strings stay raw text (a file body stays a file body). Serializer
13
+ * quoting of a top-level string is the wrong observation for read_file.
14
+ * Structured `value` payloads ride `toSExprString` caps — not a second compact
15
+ * pass in the driver.
16
+ *
17
+ * MCP image blocks never ride as base64 text: {@link liftToolImages} takes them
18
+ * out of the painted observation, and the projection decides whether the model
19
+ * sees the picture or a stub.
20
+ */
21
+ import { toSExprString, toSExprStringWithElisions } from "@here.build/arrival-serializer";
22
+ import { ProgrammaticJob, ToolCallJob, type ToolCallRepresentation } from "@inhuman.tools/harness-model";
23
+
24
+ import { humanSize, refOfNode } from "./retention/render.js";
25
+
26
+ export type SqueezeTo = "compact" | "micro";
27
+
28
+ /** Measured optimal observation size. Chars, not tokens. */
29
+ export const SQUEEZE_COMPACT_CHARS = 5000;
30
+
31
+ /** Below this, micro is a no-op — replacing a short answer with a path is pointless. */
32
+ export const SQUEEZE_MICRO_MIN_CHARS = 100;
33
+
34
+ export type SqueezeInput = {
35
+ readonly to: SqueezeTo;
36
+ readonly payload: unknown;
37
+ readonly persistable: boolean;
38
+ /** `job:<ref>` the model passes to `context_recall`. Without it a reduced result carries no pointer. */
39
+ readonly recallRef?: string;
40
+ readonly name: string;
41
+ };
42
+
43
+ /** Source observation as a string. Top-level strings stay raw; values ride the serializer. */
44
+ export function paintObservation(payload: unknown): string {
45
+ if (payload === undefined) return "";
46
+ if (typeof payload === "string") return payload;
47
+ return toSExprString(payload);
48
+ }
49
+
50
+ export function squeezeObservation(input: SqueezeInput): string {
51
+ const { to, payload, persistable, name } = input;
52
+ const pointer = persistable && input.recallRef !== undefined ? recallComment(input.recallRef) : "";
53
+
54
+ if (typeof payload === "string") {
55
+ if (to === "micro") {
56
+ if (payload.length < SQUEEZE_MICRO_MIN_CHARS) return payload;
57
+ if (!persistable) return clearedStub(name);
58
+ return pointer === "" ? compactBody(payload).text : pointer;
59
+ }
60
+ const { text, reduced } = compactBody(payload);
61
+ if (reduced && pointer !== "") return `${pointer}\n${text}`;
62
+ return text;
63
+ }
64
+
65
+ if (to === "micro") {
66
+ const full = paintObservation(payload);
67
+ if (full.length < SQUEEZE_MICRO_MIN_CHARS) return full;
68
+ if (!persistable) return clearedStub(name);
69
+ return pointer === "" ? squeezeValue(payload) : pointer;
70
+ }
71
+
72
+ const { text, reduced } = toSExprStringWithElisions(payload, {
73
+ maxTotalChars: SQUEEZE_COMPACT_CHARS,
74
+ maxStringChars: SQUEEZE_COMPACT_CHARS,
75
+ });
76
+ if (reduced && pointer !== "") return `${pointer}\n${text}`;
77
+ return text;
78
+ }
79
+
80
+ function squeezeValue(payload: unknown): string {
81
+ return toSExprString(payload, {
82
+ maxTotalChars: SQUEEZE_COMPACT_CHARS,
83
+ maxStringChars: SQUEEZE_COMPACT_CHARS,
84
+ });
85
+ }
86
+
87
+ function compactBody(payload: string): { text: string; reduced: boolean } {
88
+ if (payload.length <= SQUEEZE_COMPACT_CHARS) return { text: payload, reduced: false };
89
+ const dropped = payload.length - SQUEEZE_COMPACT_CHARS;
90
+ return {
91
+ text: `${payload.slice(0, SQUEEZE_COMPACT_CHARS)}…(+${dropped} chars)`,
92
+ reduced: true,
93
+ };
94
+ }
95
+
96
+ function recallComment(ref: string): string {
97
+ return `;;full response: context_recall ${JSON.stringify({ ref })}`;
98
+ }
99
+
100
+ function clearedStub(name: string): string {
101
+ return name === "" ? "[Old tool result content cleared]" : `[Old ${name} result content cleared]`;
102
+ }
103
+
104
+ const MAX_TOOL_IMAGE_BYTES = 5 * 1024 * 1024;
105
+
106
+ export type ToolImage = {
107
+ /** 1-based position among this result's images. */
108
+ readonly index: number;
109
+ readonly mime: string;
110
+ readonly size: number;
111
+ /** Null when the block was not valid base64 or exceeds the wire cap. */
112
+ readonly bytes: Uint8Array | null;
113
+ };
114
+
115
+ type ImageBlock = {
116
+ readonly type: "image";
117
+ readonly data?: unknown;
118
+ readonly mimeType?: unknown;
119
+ readonly mime_type?: unknown;
120
+ };
121
+
122
+ function isImageBlock(block: unknown): block is ImageBlock {
123
+ return typeof block === "object" && block !== null && (block as { type?: unknown }).type === "image";
124
+ }
125
+
126
+ function decodeBase64(data: unknown): Uint8Array | null {
127
+ if (typeof data !== "string") return null;
128
+ const compact = data.replaceAll(/\s/g, "");
129
+ if (compact === "" || compact.length % 4 !== 0 || !/^[\w+/-]+={0,2}$/.test(compact)) return null;
130
+ const decoded = Buffer.from(compact, "base64");
131
+ if (decoded.length === 0 || decoded.length > MAX_TOOL_IMAGE_BYTES) return null;
132
+ return new Uint8Array(decoded);
133
+ }
134
+
135
+ /** Dimensionless note: `[Image 1: image/png, 31.4 KB]`, or the elided form with a recall pointer. */
136
+ export function toolImageLine(image: ToolImage, state: "shown" | "elided", ref?: string): string {
137
+ const head = `Image ${image.index}${state === "elided" ? " elided" : ""}: ${image.mime}, ${humanSize(image.size)}`;
138
+ return state === "elided" && ref !== undefined ? `[${head} — recall ${ref}]` : `[${head}]`;
139
+ }
140
+
141
+ /**
142
+ * Pull image content blocks out of a result so base64 never reaches the model
143
+ * as text. The painted payload keeps a one-line note per image.
144
+ */
145
+ export function liftToolImages(
146
+ payload: unknown,
147
+ note: (image: ToolImage) => string = (image) => toolImageLine(image, "shown"),
148
+ ): { readonly payload: unknown; readonly images: readonly ToolImage[] } {
149
+ if (!Array.isArray(payload) || !payload.some((block) => isImageBlock(block))) return { payload, images: [] };
150
+ const images: ToolImage[] = [];
151
+ const lifted = payload.map((block: unknown) => {
152
+ if (!isImageBlock(block)) return block;
153
+ const mimeRaw = block.mimeType ?? block.mime_type;
154
+ const bytes = decodeBase64(block.data);
155
+ const size =
156
+ bytes === null ? (typeof block.data === "string" ? Math.floor((block.data.length * 3) / 4) : 0) : bytes.length;
157
+ const image: ToolImage = {
158
+ index: images.length + 1,
159
+ mime: typeof mimeRaw === "string" && mimeRaw !== "" ? mimeRaw : "image",
160
+ size,
161
+ bytes,
162
+ };
163
+ images.push(image);
164
+ return { type: "text", text: note(image) };
165
+ });
166
+ return { payload: lifted, images };
167
+ }
168
+
169
+ /** The images a sealed tool result carries, in order. */
170
+ export function toolImagesOf(tool: ToolCallJob): readonly ToolImage[] {
171
+ return liftToolImages(tool.observationPayload()).images;
172
+ }
173
+
174
+ function refField(node: { readonly uuid: string }): { readonly recallRef?: string } {
175
+ const ref = refOfNode(node);
176
+ return ref === undefined ? {} : { recallRef: ref };
177
+ }
178
+
179
+ /** Compact line derived from the sealed receipt. */
180
+ export function compactToolLine(job: ToolCallJob | ProgrammaticJob): string {
181
+ if (job instanceof ToolCallJob && job.regenerateable) return "";
182
+ if (job instanceof ToolCallJob) return squeezeToolCall(job, "compact");
183
+ const parts = job.resultParts();
184
+ if (parts !== undefined) return parts.map((part) => squeezeProgrammaticText(job, part.text)).join("\n");
185
+ const text = job.resultText();
186
+ return squeezeProgrammaticText(job, text !== "" ? text : (job.receiptJson ?? ""));
187
+ }
188
+
189
+ /** scheme_repl / exam results ride the same cap and pointer as tool results. */
190
+ export function squeezeProgrammaticText(job: ProgrammaticJob, text: string): string {
191
+ return squeezeObservation({
192
+ to: "compact",
193
+ payload: text,
194
+ persistable: true,
195
+ ...refField(job),
196
+ name: "",
197
+ });
198
+ }
199
+
200
+ /** Server-side compact wire paint. Not a client-document method. */
201
+ export function squeezeToolCall(tool: ToolCallJob, to: SqueezeTo, note?: (image: ToolImage) => string): string {
202
+ return squeezeObservation({
203
+ to,
204
+ payload: liftToolImages(tool.observationPayload(), note).payload,
205
+ persistable: !tool.regenerateable,
206
+ ...refField(tool),
207
+ name: tool.name,
208
+ });
209
+ }
210
+
211
+ export function toolCallRepresentation(tool: ToolCallJob): ToolCallRepresentation {
212
+ return {
213
+ id: tool.toolCallId,
214
+ name: tool.name,
215
+ args: tool.invocation(),
216
+ result: paintObservation(liftToolImages(tool.observationPayload()).payload),
217
+ };
218
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Completion tools[] face: frozen turn.tools when present, else live catalog.
3
+ * Vendor OpenAI wrapping stays in the actor. Live-catalog scheme_repl
4
+ * description overlay is also the actor — frozen ToolSpec text is left as-is.
5
+ */
6
+ import {
7
+ AGENT_RESPOND,
8
+ ApprovableJob,
9
+ CompletionExpectation,
10
+ owningTurn,
11
+ SessionRoot,
12
+ } from "@inhuman.tools/harness-model";
13
+ import invariant from "tiny-invariant";
14
+
15
+ import { catalogOf, KernelToolCatalog } from "../tool-catalog/index.js";
16
+
17
+ export type ContextTool = {
18
+ readonly name: string;
19
+ readonly description: string;
20
+ readonly parameters: Record<string, unknown>;
21
+ };
22
+
23
+ export function liveCatalogOf(
24
+ expectation: CompletionExpectation,
25
+ catalog?: KernelToolCatalog,
26
+ ): KernelToolCatalog | undefined {
27
+ return catalog ?? catalogOf(expectation.plexus?.root);
28
+ }
29
+
30
+ /** Frozen turn set wins so MCP churn cannot rewrite the prefix. */
31
+ export function selectCompletionTools(
32
+ expectation: CompletionExpectation,
33
+ catalog?: KernelToolCatalog,
34
+ ): readonly ContextTool[] {
35
+ const root = expectation.plexus?.root;
36
+ invariant(root instanceof SessionRoot, "selectCompletionTools: expectation must be wired on a SessionRoot");
37
+ if (expectation.completionMode === "isolated") return [];
38
+ if (expectation.parent instanceof ApprovableJob) return [];
39
+ const turn = owningTurn(expectation);
40
+ const live = liveCatalogOf(expectation, catalog);
41
+ const base =
42
+ turn != null && turn.tools.entries.length > 0
43
+ ? turn.tools.entries.map((entry) => {
44
+ const parameters = entry.parameters ?? {};
45
+ return {
46
+ name: entry.name,
47
+ description: entry.description,
48
+ parameters,
49
+ };
50
+ })
51
+ : live === undefined
52
+ ? []
53
+ : live
54
+ .catalogEntries()
55
+ .filter((entry) => entry.representation === "tool" || entry.representation === "both")
56
+ .map((entry) => ({
57
+ name: entry.wireName,
58
+ description: entry.description,
59
+ parameters: entry.parameters,
60
+ }));
61
+ const rest = base.filter((tool) => tool.name !== AGENT_RESPOND);
62
+ return receivesCalls(root) ? [...rest, AGENT_RESPOND_TOOL] : rest;
63
+ }
64
+
65
+ /**
66
+ * `agent__respond` is in tools[] for every round of a session that has taken a call, with
67
+ * one fixed schema and text. The open request ids are not in it: tools[] is the head of the
68
+ * cached prefix, and a tool that appears with the first open call, lists its ids and vanishes
69
+ * with the last would rewrite the whole prefix each time. The ids reach the model in the
70
+ * round's system note instead (`callbackRequiredNote`), after the cached part.
71
+ */
72
+ const AGENT_RESPOND_TOOL: ContextTool = {
73
+ name: AGENT_RESPOND,
74
+ description:
75
+ "Complete an open worker call. requestId must be one of the open ids listed in the system note. Not assistant text.",
76
+ parameters: {
77
+ type: "object",
78
+ properties: {
79
+ requestId: { type: "string", description: "Open call to settle" },
80
+ response: { type: "string", description: "Reply that settles the call" },
81
+ },
82
+ required: ["requestId", "response"],
83
+ additionalProperties: false,
84
+ },
85
+ };
86
+
87
+ /** The session was ever asked: a call is open now, or an inbound call turn is on the tail. */
88
+ function receivesCalls(root: SessionRoot): boolean {
89
+ if (root.openCallbackCount > 0) return true;
90
+ for (const turn of root.liveTurns) if (turn.inbound || turn.agentRequestId !== "") return true;
91
+ return false;
92
+ }