@namzu/sdk 58.1.1 → 60.0.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 (312) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/account/binding.d.ts +20 -0
  3. package/dist/account/binding.d.ts.map +1 -0
  4. package/dist/account/binding.js +27 -0
  5. package/dist/account/binding.js.map +1 -0
  6. package/dist/account/index.d.ts +9 -0
  7. package/dist/account/index.d.ts.map +1 -0
  8. package/dist/account/index.js +5 -0
  9. package/dist/account/index.js.map +1 -0
  10. package/dist/account/lock.d.ts +33 -0
  11. package/dist/account/lock.d.ts.map +1 -0
  12. package/dist/account/lock.js +170 -0
  13. package/dist/account/lock.js.map +1 -0
  14. package/dist/account/registry.d.ts +21 -0
  15. package/dist/account/registry.d.ts.map +1 -0
  16. package/dist/account/registry.js +340 -0
  17. package/dist/account/registry.js.map +1 -0
  18. package/dist/account/types.d.ts +124 -0
  19. package/dist/account/types.d.ts.map +1 -0
  20. package/dist/account/types.js +35 -0
  21. package/dist/account/types.js.map +1 -0
  22. package/dist/compaction/context-window.d.ts +53 -0
  23. package/dist/compaction/context-window.d.ts.map +1 -1
  24. package/dist/compaction/context-window.js +141 -2
  25. package/dist/compaction/context-window.js.map +1 -1
  26. package/dist/execution/workflow/index.d.ts +3 -1
  27. package/dist/execution/workflow/index.d.ts.map +1 -1
  28. package/dist/execution/workflow/index.js +2 -1
  29. package/dist/execution/workflow/index.js.map +1 -1
  30. package/dist/execution/workflow/journal.d.ts +24 -4
  31. package/dist/execution/workflow/journal.d.ts.map +1 -1
  32. package/dist/execution/workflow/journal.js +131 -9
  33. package/dist/execution/workflow/journal.js.map +1 -1
  34. package/dist/execution/workflow/prelude.d.ts +2 -0
  35. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  36. package/dist/execution/workflow/prelude.js +49 -8
  37. package/dist/execution/workflow/prelude.js.map +1 -1
  38. package/dist/execution/workflow/resume.d.ts +59 -0
  39. package/dist/execution/workflow/resume.d.ts.map +1 -0
  40. package/dist/execution/workflow/resume.js +273 -0
  41. package/dist/execution/workflow/resume.js.map +1 -0
  42. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  43. package/dist/execution/workflow/runtime.js +112 -8
  44. package/dist/execution/workflow/runtime.js.map +1 -1
  45. package/dist/execution/workflow/types.d.ts +105 -1
  46. package/dist/execution/workflow/types.d.ts.map +1 -1
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/pals/communication/ingress-schema.d.ts +38 -38
  52. package/dist/peers/record.d.ts +98 -0
  53. package/dist/peers/record.d.ts.map +1 -1
  54. package/dist/peers/record.js +17 -0
  55. package/dist/peers/record.js.map +1 -1
  56. package/dist/peers/registry.d.ts.map +1 -1
  57. package/dist/peers/registry.js +2 -2
  58. package/dist/peers/registry.js.map +1 -1
  59. package/dist/plugin/compat.d.ts +24 -0
  60. package/dist/plugin/compat.d.ts.map +1 -0
  61. package/dist/plugin/compat.js +149 -0
  62. package/dist/plugin/compat.js.map +1 -0
  63. package/dist/plugin/loader.d.ts +2 -1
  64. package/dist/plugin/loader.d.ts.map +1 -1
  65. package/dist/plugin/loader.js +6 -0
  66. package/dist/plugin/loader.js.map +1 -1
  67. package/dist/pricing/index.d.ts.map +1 -1
  68. package/dist/pricing/index.js +2 -1
  69. package/dist/pricing/index.js.map +1 -1
  70. package/dist/process/kill-tree.d.ts +44 -0
  71. package/dist/process/kill-tree.d.ts.map +1 -1
  72. package/dist/process/kill-tree.js +102 -4
  73. package/dist/process/kill-tree.js.map +1 -1
  74. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  75. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  76. package/dist/prompt/coding-agent-doctrine.js +9 -1
  77. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  78. package/dist/provider/errors.d.ts.map +1 -1
  79. package/dist/provider/errors.js +25 -2
  80. package/dist/provider/errors.js.map +1 -1
  81. package/dist/provider/model-window-suffix.d.ts +18 -0
  82. package/dist/provider/model-window-suffix.d.ts.map +1 -0
  83. package/dist/provider/model-window-suffix.js +77 -0
  84. package/dist/provider/model-window-suffix.js.map +1 -0
  85. package/dist/provider/token-budget.d.ts.map +1 -1
  86. package/dist/provider/token-budget.js +9 -1
  87. package/dist/provider/token-budget.js.map +1 -1
  88. package/dist/public-runtime.d.ts +6 -3
  89. package/dist/public-runtime.d.ts.map +1 -1
  90. package/dist/public-runtime.js +6 -4
  91. package/dist/public-runtime.js.map +1 -1
  92. package/dist/public-types.d.ts +3 -0
  93. package/dist/public-types.d.ts.map +1 -1
  94. package/dist/registry/command/kernel-commands.d.ts +1 -4
  95. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  96. package/dist/registry/command/kernel-commands.js +0 -28
  97. package/dist/registry/command/kernel-commands.js.map +1 -1
  98. package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
  99. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
  100. package/dist/runtime/jobs/awaited-jobs.js +11 -0
  101. package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
  102. package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
  103. package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
  104. package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
  105. package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
  106. package/dist/runtime/jobs/liveness.d.ts +67 -0
  107. package/dist/runtime/jobs/liveness.d.ts.map +1 -0
  108. package/dist/runtime/jobs/liveness.js +94 -0
  109. package/dist/runtime/jobs/liveness.js.map +1 -0
  110. package/dist/runtime/jobs/registry.d.ts +21 -0
  111. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  112. package/dist/runtime/jobs/registry.js +104 -2
  113. package/dist/runtime/jobs/registry.js.map +1 -1
  114. package/dist/runtime/jobs/wake-budget.d.ts +35 -0
  115. package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
  116. package/dist/runtime/jobs/wake-budget.js +48 -0
  117. package/dist/runtime/jobs/wake-budget.js.map +1 -0
  118. package/dist/runtime/query/detached-calls.d.ts +65 -0
  119. package/dist/runtime/query/detached-calls.d.ts.map +1 -0
  120. package/dist/runtime/query/detached-calls.js +116 -0
  121. package/dist/runtime/query/detached-calls.js.map +1 -0
  122. package/dist/runtime/query/executor.d.ts +11 -3
  123. package/dist/runtime/query/executor.d.ts.map +1 -1
  124. package/dist/runtime/query/executor.js +100 -8
  125. package/dist/runtime/query/executor.js.map +1 -1
  126. package/dist/runtime/query/index.d.ts +20 -0
  127. package/dist/runtime/query/index.d.ts.map +1 -1
  128. package/dist/runtime/query/index.js +31 -5
  129. package/dist/runtime/query/index.js.map +1 -1
  130. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  131. package/dist/runtime/query/iteration/outstanding-work.js +11 -3
  132. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  133. package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
  134. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  135. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  136. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  137. package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
  138. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  139. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  140. package/dist/runtime/query/session-storage.js +8 -3
  141. package/dist/runtime/query/session-storage.js.map +1 -1
  142. package/dist/runtime/query/steering.d.ts +29 -3
  143. package/dist/runtime/query/steering.d.ts.map +1 -1
  144. package/dist/runtime/query/steering.js +58 -3
  145. package/dist/runtime/query/steering.js.map +1 -1
  146. package/dist/runtime/query/tooling.d.ts +1 -0
  147. package/dist/runtime/query/tooling.d.ts.map +1 -1
  148. package/dist/runtime/query/tooling.js +1 -0
  149. package/dist/runtime/query/tooling.js.map +1 -1
  150. package/dist/runtime/system-events.d.ts +5 -0
  151. package/dist/runtime/system-events.d.ts.map +1 -1
  152. package/dist/runtime/system-events.js +7 -1
  153. package/dist/runtime/system-events.js.map +1 -1
  154. package/dist/sandbox/denial.d.ts +11 -0
  155. package/dist/sandbox/denial.d.ts.map +1 -0
  156. package/dist/sandbox/denial.js +61 -0
  157. package/dist/sandbox/denial.js.map +1 -0
  158. package/dist/sandbox/file-walk-program.d.ts.map +1 -1
  159. package/dist/sandbox/file-walk-program.js +31 -13
  160. package/dist/sandbox/file-walk-program.js.map +1 -1
  161. package/dist/sandbox/file-walk.d.ts +2 -0
  162. package/dist/sandbox/file-walk.d.ts.map +1 -1
  163. package/dist/sandbox/file-walk.js +64 -13
  164. package/dist/sandbox/file-walk.js.map +1 -1
  165. package/dist/sandbox/ignore-matcher.d.ts +30 -0
  166. package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
  167. package/dist/sandbox/ignore-matcher.js +11 -0
  168. package/dist/sandbox/ignore-matcher.js.map +1 -0
  169. package/dist/sandbox/ignore-source.d.ts +26 -0
  170. package/dist/sandbox/ignore-source.d.ts.map +1 -0
  171. package/dist/sandbox/ignore-source.js +221 -0
  172. package/dist/sandbox/ignore-source.js.map +1 -0
  173. package/dist/sandbox/provider/local.d.ts.map +1 -1
  174. package/dist/sandbox/provider/local.js +11 -1
  175. package/dist/sandbox/provider/local.js.map +1 -1
  176. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  177. package/dist/scheduler/completion-inbox.js +1 -0
  178. package/dist/scheduler/completion-inbox.js.map +1 -1
  179. package/dist/session/log-hash.d.ts.map +1 -1
  180. package/dist/session/log-hash.js +18 -1
  181. package/dist/session/log-hash.js.map +1 -1
  182. package/dist/store/session-index/sqlite.d.ts.map +1 -1
  183. package/dist/store/session-index/sqlite.js +169 -16
  184. package/dist/store/session-index/sqlite.js.map +1 -1
  185. package/dist/tools/builtins/bash.d.ts.map +1 -1
  186. package/dist/tools/builtins/bash.js +10 -4
  187. package/dist/tools/builtins/bash.js.map +1 -1
  188. package/dist/tools/builtins/computer-use.d.ts +35 -0
  189. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  190. package/dist/tools/builtins/computer-use.js +512 -67
  191. package/dist/tools/builtins/computer-use.js.map +1 -1
  192. package/dist/tools/builtins/glob.d.ts +1 -0
  193. package/dist/tools/builtins/glob.d.ts.map +1 -1
  194. package/dist/tools/builtins/glob.js +65 -28
  195. package/dist/tools/builtins/glob.js.map +1 -1
  196. package/dist/tools/builtins/grep.d.ts +2 -0
  197. package/dist/tools/builtins/grep.d.ts.map +1 -1
  198. package/dist/tools/builtins/grep.js +94 -40
  199. package/dist/tools/builtins/grep.js.map +1 -1
  200. package/dist/tools/builtins/job.d.ts +1 -1
  201. package/dist/tools/builtins/job.d.ts.map +1 -1
  202. package/dist/tools/builtins/job.js +34 -7
  203. package/dist/tools/builtins/job.js.map +1 -1
  204. package/dist/tools/builtins/search-support.d.ts +44 -0
  205. package/dist/tools/builtins/search-support.d.ts.map +1 -0
  206. package/dist/tools/builtins/search-support.js +89 -0
  207. package/dist/tools/builtins/search-support.js.map +1 -0
  208. package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
  209. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
  210. package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
  211. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
  212. package/dist/tools/builtins/wait-for-job.d.ts +1 -1
  213. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  214. package/dist/tools/builtins/wait-for-job.js +13 -2
  215. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  216. package/dist/tools/command-shell.d.ts.map +1 -1
  217. package/dist/tools/command-shell.js +14 -5
  218. package/dist/tools/command-shell.js.map +1 -1
  219. package/dist/types/computer-use/index.d.ts +129 -13
  220. package/dist/types/computer-use/index.d.ts.map +1 -1
  221. package/dist/types/computer-use/index.js.map +1 -1
  222. package/dist/types/errors/catalog.d.ts.map +1 -1
  223. package/dist/types/errors/catalog.js +6 -0
  224. package/dist/types/errors/catalog.js.map +1 -1
  225. package/dist/types/plugin/index.d.ts +24 -0
  226. package/dist/types/plugin/index.d.ts.map +1 -1
  227. package/dist/types/plugin/index.js +4 -0
  228. package/dist/types/plugin/index.js.map +1 -1
  229. package/dist/types/provider/errors.d.ts +7 -0
  230. package/dist/types/provider/errors.d.ts.map +1 -1
  231. package/dist/types/provider/errors.js +27 -3
  232. package/dist/types/provider/errors.js.map +1 -1
  233. package/dist/types/sandbox/index.d.ts +38 -1
  234. package/dist/types/sandbox/index.d.ts.map +1 -1
  235. package/dist/types/sandbox/index.js.map +1 -1
  236. package/dist/types/session/records.d.ts +479 -1
  237. package/dist/types/session/records.d.ts.map +1 -1
  238. package/dist/types/session/records.js +45 -1
  239. package/dist/types/session/records.js.map +1 -1
  240. package/dist/types/tool/index.d.ts +30 -0
  241. package/dist/types/tool/index.d.ts.map +1 -1
  242. package/dist/types/tool/index.js.map +1 -1
  243. package/dist/types/tool/presentation.d.ts +8 -0
  244. package/dist/types/tool/presentation.d.ts.map +1 -1
  245. package/package.json +1 -1
  246. package/src/account/binding.ts +43 -0
  247. package/src/account/index.ts +32 -0
  248. package/src/account/lock.ts +184 -0
  249. package/src/account/registry.ts +413 -0
  250. package/src/account/types.ts +137 -0
  251. package/src/compaction/context-window.ts +163 -2
  252. package/src/execution/workflow/index.ts +14 -1
  253. package/src/execution/workflow/journal.ts +161 -11
  254. package/src/execution/workflow/prelude.ts +51 -8
  255. package/src/execution/workflow/resume.ts +313 -0
  256. package/src/execution/workflow/runtime.ts +127 -9
  257. package/src/execution/workflow/types.ts +109 -1
  258. package/src/index.ts +17 -0
  259. package/src/peers/record.ts +18 -0
  260. package/src/peers/registry.ts +2 -2
  261. package/src/plugin/compat.ts +173 -0
  262. package/src/plugin/loader.ts +7 -1
  263. package/src/pricing/index.ts +2 -1
  264. package/src/process/kill-tree.ts +106 -4
  265. package/src/prompt/coding-agent-doctrine.ts +9 -1
  266. package/src/provider/errors.ts +27 -1
  267. package/src/provider/model-window-suffix.ts +84 -0
  268. package/src/provider/token-budget.ts +9 -3
  269. package/src/public-runtime.ts +38 -2
  270. package/src/public-types.ts +16 -0
  271. package/src/registry/command/kernel-commands.ts +1 -32
  272. package/src/runtime/jobs/awaited-jobs.ts +11 -0
  273. package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
  274. package/src/runtime/jobs/liveness.ts +144 -0
  275. package/src/runtime/jobs/registry.ts +114 -2
  276. package/src/runtime/jobs/wake-budget.ts +55 -0
  277. package/src/runtime/query/detached-calls.ts +135 -0
  278. package/src/runtime/query/executor.ts +102 -12
  279. package/src/runtime/query/index.ts +50 -7
  280. package/src/runtime/query/iteration/outstanding-work.ts +12 -3
  281. package/src/runtime/query/iteration/phases/context.ts +7 -0
  282. package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
  283. package/src/runtime/query/session-storage.ts +8 -3
  284. package/src/runtime/query/steering.ts +77 -6
  285. package/src/runtime/query/tooling.ts +2 -0
  286. package/src/runtime/system-events.ts +12 -1
  287. package/src/sandbox/denial.ts +79 -0
  288. package/src/sandbox/file-walk-program.ts +32 -13
  289. package/src/sandbox/file-walk.ts +75 -12
  290. package/src/sandbox/ignore-matcher.ts +46 -0
  291. package/src/sandbox/ignore-source.ts +220 -0
  292. package/src/sandbox/provider/local.ts +11 -1
  293. package/src/scheduler/completion-inbox.ts +1 -0
  294. package/src/session/log-hash.ts +21 -0
  295. package/src/store/session-index/sqlite.ts +171 -16
  296. package/src/tools/builtins/bash.ts +11 -4
  297. package/src/tools/builtins/computer-use.ts +593 -68
  298. package/src/tools/builtins/glob.ts +79 -32
  299. package/src/tools/builtins/grep.ts +107 -43
  300. package/src/tools/builtins/job.ts +42 -12
  301. package/src/tools/builtins/search-support.ts +107 -0
  302. package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
  303. package/src/tools/builtins/wait-for-job.ts +14 -2
  304. package/src/tools/command-shell.ts +13 -5
  305. package/src/types/computer-use/index.ts +157 -15
  306. package/src/types/errors/catalog.ts +6 -0
  307. package/src/types/plugin/index.ts +8 -0
  308. package/src/types/provider/errors.ts +40 -3
  309. package/src/types/sandbox/index.ts +40 -1
  310. package/src/types/session/records.ts +52 -2
  311. package/src/types/tool/index.ts +30 -0
  312. package/src/types/tool/presentation.ts +8 -0
@@ -0,0 +1,313 @@
1
+ import { createHash, createHmac, timingSafeEqual } from 'node:crypto'
2
+
3
+ import type {
4
+ ChainedWorkflowJournalRecord,
5
+ CheckedWorkflowJournal,
6
+ WorkflowFinishStatus,
7
+ WorkflowJournalRecord,
8
+ WorkflowResumeState,
9
+ } from './types.js'
10
+
11
+ /** The key version. A change to how keys are made bumps it, so old keys simply never match. */
12
+ const KEY_VERSION = 'k1'
13
+
14
+ function sha256Hex(text: string): string {
15
+ return createHash('sha256').update(text).digest('hex')
16
+ }
17
+
18
+ /** JSON with every object's keys sorted and `__proto__` dropped, so equal values give equal text. */
19
+ function canonicalJson(value: unknown): string {
20
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(',')}]`
21
+ if (typeof value === 'object' && value !== null) {
22
+ const record = value as Record<string, unknown>
23
+ const keys = Object.keys(record)
24
+ .filter((key) => key !== '__proto__' && record[key] !== undefined)
25
+ .sort()
26
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`
27
+ }
28
+ return JSON.stringify(value) ?? 'null'
29
+ }
30
+
31
+ /** What of an `agent()` call decides its result. `label`, `phase` and progress callbacks do not. */
32
+ export interface WorkflowCallIdentity {
33
+ readonly schema?: unknown
34
+ readonly model?: string
35
+ readonly effort?: string
36
+ readonly agentType?: string
37
+ }
38
+
39
+ /**
40
+ * The key a call is saved and looked up under: the prompt, the options that
41
+ * change what the agent does (schema, model, effort, agent type) and how many
42
+ * identical calls came before it in the same pass. Each part is length-prefixed
43
+ * so one cannot be read as part of another.
44
+ */
45
+ export function workflowCallKey(
46
+ prompt: string,
47
+ call: WorkflowCallIdentity,
48
+ ordinal: number,
49
+ ): string {
50
+ return `${KEY_VERSION}:${sha256Hex(`${workflowCallContent(prompt, call)}${ordinal}`)}`
51
+ }
52
+
53
+ /**
54
+ * What makes two calls the same call: the prompt and the options that change
55
+ * the agent, in a form where key order and `undefined` do not matter. The
56
+ * runtime counts repeats of this text to give identical calls distinct slots,
57
+ * and the key hashes the same text, so the two can never disagree.
58
+ */
59
+ export function workflowCallContent(prompt: string, call: WorkflowCallIdentity): string {
60
+ const options = canonicalJson({
61
+ schema: call.schema,
62
+ model: call.model,
63
+ effort: call.effort,
64
+ agentType: call.agentType,
65
+ })
66
+ return `${prompt.length}:${prompt}${options.length}:${options}`
67
+ }
68
+
69
+ /** The hash the first line of a run's journal chains from. It binds the file to its run id. */
70
+ export function workflowJournalSeed(runId: string): string {
71
+ return sha256Hex(`namzu-workflow-journal\0${runId}`)
72
+ }
73
+
74
+ /** The hash a line contributes to the next one: its exact bytes, newline included. */
75
+ export function workflowJournalLineHash(line: string): string {
76
+ return sha256Hex(`${line}\n`)
77
+ }
78
+
79
+ /** The line `record` is written as. With a key, a `mac` over the rest of the line is added last. */
80
+ export function formatWorkflowJournalLine(
81
+ record: WorkflowJournalRecord,
82
+ seq: number,
83
+ prev: string,
84
+ key?: string | Uint8Array,
85
+ ): string {
86
+ const body = { seq, prev, ...record }
87
+ if (key === undefined) return JSON.stringify(body)
88
+ const mac = createHmac('sha256', key).update(JSON.stringify(body)).digest('hex')
89
+ return JSON.stringify({ ...body, mac })
90
+ }
91
+
92
+ function macMatches(record: Record<string, unknown>, key: string | Uint8Array): boolean {
93
+ const { mac, ...rest } = record
94
+ if (typeof mac !== 'string') return false
95
+ const expected = createHmac('sha256', key).update(JSON.stringify(rest)).digest()
96
+ const actual = Buffer.from(mac, 'hex')
97
+ return actual.length === expected.length && timingSafeEqual(actual, expected)
98
+ }
99
+
100
+ function parseLine(line: string): Record<string, unknown> | undefined {
101
+ try {
102
+ const value: unknown = JSON.parse(line)
103
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
104
+ ? (value as Record<string, unknown>)
105
+ : undefined
106
+ } catch {
107
+ return undefined
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Reads a journal's text and keeps only the records that can be trusted.
113
+ *
114
+ * Every line carries `seq` (counting trusted lines from 1) and `prev` (the hash
115
+ * of the line before; the first chains from a seed bound to the run id). A
116
+ * line that does not chain, a line that does not parse, and anything after
117
+ * one is dropped until a `run_started` of a resumed pass picks the chain up
118
+ * again at the last trusted line. A last line without its newline that is not
119
+ * whole JSON is a torn write and is ignored; one that is whole JSON is read
120
+ * like any other line. The file is never rewritten.
121
+ *
122
+ * The chain alone cannot notice an edit to the final line (nothing chains from
123
+ * it yet). With `key`, every line must also carry a `mac` made with that
124
+ * secret, which closes that gap against anyone who does not hold the key.
125
+ */
126
+ export function readWorkflowJournalChecked(
127
+ text: string,
128
+ runId: string,
129
+ options: { readonly key?: string | Uint8Array } = {},
130
+ ): CheckedWorkflowJournal {
131
+ const endsWithNewline = text === '' || text.endsWith('\n')
132
+ const pieces = text.split('\n')
133
+ const fragment = endsWithNewline ? undefined : pieces.pop()
134
+ if (endsWithNewline) pieces.pop()
135
+ // A last line without its newline that is whole JSON was only missing the newline: it is
136
+ // judged like any other line (the writer adds the newline before the next record, so the
137
+ // hash it contributes is the one of the line with its newline). Anything else is a torn write.
138
+ const wholeFragment =
139
+ fragment !== undefined && fragment !== '' && parseLine(fragment) !== undefined
140
+ if (wholeFragment) pieces.push(fragment as string)
141
+ const tornTail = fragment !== undefined && fragment !== '' && !wholeFragment
142
+
143
+ const parsed = pieces.map((line) => parseLine(line))
144
+ const firstRecord = parsed.find((record) => record !== undefined)
145
+ if (firstRecord !== undefined && typeof firstRecord.seq !== 'number') {
146
+ return {
147
+ records: [],
148
+ legacy: true,
149
+ tornTail,
150
+ lastSeq: 0,
151
+ lastHash: workflowJournalSeed(runId),
152
+ endsWithNewline,
153
+ untrusted: parsed.filter((record) => record !== undefined) as WorkflowJournalRecord[],
154
+ }
155
+ }
156
+
157
+ const records: ChainedWorkflowJournalRecord[] = []
158
+ let lastSeq = 0
159
+ let lastHash = workflowJournalSeed(runId)
160
+ let gapStart: number | undefined
161
+ let gapLines = 0
162
+ let gapParsed = false
163
+ let brokenAt: number | undefined
164
+ const closeGap = (): void => {
165
+ if (gapStart !== undefined && (gapLines > 1 || gapParsed)) brokenAt ??= gapStart
166
+ gapStart = undefined
167
+ gapLines = 0
168
+ gapParsed = false
169
+ }
170
+
171
+ for (let i = 0; i < pieces.length; i++) {
172
+ const line = pieces[i] as string
173
+ const record = parsed[i]
174
+ const chains =
175
+ record !== undefined &&
176
+ record.seq === lastSeq + 1 &&
177
+ record.prev === lastHash &&
178
+ (options.key === undefined || macMatches(record, options.key))
179
+ const first = records.length === 0
180
+ const acceptable =
181
+ chains &&
182
+ (first
183
+ ? record.type === 'run_started' && record.runId === runId
184
+ : gapStart === undefined || record.type === 'run_started')
185
+ if (acceptable) {
186
+ closeGap()
187
+ records.push(record as unknown as ChainedWorkflowJournalRecord)
188
+ lastSeq = record.seq as number
189
+ lastHash = workflowJournalLineHash(line)
190
+ continue
191
+ }
192
+ if (gapStart === undefined) gapStart = i + 1
193
+ gapLines++
194
+ if (record !== undefined) gapParsed = true
195
+ }
196
+ // A gap that runs to the end is a change, except a single line that does not parse
197
+ // (what a torn write leaves once the next pass closed it with a newline).
198
+ closeGap()
199
+
200
+ return {
201
+ records,
202
+ legacy: false,
203
+ tornTail,
204
+ ...(brokenAt !== undefined ? { brokenAt } : {}),
205
+ lastSeq,
206
+ lastHash,
207
+ endsWithNewline,
208
+ untrusted: [],
209
+ }
210
+ }
211
+
212
+ function hasResult(record: { readonly result?: unknown }): boolean {
213
+ return Object.hasOwn(record, 'result') && record.result !== undefined && record.result !== null
214
+ }
215
+
216
+ /**
217
+ * Folds a checked journal into what a resumed pass needs.
218
+ *
219
+ * The last saved result for a key wins. An agent that was asked for again
220
+ * after a result was saved gives that result up until it finishes again. A
221
+ * failed or skipped outcome removes the result and marks the key failed; an
222
+ * outcome the run recorded as interrupted (stopped, not failed) leaves the key
223
+ * as seen but unfinished. Lines marked `cached` repeat an earlier result and
224
+ * are skipped, and so is any agent record from a pass older than the latest
225
+ * `run_started`, which a slow agent of an earlier pass can still write.
226
+ */
227
+ export function loadWorkflowResume(checked: CheckedWorkflowJournal): WorkflowResumeState {
228
+ const results = new Map<
229
+ string,
230
+ { value: unknown; pass: number; index: number; childSessionId?: string }
231
+ >()
232
+ const seen = new Set<string>()
233
+ const failed = new Set<string>()
234
+ const demoted = new Set<string>()
235
+ const problems: string[] = []
236
+ const keysByIndex = new Map<string, string>()
237
+ let currentPass = 0
238
+ let scriptSha256: string | undefined
239
+ let previousFinish: WorkflowFinishStatus | 'unfinished' = 'unfinished'
240
+
241
+ for (const record of checked.records) {
242
+ if (record.type === 'run_started') {
243
+ currentPass = record.resume?.pass ?? 1
244
+ scriptSha256 = record.scriptSha256
245
+ previousFinish = 'unfinished'
246
+ keysByIndex.clear()
247
+ continue
248
+ }
249
+ if (record.type === 'run_finished') {
250
+ previousFinish = record.status
251
+ continue
252
+ }
253
+ if (record.type !== 'agent_request' && record.type !== 'agent_outcome') continue
254
+ const pass = record.pass ?? 1
255
+ if (pass < currentPass || record.cached === true) continue
256
+ if (record.type === 'agent_request') {
257
+ if (record.key === undefined) continue
258
+ keysByIndex.set(`${pass}:${record.index}`, record.key)
259
+ seen.add(record.key)
260
+ if (results.delete(record.key)) demoted.add(record.key)
261
+ continue
262
+ }
263
+ const key = record.key ?? keysByIndex.get(`${pass}:${record.index}`)
264
+ if (key === undefined) continue
265
+ seen.add(key)
266
+ if (record.state === 'done' && hasResult(record)) {
267
+ results.set(key, {
268
+ value: record.result,
269
+ pass,
270
+ index: record.index,
271
+ ...(record.childSessionId !== undefined ? { childSessionId: record.childSessionId } : {}),
272
+ })
273
+ failed.delete(key)
274
+ demoted.delete(key)
275
+ } else if (record.state === 'done' && record.unsaved === true) {
276
+ results.delete(key)
277
+ demoted.add(key)
278
+ } else if (record.interrupted === true) {
279
+ // Stopped, not failed: the agent runs again and the cache stays open.
280
+ results.delete(key)
281
+ } else {
282
+ results.delete(key)
283
+ demoted.delete(key)
284
+ failed.add(key)
285
+ }
286
+ }
287
+
288
+ if (checked.legacy) {
289
+ problems.push(
290
+ 'This run was saved before resuming was possible, so its agents cannot be reused.',
291
+ )
292
+ }
293
+ if (checked.brokenAt !== undefined) {
294
+ problems.push(
295
+ `The journal was changed or cut short at line ${checked.brokenAt}. Results after that point are not trusted and those agents will run again.`,
296
+ )
297
+ }
298
+ const started = checked.records.some((record) => record.type === 'run_started')
299
+ if (!checked.legacy && !started) problems.push('The journal has no usable record of the run.')
300
+
301
+ return {
302
+ pass: currentPass + 1,
303
+ results,
304
+ seen,
305
+ failed,
306
+ demoted,
307
+ ...(scriptSha256 !== undefined ? { scriptSha256 } : {}),
308
+ previousFinish,
309
+ problems,
310
+ resumable: !checked.legacy && started,
311
+ ...(checked.brokenAt !== undefined ? { brokenAt: checked.brokenAt } : {}),
312
+ }
313
+ }
@@ -9,6 +9,7 @@ import { preview, sha256 } from './journal.js'
9
9
  import { splitWorkflowScript } from './meta.js'
10
10
  import { type PhaseAgentView, PhaseRegistry, summarizeWorkflowPhases } from './phases.js'
11
11
  import { buildPrelude, preludeLineCount } from './prelude.js'
12
+ import { workflowCallContent, workflowCallKey } from './resume.js'
12
13
  import { parseJsonText, validateAgainstSchema } from './schema.js'
13
14
  import type {
14
15
  WorkflowAgentEvent,
@@ -83,7 +84,9 @@ export function resolveWorkflowLimits(limits: WorkflowLimits = {}): WorkflowReso
83
84
  16 * 1024 * 1024,
84
85
  ),
85
86
  ...(limits.tokenBudget !== undefined
86
- ? { tokenBudget: integer('tokenBudget', limits.tokenBudget, 1, Number.MAX_SAFE_INTEGER) }
87
+ ? {
88
+ tokenBudget: integer('tokenBudget', limits.tokenBudget, 1, Number.MAX_SAFE_INTEGER),
89
+ }
87
90
  : {}),
88
91
  maxDurationMs: integer(
89
92
  'maxDurationMs',
@@ -198,6 +201,14 @@ interface AgentRecord {
198
201
  error?: string
199
202
  skipped: boolean
200
203
  userSkipped: boolean
204
+ key?: string
205
+ /** Set when the result came from an earlier pass instead of a run. */
206
+ cached?: {
207
+ readonly pass: number
208
+ readonly index: number
209
+ readonly childSessionId?: string
210
+ }
211
+ childSessionId?: string
201
212
  }
202
213
 
203
214
  interface Waiter {
@@ -283,6 +294,10 @@ export class WorkflowRuntime {
283
294
  const runId =
284
295
  options.runId ?? `w${[...randomBytes(8)].map((b) => (b % 36).toString(36)).join('')}`
285
296
  const startedAt = now()
297
+ const pass = options.resume?.pass ?? 1
298
+ const resume = options.resume
299
+ let latch = false
300
+ const seenContent = new Map<string, number>()
286
301
  const phases = new PhaseRegistry()
287
302
  const agents = new Map<number, AgentRecord>()
288
303
  const inFlight = new Set<Promise<unknown>>()
@@ -335,7 +350,13 @@ export class WorkflowRuntime {
335
350
  declared,
336
351
  at,
337
352
  })
338
- journal({ type: 'phase', at, index: phase.index, title: phase.title, declared })
353
+ journal({
354
+ type: 'phase',
355
+ at,
356
+ index: phase.index,
357
+ title: phase.title,
358
+ declared,
359
+ })
339
360
  }
340
361
  return phase
341
362
  }
@@ -361,6 +382,7 @@ export class WorkflowRuntime {
361
382
  ...(agent.resultPreview !== undefined ? { resultPreview: agent.resultPreview } : {}),
362
383
  ...(agent.error !== undefined ? { error: agent.error } : {}),
363
384
  ...(agent.skipped ? { skipped: true } : {}),
385
+ ...(agent.cached !== undefined ? { cached: true } : {}),
364
386
  })
365
387
 
366
388
  // FIFO admission: an agent starts when fewer than maxConcurrency run.
@@ -411,7 +433,13 @@ export class WorkflowRuntime {
411
433
 
412
434
  const settle = (
413
435
  agent: AgentRecord,
414
- outcome: { state: 'done' | 'error'; skipped?: boolean; value?: unknown; error?: string },
436
+ outcome: {
437
+ state: 'done' | 'error'
438
+ skipped?: boolean
439
+ value?: unknown
440
+ error?: string
441
+ interrupted?: boolean
442
+ },
415
443
  ): HostCallResult => {
416
444
  const at = now()
417
445
  agent.state = outcome.state
@@ -434,11 +462,25 @@ export class WorkflowRuntime {
434
462
  value = null
435
463
  }
436
464
  emit(agentEvent(agent))
465
+ const finalState = agent.state === 'error' ? 'error' : agent.skipped ? 'skipped' : 'done'
466
+ let saved: { result: unknown } | { unsaved: true } | undefined
467
+ if (finalState === 'done' && value !== null && agent.cached === undefined) {
468
+ try {
469
+ saved = { result: JSON.parse(JSON.stringify(value)) as unknown }
470
+ } catch {
471
+ saved = { unsaved: true }
472
+ journal({
473
+ type: 'log',
474
+ at,
475
+ message: "An agent's result could not be saved, so a resume will run it again.",
476
+ })
477
+ }
478
+ }
437
479
  journal({
438
480
  type: 'agent_outcome',
439
481
  at,
440
482
  index: agent.index,
441
- state: agent.state === 'error' ? 'error' : agent.skipped ? 'skipped' : 'done',
483
+ state: finalState,
442
484
  tokens: agent.tokens,
443
485
  toolCalls: agent.toolCalls,
444
486
  queuedAt: agent.queuedAt,
@@ -446,6 +488,22 @@ export class WorkflowRuntime {
446
488
  finishedAt: at,
447
489
  ...(agent.resultPreview !== undefined ? { resultPreview: agent.resultPreview } : {}),
448
490
  ...(agent.error !== undefined ? { error: agent.error } : {}),
491
+ ...(agent.key !== undefined ? { key: agent.key } : {}),
492
+ pass,
493
+ ...(agent.cached !== undefined
494
+ ? {
495
+ cached: true as const,
496
+ resultFrom: {
497
+ pass: agent.cached.pass,
498
+ index: agent.cached.index,
499
+ },
500
+ }
501
+ : {}),
502
+ ...(saved ?? {}),
503
+ ...(outcome.interrupted === true && finalState === 'error'
504
+ ? { interrupted: true as const }
505
+ : {}),
506
+ ...(agent.childSessionId !== undefined ? { childSessionId: agent.childSessionId } : {}),
449
507
  })
450
508
  return { ok: true, value: { v: value, t: totalTokens() } }
451
509
  }
@@ -457,6 +515,7 @@ export class WorkflowRuntime {
457
515
  ): HostCallResult => {
458
516
  agent.tokens = count(outcome.tokens)
459
517
  agent.toolCalls = count(outcome.toolCalls)
518
+ if (outcome.sessionId !== undefined) agent.childSessionId = outcome.sessionId
460
519
  if (agent.userSkipped || outcome.state === 'skipped') {
461
520
  return settle(agent, { state: 'done', skipped: true })
462
521
  }
@@ -464,10 +523,14 @@ export class WorkflowRuntime {
464
523
  return settle(agent, {
465
524
  state: 'error',
466
525
  error: 'The workflow was stopped before this agent finished.',
526
+ interrupted: true,
467
527
  })
468
528
  }
469
529
  if (outcome.state === 'error') {
470
- return settle(agent, { state: 'error', error: outcome.error ?? 'The agent failed.' })
530
+ return settle(agent, {
531
+ state: 'error',
532
+ error: outcome.error ?? 'The agent failed.',
533
+ })
471
534
  }
472
535
  if (call.schema === undefined) {
473
536
  return settle(agent, { state: 'done', value: outcome.text ?? '' })
@@ -503,6 +566,18 @@ export class WorkflowRuntime {
503
566
  return { ok: false, error: reason }
504
567
  }
505
568
  const index = ++requested
569
+ const contentId = workflowCallContent(call.prompt, call)
570
+ const ordinal = seenContent.get(contentId) ?? 0
571
+ seenContent.set(contentId, ordinal + 1)
572
+ const key = workflowCallKey(call.prompt, call, ordinal)
573
+ const hit = latch ? undefined : resume?.results.get(key)
574
+ if (
575
+ resume !== undefined &&
576
+ hit === undefined &&
577
+ !(resume.seen.has(key) && !resume.failed.has(key) && !resume.demoted.has(key))
578
+ ) {
579
+ latch = true
580
+ }
506
581
  const phaseTitle = call.phase ?? call.cursor
507
582
  const phase = phaseTitle?.trim() ? announce(phaseTitle, false) : undefined
508
583
  const queuedAt = now()
@@ -521,13 +596,26 @@ export class WorkflowRuntime {
521
596
  toolCalls: 0,
522
597
  skipped: false,
523
598
  userSkipped: false,
599
+ key,
524
600
  }
525
601
  agents.set(index, agent)
602
+ if (hit !== undefined) {
603
+ agent.cached = {
604
+ pass: hit.pass,
605
+ index: hit.index,
606
+ ...(hit.childSessionId !== undefined ? { childSessionId: hit.childSessionId } : {}),
607
+ }
608
+ if (hit.childSessionId !== undefined) agent.childSessionId = hit.childSessionId
609
+ agent.startedAt = queuedAt
610
+ }
526
611
  emit(agentEvent(agent))
527
612
  journal({
528
613
  type: 'agent_request',
529
614
  at: queuedAt,
530
615
  index,
616
+ key,
617
+ pass,
618
+ ...(hit !== undefined ? { cached: true as const } : {}),
531
619
  label: agent.label,
532
620
  ...(phase ? { phaseIndex: phase.index, phaseTitle: phase.title } : {}),
533
621
  promptPreview: preview(call.prompt),
@@ -540,6 +628,12 @@ export class WorkflowRuntime {
540
628
  },
541
629
  })
542
630
 
631
+ if (hit !== undefined) {
632
+ return settle(agent, {
633
+ state: 'done',
634
+ value: structuredClone(hit.value),
635
+ })
636
+ }
543
637
  const admitted = await acquire(agent)
544
638
  if (!admitted) {
545
639
  return agent.userSkipped
@@ -547,6 +641,7 @@ export class WorkflowRuntime {
547
641
  : settle(agent, {
548
642
  state: 'error',
549
643
  error: 'The workflow was stopped before this agent started.',
644
+ interrupted: true,
550
645
  })
551
646
  }
552
647
  try {
@@ -554,6 +649,7 @@ export class WorkflowRuntime {
554
649
  return settle(agent, {
555
650
  state: 'error',
556
651
  error: `Not started: the token budget for this workflow (${limits.tokenBudget} tokens) is spent.`,
652
+ interrupted: true,
557
653
  })
558
654
  }
559
655
  if (controller.signal.aborted || agent.userSkipped) {
@@ -562,6 +658,7 @@ export class WorkflowRuntime {
562
658
  : settle(agent, {
563
659
  state: 'error',
564
660
  error: 'The workflow was stopped before this agent started.',
661
+ interrupted: true,
565
662
  })
566
663
  }
567
664
  agent.startedAt = now()
@@ -630,7 +727,7 @@ export class WorkflowRuntime {
630
727
  // --- start ---
631
728
  if (options.journal?.writeScript) {
632
729
  try {
633
- await options.journal.writeScript(options.script, sha256(options.script))
730
+ await options.journal.writeScript(options.script, sha256(options.script), pass)
634
731
  } catch (error) {
635
732
  journalError = error instanceof Error ? error.message : String(error)
636
733
  }
@@ -644,6 +741,17 @@ export class WorkflowRuntime {
644
741
  meta,
645
742
  args,
646
743
  limits,
744
+ ...(resume !== undefined
745
+ ? {
746
+ resume: {
747
+ pass,
748
+ previousFinish: resume.previousFinish,
749
+ scriptSha256Changed:
750
+ resume.scriptSha256 !== undefined && resume.scriptSha256 !== sha256(options.script),
751
+ },
752
+ }
753
+ : {}),
754
+ ...(options.scriptPath !== undefined ? { scriptPath: options.scriptPath } : {}),
647
755
  })
648
756
  emit({
649
757
  type: 'workflow_started',
@@ -665,7 +773,11 @@ export class WorkflowRuntime {
665
773
  maxPendingHostCalls: limits.maxAgents + 100,
666
774
  })
667
775
 
668
- const preludeConfig = { args, tokenLimit: limits.tokenBudget ?? null }
776
+ const preludeConfig = {
777
+ args,
778
+ tokenLimit: limits.tokenBudget ?? null,
779
+ deterministic: options.deterministic === true,
780
+ }
669
781
  const runResult = await codeRuntime.run({
670
782
  source: `${buildPrelude(preludeConfig)}\n${body}`,
671
783
  allowedCalls: [WORKFLOW_HOST_CALL],
@@ -676,7 +788,10 @@ export class WorkflowRuntime {
676
788
  onOutput,
677
789
  onHostCall: (request) => {
678
790
  if (request.name !== WORKFLOW_HOST_CALL) {
679
- return Promise.resolve({ ok: false, error: `Unknown host call "${request.name}".` })
791
+ return Promise.resolve({
792
+ ok: false,
793
+ error: `Unknown host call "${request.name}".`,
794
+ })
680
795
  }
681
796
  let call: AgentCall
682
797
  try {
@@ -738,7 +853,7 @@ export class WorkflowRuntime {
738
853
  const progress =
739
854
  begun.length === 0
740
855
  ? 'No agent had started.'
741
- : `Before it failed: ${begun.length} agent${begun.length === 1 ? '' : 's'} started, ${finished} finished, ${failedAgents} failed. A rerun starts the whole script again, so finished agents run again.`
856
+ : `Before it failed: ${begun.length} agent${begun.length === 1 ? '' : 's'} started, ${finished} finished, ${failedAgents} failed. ${options.canResume === true ? 'Resume the run to continue: agents whose prompt and options are unchanged return their saved results.' : 'A rerun starts the whole script again, so finished agents run again.'}`
742
857
  error = `${describeScriptFailure(outcome, body, preludeLineCount(preludeConfig))}\n${progress}${scriptPhase !== undefined ? ` Last phase: "${scriptPhase}".` : ''}`
743
858
  }
744
859
 
@@ -751,6 +866,9 @@ export class WorkflowRuntime {
751
866
  tokens: all.reduce((sum, a) => sum + a.tokens, 0),
752
867
  toolCalls: all.reduce((sum, a) => sum + a.toolCalls, 0),
753
868
  elapsedMs: Math.max(0, now() - startedAt),
869
+ ...(all.some((a) => a.cached !== undefined)
870
+ ? { cachedAgents: all.filter((a) => a.cached !== undefined).length }
871
+ : {}),
754
872
  }
755
873
  const at = now()
756
874
  emit({