@earendil-works/pi-durable 1.4.0 → 1.5.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 (221) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.md +181 -84
  3. package/dist/codemode/codemode.d.ts +4 -4
  4. package/dist/codemode/codemode.d.ts.map +1 -1
  5. package/dist/codemode/codemode.js +9 -12
  6. package/dist/codemode/codemode.js.map +1 -1
  7. package/dist/codemode/models.js +4 -4
  8. package/dist/codemode/models.js.map +1 -1
  9. package/dist/env/index.d.ts +12 -10
  10. package/dist/env/index.d.ts.map +1 -1
  11. package/dist/env/index.js.map +1 -1
  12. package/dist/env/node-watch.d.ts +3 -3
  13. package/dist/env/node-watch.d.ts.map +1 -1
  14. package/dist/env/node-watch.js +18 -16
  15. package/dist/env/node-watch.js.map +1 -1
  16. package/dist/env/node.d.ts +2 -2
  17. package/dist/env/node.d.ts.map +1 -1
  18. package/dist/env/node.js +1 -1
  19. package/dist/env/node.js.map +1 -1
  20. package/dist/errors.d.ts +18 -2
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +26 -3
  23. package/dist/errors.js.map +1 -1
  24. package/dist/harness/agent.d.ts.map +1 -1
  25. package/dist/harness/agent.js +10 -2
  26. package/dist/harness/agent.js.map +1 -1
  27. package/dist/harness/compaction.d.ts.map +1 -1
  28. package/dist/harness/compaction.js +19 -2
  29. package/dist/harness/compaction.js.map +1 -1
  30. package/dist/harness/context.d.ts +23 -7
  31. package/dist/harness/context.d.ts.map +1 -1
  32. package/dist/harness/context.js +71 -25
  33. package/dist/harness/context.js.map +1 -1
  34. package/dist/harness/define.d.ts +7 -3
  35. package/dist/harness/define.d.ts.map +1 -1
  36. package/dist/harness/define.js +4 -0
  37. package/dist/harness/define.js.map +1 -1
  38. package/dist/harness/generation.d.ts +38 -5
  39. package/dist/harness/generation.d.ts.map +1 -1
  40. package/dist/harness/generation.js +445 -96
  41. package/dist/harness/generation.js.map +1 -1
  42. package/dist/harness/harness.d.ts +2 -0
  43. package/dist/harness/harness.d.ts.map +1 -1
  44. package/dist/harness/harness.js +87 -62
  45. package/dist/harness/harness.js.map +1 -1
  46. package/dist/harness/inbox.d.ts +5 -1
  47. package/dist/harness/inbox.d.ts.map +1 -1
  48. package/dist/harness/inbox.js +4 -1
  49. package/dist/harness/inbox.js.map +1 -1
  50. package/dist/harness/json.d.ts +20 -4
  51. package/dist/harness/json.d.ts.map +1 -1
  52. package/dist/harness/json.js +23 -5
  53. package/dist/harness/json.js.map +1 -1
  54. package/dist/harness/live.d.ts +122 -16
  55. package/dist/harness/live.d.ts.map +1 -1
  56. package/dist/harness/live.js +93 -12
  57. package/dist/harness/live.js.map +1 -1
  58. package/dist/harness/partial-changes.d.ts +54 -0
  59. package/dist/harness/partial-changes.d.ts.map +1 -0
  60. package/dist/harness/partial-changes.js +162 -0
  61. package/dist/harness/partial-changes.js.map +1 -0
  62. package/dist/harness/registry-reader.d.ts +11 -0
  63. package/dist/harness/registry-reader.d.ts.map +1 -0
  64. package/dist/harness/registry-reader.js +25 -0
  65. package/dist/harness/registry-reader.js.map +1 -0
  66. package/dist/harness/scheduler.d.ts +46 -19
  67. package/dist/harness/scheduler.d.ts.map +1 -1
  68. package/dist/harness/scheduler.js +973 -376
  69. package/dist/harness/scheduler.js.map +1 -1
  70. package/dist/harness/submissions.d.ts +17 -10
  71. package/dist/harness/submissions.d.ts.map +1 -1
  72. package/dist/harness/submissions.js +62 -25
  73. package/dist/harness/submissions.js.map +1 -1
  74. package/dist/harness/task-graph.d.ts +12 -9
  75. package/dist/harness/task-graph.d.ts.map +1 -1
  76. package/dist/harness/task-graph.js +38 -28
  77. package/dist/harness/task-graph.js.map +1 -1
  78. package/dist/harness/telemetry.d.ts +11 -11
  79. package/dist/harness/telemetry.d.ts.map +1 -1
  80. package/dist/harness/telemetry.js +7 -7
  81. package/dist/harness/telemetry.js.map +1 -1
  82. package/dist/harness/tool.d.ts +2 -0
  83. package/dist/harness/tool.d.ts.map +1 -1
  84. package/dist/harness/tool.js +171 -55
  85. package/dist/harness/tool.js.map +1 -1
  86. package/dist/harness/transcript.d.ts +72 -0
  87. package/dist/harness/transcript.d.ts.map +1 -0
  88. package/dist/harness/transcript.js +264 -0
  89. package/dist/harness/transcript.js.map +1 -0
  90. package/dist/harness/types.d.ts +218 -59
  91. package/dist/harness/types.d.ts.map +1 -1
  92. package/dist/harness/types.js.map +1 -1
  93. package/dist/harness/util.d.ts +3 -1
  94. package/dist/harness/util.d.ts.map +1 -1
  95. package/dist/harness/util.js +9 -0
  96. package/dist/harness/util.js.map +1 -1
  97. package/dist/harness/view.d.ts +43 -22
  98. package/dist/harness/view.d.ts.map +1 -1
  99. package/dist/harness/view.js +130 -71
  100. package/dist/harness/view.js.map +1 -1
  101. package/dist/index.d.ts +9 -7
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +4 -3
  104. package/dist/index.js.map +1 -1
  105. package/dist/search.d.ts +19 -3
  106. package/dist/search.d.ts.map +1 -1
  107. package/dist/search.js +107 -44
  108. package/dist/search.js.map +1 -1
  109. package/dist/session/cached-storage.d.ts +71 -0
  110. package/dist/session/cached-storage.d.ts.map +1 -0
  111. package/dist/session/cached-storage.js +269 -0
  112. package/dist/session/cached-storage.js.map +1 -0
  113. package/dist/session/conversation-tails.d.ts +33 -0
  114. package/dist/session/conversation-tails.d.ts.map +1 -0
  115. package/dist/session/conversation-tails.js +101 -0
  116. package/dist/session/conversation-tails.js.map +1 -0
  117. package/dist/session/keys.d.ts +68 -0
  118. package/dist/session/keys.d.ts.map +1 -0
  119. package/dist/session/keys.js +161 -0
  120. package/dist/session/keys.js.map +1 -0
  121. package/dist/session/session.d.ts +167 -26
  122. package/dist/session/session.d.ts.map +1 -1
  123. package/dist/session/session.js +1029 -171
  124. package/dist/session/session.js.map +1 -1
  125. package/dist/session/transaction.d.ts +81 -3
  126. package/dist/session/transaction.d.ts.map +1 -1
  127. package/dist/session/transaction.js +224 -37
  128. package/dist/session/transaction.js.map +1 -1
  129. package/dist/session/writer.d.ts +74 -0
  130. package/dist/session/writer.d.ts.map +1 -0
  131. package/dist/session/writer.js +294 -0
  132. package/dist/session/writer.js.map +1 -0
  133. package/dist/storage/indexeddb.d.ts +10 -2
  134. package/dist/storage/indexeddb.d.ts.map +1 -1
  135. package/dist/storage/indexeddb.js +91 -41
  136. package/dist/storage/indexeddb.js.map +1 -1
  137. package/dist/storage/jsonl/storage.d.ts +14 -2
  138. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  139. package/dist/storage/jsonl/storage.js +88 -24
  140. package/dist/storage/jsonl/storage.js.map +1 -1
  141. package/dist/storage/memory.d.ts +7 -2
  142. package/dist/storage/memory.d.ts.map +1 -1
  143. package/dist/storage/memory.js +28 -2
  144. package/dist/storage/memory.js.map +1 -1
  145. package/dist/storage/sqlite/storage.d.ts +14 -2
  146. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  147. package/dist/storage/sqlite/storage.js +110 -36
  148. package/dist/storage/sqlite/storage.js.map +1 -1
  149. package/dist/storage/store/commit.d.ts +33 -0
  150. package/dist/storage/store/commit.d.ts.map +1 -0
  151. package/dist/storage/store/commit.js +286 -0
  152. package/dist/storage/store/commit.js.map +1 -0
  153. package/dist/storage/store/documents.d.ts +43 -0
  154. package/dist/storage/store/documents.d.ts.map +1 -0
  155. package/dist/storage/store/documents.js +102 -0
  156. package/dist/storage/store/documents.js.map +1 -0
  157. package/dist/storage/store/index.d.ts +2 -0
  158. package/dist/storage/store/index.d.ts.map +1 -0
  159. package/dist/storage/store/index.js +2 -0
  160. package/dist/storage/store/index.js.map +1 -0
  161. package/dist/storage/store/queries.d.ts +49 -0
  162. package/dist/storage/store/queries.d.ts.map +1 -0
  163. package/dist/storage/store/queries.js +93 -0
  164. package/dist/storage/store/queries.js.map +1 -0
  165. package/dist/storage/store/storage.d.ts +59 -0
  166. package/dist/storage/store/storage.d.ts.map +1 -0
  167. package/dist/storage/store/storage.js +438 -0
  168. package/dist/storage/store/storage.js.map +1 -0
  169. package/dist/storage/store/tables.d.ts +54 -0
  170. package/dist/storage/store/tables.d.ts.map +1 -0
  171. package/dist/storage/store/tables.js +108 -0
  172. package/dist/storage/store/tables.js.map +1 -0
  173. package/dist/testing/commit-writes.d.ts +5 -0
  174. package/dist/testing/commit-writes.d.ts.map +1 -0
  175. package/dist/testing/commit-writes.js +12 -0
  176. package/dist/testing/commit-writes.js.map +1 -0
  177. package/dist/testing/env-conformance.js +7 -7
  178. package/dist/testing/env-conformance.js.map +1 -1
  179. package/dist/testing/index.d.ts +1 -0
  180. package/dist/testing/index.d.ts.map +1 -1
  181. package/dist/testing/index.js +1 -0
  182. package/dist/testing/index.js.map +1 -1
  183. package/dist/testing/storage-benchmark.d.ts.map +1 -1
  184. package/dist/testing/storage-benchmark.js +18 -17
  185. package/dist/testing/storage-benchmark.js.map +1 -1
  186. package/dist/testing/storage-conformance.d.ts.map +1 -1
  187. package/dist/testing/storage-conformance.js +354 -69
  188. package/dist/testing/storage-conformance.js.map +1 -1
  189. package/dist/tool-search/tool-search.d.ts +6 -12
  190. package/dist/tool-search/tool-search.d.ts.map +1 -1
  191. package/dist/tool-search/tool-search.js +7 -7
  192. package/dist/tool-search/tool-search.js.map +1 -1
  193. package/dist/tools/bash.d.ts +12 -12
  194. package/dist/tools/bash.d.ts.map +1 -1
  195. package/dist/tools/bash.js +16 -15
  196. package/dist/tools/bash.js.map +1 -1
  197. package/dist/tools/edit.d.ts +7 -7
  198. package/dist/tools/edit.d.ts.map +1 -1
  199. package/dist/tools/edit.js +9 -9
  200. package/dist/tools/edit.js.map +1 -1
  201. package/dist/tools/index.d.ts +32 -32
  202. package/dist/tools/read.d.ts +6 -6
  203. package/dist/tools/read.d.ts.map +1 -1
  204. package/dist/tools/read.js +19 -8
  205. package/dist/tools/read.js.map +1 -1
  206. package/dist/tools/write.d.ts +4 -4
  207. package/dist/tools/write.d.ts.map +1 -1
  208. package/dist/tools/write.js +5 -5
  209. package/dist/tools/write.js.map +1 -1
  210. package/dist/types.d.ts +223 -77
  211. package/dist/types.d.ts.map +1 -1
  212. package/dist/types.js.map +1 -1
  213. package/package.json +19 -12
  214. package/dist/harness/events.d.ts +0 -186
  215. package/dist/harness/events.d.ts.map +0 -1
  216. package/dist/harness/events.js +0 -373
  217. package/dist/harness/events.js.map +0 -1
  218. package/dist/session/observation.d.ts +0 -42
  219. package/dist/session/observation.d.ts.map +0 -1
  220. package/dist/session/observation.js +0 -281
  221. package/dist/session/observation.js.map +0 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Experimental.** The API changes without notice between releases.
4
4
 
5
- A durable agent harness. Conversations, model turns, tool calls, and your own state are committed to storage before anything is shown. If the process dies mid-turn, reopening the storage picks the work up where it stopped.
5
+ A durable agent harness. Conversations, model turns, tool calls, and your own state are committed to storage before anything is shown. If the process dies mid-turn, reopening the storage picks the work up where it stopped; a model request it died during counts as a failed attempt and is retried, so a request that keeps killing the process ends (see [Persist and Resume](#persist-and-resume)).
6
6
 
7
7
  Built on [`@earendil-works/pi-ai`](../ai/README.md) for model access and `@earendil-works/chord` for document state.
8
8
 
@@ -81,10 +81,10 @@ Every async call takes a Chord `Context`, which carries cancellation and its cur
81
81
 
82
82
  `HarnessOptions.telemetry` accepts a host-owned `TelemetryContext` from `@earendil-works/pi-telemetry`, with optional `propagation` and `metrics` capabilities; omitted, it is a no-op. `DURABLE_TELEMETRY_SCHEMA`, exported from the package root, documents its typed spans, events, and metrics. Attributes contain IDs and control metadata, never messages, arguments, or results; IDs point to the transcript.
83
83
 
84
- - `pi.durable.submit` covers the public submit call and its commit. Each input starts a new root trace with a link to its caller's current span; a write is a child of its caller. Duplicate `requestId` preserves the original submission and carrier. Internal admissions, such as compaction writes, emit events on their phase span without a submit span.
84
+ - `pi.durable.submit` covers the public submit call and its commit; for `submit(tx, draft)` it covers the admission only, its `result` is provisional until your commit is stored, and the commit's events go to your commit's context. Each input starts a new root trace with a link to its caller's current span; a write is a child of its caller. Duplicate `requestId` preserves the original submission and carrier. Internal admissions, such as compaction writes, emit events on their phase span without a submit span.
85
85
  - `pi.durable.phase` covers one run or abort phase of every task, including its decision commit, never the next phase, a durable wait, or a restart. Its attributes identify the task, phase, attempt when present, `resumed`, and outcome. Tool phases also identify the tool and call. Nested tasks and background compactions inherit the creating phase; model requests receive that phase context. Hooks add no spans.
86
86
  - Failed phase outcomes set error status with a fixed category (`task_faulted`, `task_failed`, or `task_orphaned`) and the outcome's diagnostic message, truncated to 500 characters (the reason for an orphaned task). Diagnostics can contain caller content; no error detail or exception name is recorded. An `aborted` outcome keeps status `ok`, including interruption by close or cancellation.
87
- - Every `onReport` report also emits a `pi.durable.report` event on its current span. Without a span, the NOOP span drops the event; no root report span is created. Only `type`, optional fixed `reason`, `extension`, and `task.id` are recorded; no error payload is included. `pi.durable.reports` counts all reports by `type` only, including those without a span. Clock failures report without a phase context. The `onReport` callback is unchanged.
87
+ - Every `onReport` report also emits a `pi.durable.report` event on its current span. Without a span, the NOOP span drops the event; no root report span is created. Only `type` (the report's `kind`), optional fixed `reason`, `extension`, and `task.id` are recorded; no error payload is included. `pi.durable.reports` counts all reports by `type` only, including those without a span. Clock failures report without a phase context. The `onReport` callback is unchanged.
88
88
  - Committed entries and settled submissions emit `pi.durable.entry` and `pi.durable.submission`. Committed running phase changes emit `pi.durable.checkpoint`; a nested call answered from its stored result emits `pi.durable.replay`.
89
89
  - Propagation stores optional opaque `trace` carriers in submission and task JSON without a storage migration. Phase spans extract their task's stored carrier, or start at the adapter root when absent or unknown. `TaskOptions.trace` inherits the creator's carrier when absent, accepts an explicit carrier override, or uses `"root"` to start at the adapter root without storing a carrier. A run started for several queued inputs takes the first input's trace; its first invocation's phase spans link to the other inputs through immutable task `links`. A steer stays in the running trace and its submit span links to that run's stored carrier. A reopened task's first invocation has `resumed: true` in the same trace.
90
90
  - Metrics record `pi.durable.phase.duration` (milliseconds; task, phase, outcome) and `pi.durable.tasks.active` (live tasks; task name), without record IDs. Closing the Harness releases its active counts.
@@ -124,7 +124,7 @@ const root = await harness.root(context); // the same root as last time
124
124
  harness.resume(); // continue any run the last process left unfinished
125
125
  ```
126
126
 
127
- Work interrupted by a crash or close stays pending. `resume()` starts the task scheduler; submitting or waiting starts it too. Each conversation has its own persisted UUIDv7 in `pi.provider`, forwarded to pi-ai as `sessionId` for provider prompt-cache and session affinity. It survives reopen, retries, reset, compaction, and model changes; a child or fork receives a fresh identity. A legacy conversation receives and persists one before its first generation or compaction request.
127
+ Work interrupted by a crash or close stays pending. `resume()` starts the task scheduler; submitting or waiting starts it too. A process that only shows a session another one drives opens it with `readOnly: true`: open recovers nothing, and every write, `resume()`, and every call that would run tasks rejects with `ReadOnlySession`. Each conversation has its own persisted UUIDv7 in `pi.provider`, forwarded to pi-ai as `sessionId` for provider prompt-cache and session affinity. It survives reopen, retries, reset, compaction, and model changes; a child or fork receives a fresh identity. A legacy conversation receives and persists one before its first generation or compaction request.
128
128
 
129
129
  A retried submission with the same `requestId` returns the existing submission instead of submitting twice:
130
130
 
@@ -213,11 +213,11 @@ The default `setTimer` declines the timers that only free memory in Workers, so
213
213
 
214
214
  ### Failures and Recovery
215
215
 
216
- **Crash loops.** Work that deterministically crashes its process, by exhausting memory or CPU before its next checkpoint, would otherwise be resumed and crash again forever. Open counts each task a crash, eviction, or reset cut off; a close is not counted, and a new state the task commits starts the count over. Once the count reaches `settings.restartLimit` (default 5; `Infinity` never parks), the task is parked: `inspect()` and the task graph show it as blocked with reason `restart_limit`, `wake()` does not count it as active, and `onReport` gets it once as `task_parked`. Every task running when a process dies is counted, so show parked tasks and offer `harness.retryTask(id)`, which runs one again. `abortTask()` still runs its abort handler; an abort handler cut off that many times ends the task `faulted`.
216
+ **Crash loops.** Work that deterministically crashes its process, by exhausting memory or CPU before its next checkpoint, would otherwise be resumed and crash again forever. A model request the host died during is a failed attempt under `settings.retry`: its partial answer is kept as an aborted entry, the next attempt waits for the backoff (shown in `docs["pi.live"].generation.retry`), and with no retry left the run ends with `docs["pi.live"].runFailure` reason `interrupted`. A deferred response that was being fetched is polled again as the next attempt, not requested again, and cancelled when the run ends. A close is not a crash: a request it stopped is sent again. Other work is counted: open counts each task a crash, eviction, or reset cut off, and a new state the task commits starts the count over. Once the count reaches `settings.restartLimit` (default 5; `Infinity` never ends a task for it), the task ends instead of running again: a tool call with an error result (diagnostic code `restart_limit`), a compaction failed into `compactionFailure`, a run with `runFailure` reason `restart_limit` (also when charging an interrupted request keeps crashing), and a task of your own kinds `faulted`, reported as `task_faulted` with reason `restart_limit`. The built-in ones are reported as `task_interrupted`, as is a run that ends `interrupted`, so a crash loop always reaches `onReport`. Every task running when a process dies is counted, not only the one that killed it. An abort handler cut off that many times ends its task `faulted` too; a run someone aborted still ends `aborted`, with the cut-off in `detail`.
217
217
 
218
- **Reports.** `onReport(error, info)` says what happened in `info.type`: `session_failed` for the error that failed the Harness (a Storage error, often a platform reset); `task_faulted`, `task_parked`, `migration_failed`, `task_outdated`, or `task_error` for a task, with `info.task` (its `id`, `kind`, and `conversationId`) and the extension that installed it in `info.extension`; `extension_error` for hooks, sections, wrappers, and tools, with the extension's name; `host_error` for a host callback, named in `info.callback`; and `listener_error`. A host can feed `task_faulted` reports of one kind to its own breaker.
218
+ **Reports.** `onReport(report)` gets one `Report`: the `error` and what happened in `report.kind`: `storage_failed` for the error that failed the Harness (a Storage error, often a platform reset); `task_faulted`, `task_interrupted` (crashes ended it, with `report.reason` and `report.restarts`), `migration_failed`, `task_outdated` (`reason` `missing_task`, `task_too_old`, or `migration_failed`, the words a blocked task shows), or `task_error` for a task, with `report.task` (its `id`, `kind`, and `conversationId`) and the extension that installed it in `report.extension`; `extension_error` for hooks, sections, wrappers, and tools, with the extension's name; `host_error` for a host callback, named in `report.callback`; and `listener_error`. A host can feed `task_faulted` reports of one kind to its own breaker.
219
219
 
220
- **Queued inputs after a failed run.** A run that fails with a model error leaves the inputs queued behind it waiting until the next input arrives. `settings.afterFailedRun` changes that: `continue` starts the next run with them, as after an answer, and `withdraw` settles them `unanswered` with reason `run_failed`.
220
+ **Queued inputs after a failed run.** A run that fails with a model error, a missing model, nothing to answer, its turn limit, or crashes (`interrupted`, `restart_limit`) leaves the inputs queued behind it waiting until the next input arrives. `settings.afterFailedRun` changes that: `continue` starts the next run with them, as after an answer, and `withdraw` settles them `unanswered` with reason `run_failed`.
221
221
 
222
222
  **Blocked tasks.** Tasks blocked on a missing or unmigratable definition show their reason in the task graph too, and a registry change that unblocks them makes `onWake` report the Session active, so a host need not poll them.
223
223
 
@@ -257,15 +257,15 @@ registry.install(createCodingTools({ images: await createNodePhotonImages() }));
257
257
 
258
258
  A model without image input sees a placeholder instead of the image, and `read` says so in a diagnostic. `/images/node` prepares images in a worker thread, so a large photo (a second or two of work) never blocks the event loop; the worker exits after 30 s without images, freeing Photon's WebAssembly memory (`createNodePhotonImages({ worker: false, idleMs })`). Elsewhere, `createPhotonImages()` and `/images/cloudflare` work on the calling thread, and decoding takes the image's full size in WebAssembly memory, which does not shrink again; leave the processor out on memory-constrained hosts such as Cloudflare Workers unless images stay small.
259
259
 
260
- Define your own tool with a TypeBox schema. `defineTool()` types `args` from `parameters`, which the Harness validates before `execute()`. `api.output()` streams running output, which becomes the result when `execute()` returns no `output`:
260
+ Define your own tool with a JSON Schema, built here with `@earendil-works/pi-schema`. `defineTool()` types `args` from `parameters` (through pi-schema's `Static`; other JSON Schema gives `unknown`), which the Harness coerces and validates before `execute()`. `api.output()` streams running output, which becomes the result when `execute()` returns no `output`:
261
261
 
262
262
  ```typescript
263
- import { Type } from "@earendil-works/pi-ai";
263
+ import { num, obj, str } from "@earendil-works/pi-schema";
264
264
 
265
265
  const count = defineTool({
266
266
  name: "count",
267
267
  description: "Count from 1 to n",
268
- parameters: Type.Object({ n: Type.Number() }),
268
+ parameters: obj({ n: num() }),
269
269
  execute: async (args, api) => {
270
270
  for (let i = 1; i <= args.n; i++) api.output(`${i}\n`);
271
271
  return {};
@@ -274,7 +274,7 @@ const count = defineTool({
274
274
  registry.install(defineExtension({ name: "count", tools: [count] }));
275
275
  ```
276
276
 
277
- Each call runs as its own durable task. Its intent is committed before `execute()` runs. If the process dies mid-call, the tool reruns on reopen only when it is declared `replay: "safe"`; otherwise the model gets an `interrupted` error result with the output committed so far. Throwing from `execute()` gives the model an error result. A result can also return `usage`, which is added to the conversation's [usage](#usage-and-cost). It can also return `control: { terminate: true }`: when every result of the round asks for it, the run ends without another model request.
277
+ Each call runs as its own durable task. Its intent is committed before `execute()` runs. If the process dies mid-call, the tool reruns on reopen only when it is declared `replay: "safe"`; otherwise the model gets an `interrupted` error result with the output committed so far. A replay-safe call cut off `settings.restartLimit` times in a row ends with a `restart_limit` error result instead of running again. Throwing from `execute()` gives the model an error result. A result can also return `usage`, which is added to the conversation's [usage](#usage-and-cost). It can also return `control: { terminate: true }`: when every result of the round asks for it, the run ends without another model request.
278
278
 
279
279
  A result has three channels: `output` for the model, `structuredOutput` for programs that call the tool (see [Calling tools from tools](#calling-tools-from-tools)), and `details` for UIs. A tool that declares `structuredOutputSchema` returns a matching `structuredOutput`, which the Harness validates. Programs calling a tool without one get its bounded output: one text item as a string, one image as its `ImageContent`, nothing as `""`, and anything else as the content list, for errors too. `structuredOutput` is always the tool's own value: a schema tool's error result keeps the value it returned, and the text of one without is in a `tool_error` diagnostic, as a thrown error's message is. The transcript stores only `output` and `details`:
280
280
 
@@ -282,8 +282,8 @@ A result has three channels: `output` for the model, `structuredOutput` for prog
282
282
  const lines = defineTool({
283
283
  name: "lines",
284
284
  description: "Count the lines of a file",
285
- parameters: Type.Object({ path: Type.String() }),
286
- structuredOutputSchema: Type.Object({ lines: Type.Number() }),
285
+ parameters: obj({ path: str() }),
286
+ structuredOutputSchema: obj({ lines: num() }),
287
287
  execute: async (args, api, context) => {
288
288
  const text = await readText(api, args.path, context);
289
289
  const count = text.split("\n").length;
@@ -292,7 +292,7 @@ const lines = defineTool({
292
292
  });
293
293
  ```
294
294
 
295
- A missing, undeclared, or invalid `structuredOutput` turns a nested call's result into an error result with an `invalid_structured_output` diagnostic; for a call the model made, which never sees it, it is dropped and reported through `onReport`. An error result may omit it, and programs then find its text in a `tool_error` diagnostic; error results the Harness writes itself (blocked, invalid, interrupted, aborted) carry none, and their `diagnostics` say what went wrong. An `afterTool` hook that redacts a schema tool's `output` must redact its `structuredOutput` too. The built-in `bash` returns `{ output, truncated, fullOutputPath?, exitCode }`, where `output` is the retained tail the model sees, read with `api.retainedOutput()`, and answers a nonzero exit with an error result that still carries it.
295
+ A missing, undeclared, or invalid `structuredOutput` turns a nested call's result into an error result with an `invalid_structured_output` diagnostic; for a call the model made, which never sees it, it is dropped and reported through `onReport`. An error result may omit it, and programs then find its text in a `tool_error` diagnostic; error results the Harness writes itself (blocked, invalid, interrupted, aborted) carry none, even when an `afterTool` hook returns one, and their `diagnostics` say what went wrong; one a hook turns into a success gives a schema-less tool's program its output. An `afterTool` hook that redacts a schema tool's `output` must redact its `structuredOutput` too. The built-in `bash` returns `{ output, truncated, fullOutputPath?, exitCode }`, where `output` is the retained tail the model sees, read with `api.retainedOutput()`, and answers a nonzero exit with an error result that still carries it. The built-in `read` returns a text file's text as a string and an image as its `ImageContent`, which code mode's `image()` shows.
296
296
 
297
297
  A later extension's tool with the same name replaces an earlier one where both are selected, and `wrapTool()` decorates whichever tool won:
298
298
 
@@ -312,7 +312,7 @@ A tool calls another tool with `api.executeTool()`, as a code mode script or an
312
312
  const testsPass = defineTool({
313
313
  name: "tests_pass",
314
314
  description: "Run the test suite and say whether it passed",
315
- parameters: Type.Object({}),
315
+ parameters: obj({}),
316
316
  execute: async (_args, api, context) => {
317
317
  const run = await api.executeTool("bash", { command: "npm test" }, context, { progress: false });
318
318
  // Absent when bash did not finish, for example on a timeout.
@@ -324,7 +324,7 @@ const testsPass = defineTool({
324
324
 
325
325
  - The caller gets the nested call's `taskId`, `structuredOutput`, `details`, `diagnostics`, `usage`, and `isError`, not the `output` meant for the model. A blocked, invalid, failed, or aborted nested call returns an `isError` result. `executeTool()` itself rejects once `execute()` has returned, when the caller is aborted, or when the Harness closes; cancelling its `context` stops only the wait.
326
326
  - Hooks see a nested call's `call.parent`, so a guard can treat calls from scripts differently.
327
- - While the caller runs, its nested calls show in `docs["pi.live"].nestedTools`, with the arguments they run with and a `summary` (error, duration, usage) once done, and as tool events with `parentToolCallId` and `parentTaskId`. `progress: false` keeps a nested call's running output out of `pi.live`. Before the caller settles, nested calls it left running are aborted, also those an earlier attempt made. The caller's result message does not list its nested calls; their task records keep them.
327
+ - While the caller runs, its nested calls show in `docs["pi.live"].nestedTools`, with their caller's `parentCallId` and `parentTaskId`, the arguments they run with, and a `summary` (error, duration, usage) once done; a nested call's full result is in its caller's `NestedCallDoc` while the caller runs. `progress: false` keeps a nested call's running output out of `pi.live`. Before the caller settles, nested calls it left running are aborted, also those an earlier attempt made. The caller's result message does not list its nested calls; their task records keep them.
328
328
  - Each nested call has a key, its position among the caller's nested calls unless the caller passes `{ key }`. A replay-safe caller that reruns after a crash and calls in the same order, or passes the same keys, gets the nested calls it already made back, finished or still running, instead of starting them again. A nested call the crash interrupted before its result was committed follows its own `replay` policy, so a replay-safe tool with external effects still needs its own idempotency. A caller that is not replay-safe never reruns, so after a restart its unfinished nested calls are abandoned, with everything they own, before they run again.
329
329
  - A nested call's `usage` is counted under its own tool; passing it on in the caller's result counts it twice.
330
330
  - Nested results are not kept: they live in the caller's task documents (`NestedCallDoc`, one per nested call, keyed by its key, holding `{ taskId, result? }`) only until the caller settles. The nested call's task record keeps only a small receipt. A stored result is exactly what `executeTool()` returns, without the model's `output`, so an image or a `bash` tail is stored once.
@@ -391,6 +391,7 @@ await root.configure(
391
391
  modelTools: [readTool], // of those, offer the model only read; tools may still call bash
392
392
  instructions: "Only read; never edit files.",
393
393
  cwd: "/work/repo",
394
+ maxTurns: 50, // tool rounds one run takes at most; unset: no limit
394
395
  },
395
396
  context,
396
397
  );
@@ -400,6 +401,8 @@ const agent = await root.agent(context); // resolved: model, extensions, tools,
400
401
 
401
402
  Extensions and tools are passed as objects and stored by name, so a stored name outlives its code: after an extension is uninstalled, conversations that select it just stop getting it until it is installed again. `createConversation()`, `fork()`, and `root()` take the same change as `agent`. A task-owned conversation, such as a subagent's, starts as a copy of its owner's conversation's agent. A fork starts with the agent its parent had at the fork entry. The model, prompt, and offered tools of a request are fixed when it is prepared; a change applies from the next request. Tool calls and hooks use the agent as their task phase resolves it, and the environment is built from the current `cwd` at each use, so a `cwd` or extension change can reach calls the model already made.
402
403
 
404
+ **Turn limits.** `maxTurns` bounds the tool rounds one run takes: the round that reaches it still runs its tools, then the run ends `unanswered` with `turn_limit`, like a failed run (`runFailure`, and `settings.afterFailedRun` decides the inputs queued behind it). An answer always ends the run, so it is never cut off. The count is `docs["pi.live"].run.turns`; it survives restarts, and every run starts at 0, also one a continue starts (see [Busy Conversations](#busy-conversations)), so a host or tool that continues automatically gets a fresh budget each time and must bound that itself. An `onYield` continuation also starts the count over: an extension that keeps a run going bounds its own loop, reading `pi.live.run.turns` and `pi.agent.maxTurns` with `api.snapshot`. `configure()` throws a `RangeError` for anything but an integer 1 or more (or `null`); a change applies at the end of the current round, and children and forks inherit it like the rest of the agent.
405
+
403
406
  ## Settings
404
407
 
405
408
  Run policy shared by every conversation is passed as `settings`. It is read at every use and never stored, so getters make it live, for example backed by a settings file:
@@ -413,7 +416,7 @@ const harness = await Harness.open(storage, {
413
416
  stream: { timeoutMs: 120_000 },
414
417
  retry: { maxRetries: 3 },
415
418
  compaction: { reserveTokens: 16384 },
416
- progress: { partialIntervalMs: 100, outputIntervalMs: 100 },
419
+ progress: { partialIntervalMs: 100, outputIntervalMs: 100, persistWithinMs: 1_000 },
417
420
  toolExecution: "parallel",
418
421
  contextRetentionMs: 600_000, // idle conversations keep their context in memory this long (checked lazily where background timers are declined, as in Workers)
419
422
  documentRetentionMs: 600_000, // loaded documents nothing observes stay in memory this long after their last use; observed ones always
@@ -473,33 +476,55 @@ Work that already started keeps the code it took: a running tool call finishes u
473
476
 
474
477
  ## Watching a Conversation
475
478
 
476
- Everything a UI needs is committed state. `viewState()` returns the conversation's structural view as a read-only Chord state, updated after every commit that touches it:
479
+ Everything a UI needs is committed state. `watch()` returns a watch of the conversation's structural view: its value at attachment, then each commit that touches it, with the Chord operations of that commit, one callback at a time:
477
480
 
478
481
  ```typescript
479
- const view = await root.viewState(context);
480
- view.subscribe((value) => {
481
- // value.entries: the active transcript
482
- // value.docs["pi.live"]: the running generation (streamed partial, retry, deferred) and tool calls (output, details)
482
+ const watch = await root.watch(context);
483
+ render(watch.value); // the state at attachment
484
+ watch.start(async (call) => {
485
+ if (call.kind !== "value") return; // Durable's watches end on a failure; they never send a failed call
486
+ const { value, ops } = call;
487
+ // value.entries: a window over the transcript, newest last; value.before: the entry just before it
488
+ // value.docs["pi.live"]: the run, the running generation (streamed partial, retry, deferred), tool calls (output,
489
+ // details), compactions, and why the last run ended without an answer (runFailure)
483
490
  // value.docs["pi.inbox"], value.docs["pi.usage"], value.docs["pi.agent"], value.docs["pi.provider"]
484
491
  render(value);
492
+ await send(ops); // for example to a remote client that applies them
485
493
  });
486
- // later: view.dispose();
494
+ // later: await watch.stop();
487
495
  ```
488
496
 
489
- `watch()` delivers the same view with the exact Chord operations of each commit, one callback at a time:
497
+ The view's `entries` are a window, so neither the first value nor a reconnect costs the whole transcript. A watch starts with the newest 50 entries; new entries are appended, and once the window would hold more than 100, its oldest leave in one splice, leaving the newest 50. `watch(context, { min, max })` changes the bounds. `from` picks where the first value starts (as `transcript.resume` does after a drop); every entry committed after the watch attached is appended, so a `from` past the newest entry starts empty and receives what comes next. Compaction summaries and resets are entries like any other, in place; a summary's `head` says where the entries it summarizes end. `conversation.context()` gives the active transcript the model sees.
498
+
499
+ `ConversationTranscript` keeps what a client has seen beyond the window: call `merge()` with every value of the view, and render the `items` it returns, the client's whole transcript with a "load earlier" marker at the top and a "load more" gap wherever the client missed entries, after a stall or a reconnect:
490
500
 
491
501
  ```typescript
502
+ import { ConversationTranscript } from "@earendil-works/pi-durable";
503
+
504
+ const transcript = new ConversationTranscript((query, limit, ctx) => root.entries(query, limit, undefined, ctx));
492
505
  const watch = await root.watch(context);
493
- render(watch.value); // the state at attachment
494
- watch.start(async (value, ops) => {
495
- await send(ops); // for example to a remote client that applies them
506
+ render(transcript.merge(watch.value));
507
+ watch.start(async (call) => {
508
+ if (call.kind === "value") render(transcript.merge(call.value));
496
509
  });
497
- // later: await watch.stop();
510
+
511
+ // When a marker scrolls into view:
512
+ render(await transcript.loadEarlier(context)); // the page before the oldest entry
513
+ render(await transcript.fillGap(gap, context)); // the next page of a gap item
514
+
515
+ // Reconnect after a drop: only what was missed, when it fits the window, otherwise the newest entries and a gap.
516
+ const resumed = await root.watch(context, transcript.resume);
498
517
  ```
499
518
 
500
- A slow watch keeps at most 100 undelivered frames. After that, the pending frames are replaced by one frame holding the whole newest view. A client that joins late or reconnects starts from the current view; nothing is replayed.
519
+ Entries never change once committed, so a merge only adds; one that adds nothing returns the same `items` array. See `test/examples/31-transcript.ts`.
520
+
521
+ Watches are Chord's `WatchHandle` (exported by `@earendil-works/chord`; Durable re-exports it). The listener gets one `WatchCall` per call, tagged by `kind`, and the newest commit's context: `{ kind: "value", value, ops, retired }`. Chord's watches also have `{ kind: "failed", error }` calls for failures a source recovers from; Durable's failures are final, so a Durable watch never sends one and ends instead. A watch ends once, and `closed` (or `stop()`) gives a `WatchEnd` tagged by `kind`: `stopped`, `aborted` (its acquisition context was aborted), `ended` (the Harness closed, or a task document's task ended), `listener_error`, or `failed` (the Harness failed). An ended watch keeps its last value and no longer references the Harness.
501
522
 
502
- Partial answers and tool output are committed at most every 100 ms by default, so a crash loses at most that window. `settings.progress` changes the intervals; a host whose storage is remote can commit less often, for example `{ partialIntervalMs: 500, outputIntervalMs: 500 }`.
523
+ Adapters that must change in the same step as Durable's memory, such as a Chord source over a view, attach a synchronous observer instead of a watch: `harness.observeConversation(id, observer, context)`, `harness.observeTaskGraph(observer, context)`, and `harness.observeDoc(token, ...scope, observer, context)` return the current `value` and `detach()`, and call `observer.advance(call, context)` with every later change as a `WatchCall` (always a `value` call), unfolded, as memory adopts it: inside the storage's commit for a stored commit, at once for a deferred one (before it is stored). A document observer gets one call per change at its address, so a commit that retires and recreates the document gives two; the task graph also advances without a commit when a registry change alters a task's `blocked` reason. A change can arrive before the `observeXXX()` promise resolves; its ops apply from the observation's `value`. `observer.end(end)` comes once with a `WatchEnd`: `ended` when the Harness closes or, for a task document, right after the retirement of the commit that ends its task, `failed` when the Harness fails. `advance` only records the change: no awaits, no Harness calls, no throws, and quick, since every commit waits for it. One that throws (or returns a rejected promise) is reported, detached, and ended `listener_error`. Watches are built on these observers.
524
+
525
+ Commits that land while a callback runs are delivered together in the next callback: the newest view and the operations of all of them, folded (appends to one string join, sets of one field keep the last). Past 10,000 pending operations the callback gets one root replacement `[["r", view]]` holding the whole newest view instead, its window included. A client that joins late or reconnects starts from the current view; nothing is replayed.
526
+
527
+ Partial answers and tool output are committed at most every 100 ms by default, and watches see each commit at once. Storage gets them later and together, as deferred commits (see [Your Own State](#your-own-state)): with the next commit that reads `pi.live`, such as the end of the answer or of a tool, and at the latest after `settings.progress.persistWithinMs` (default 1,000 ms). A crash loses at most about that window of an answer or of tool output; a stream that crashes in its first second may leave no aborted entry, and that attempt's usage is not recorded. `settings.progress` changes the intervals; a host whose storage is remote can commit less often, for example `{ partialIntervalMs: 500, outputIntervalMs: 500 }`.
503
528
 
504
529
  ## Busy Conversations
505
530
 
@@ -509,16 +534,45 @@ A conversation is busy while a run is working on an input. Submitting to a busy
509
534
  await root.submit({ type: "input", content: "Also run the tests" }, context); // follow-up (default)
510
535
  await root.submit({ type: "input", content: "Use pnpm, not npm", whenBusy: "steer" }, context);
511
536
  await root.submit({ type: "input", content: "Only if idle", whenBusy: "reject" }, context); // throws ConversationBusy
537
+ await root.submit({ type: "input", content: "Only if busy", whenBusy: "steer", whenIdle: "reject" }, context); // throws ConversationIdle when idle
512
538
  await root.submit({ type: "write", entry: { kind: "app.note", data: "user opened a file" } }, context);
513
539
  ```
514
540
 
515
541
  - **Steers** are placed after the current tool round and join the running work.
516
542
  - **Follow-ups** are placed when the run answers, and start the next run.
543
+ - **`whenIdle: "reject"`** refuses an input when no run is working, so it never starts a run of its own. With `whenBusy: "steer"`, it steers the current run or throws `ConversationIdle`. A run that answered and waits for its [after-run tasks](#work-at-the-end-of-a-run) is still working, so such an input queues and starts the next run.
544
+ - **`submit(tx, draft)`** admits inside your own commit, with the same rules, and returns the submission's ID. Your checks, the admission, and your writes are stored together or not at all:
545
+
546
+ ```typescript
547
+ const id = await harness.commit(async (tx) => {
548
+ const chat = await tx.doc(MyChat, root.id); // your own conversation document
549
+ if (chat.muted.includes(userId)) throw new Error("Muted");
550
+ const id = await root.submit(tx, { type: "input", content: text, whenBusy: "steer", whenIdle: "reject" });
551
+ chat.sent.push({ id, userId }); // your own state, same commit
552
+ return id;
553
+ }, context);
554
+ ```
555
+
556
+ Call it before the commit's own table writes (`appendEntry()`, `createTask()`, another admission), once per commit: otherwise it throws `ReadAfterWrite`, on every path. Documents may be read and written before and after it. A rejection (`ConversationBusy`, `ConversationIdle`) throws before any write, so you may catch it. Until your commit is stored the admission is provisional: if your callback throws later, no submission exists. Tasks and tools do the same in their own commits through `api.conversation(id)`; entries the admission places carry the task's `byTaskId`.
517
557
  - **Writes** append an entry without asking the model anything. A write of only `kind` and `data` changes nothing the model sees, so it is appended at once, even mid-run, and is never queued: a chat message shows in the transcript as soon as it is sent. It can therefore appear before items submitted earlier that are still queued, except behind a queued reset, where it waits so the reset does not hide it. A write with `model`, `head`, or `edits` waits for the next boundary like a steer.
518
558
  - `await submission.abort(context)` withdraws a queued submission.
519
559
  - The [settings](#settings) `steeringMode: "all"` and `followUpMode: "all"` place every queued item at once instead of one per turn.
520
560
 
521
- If a run fails, queued items stay in the inbox until the next input or queued write places them, oldest first.
561
+ If a run fails, queued items stay in the inbox until the next input, queued write, or continue places them, oldest first.
562
+
563
+ When a run ends without an answer, `docs["pi.live"].runFailure` says why until the next run starts: `model_error` and `no_model` (with the error text or the missing model in `detail`), `aborted`, `reset`, `interrupted` (the host stopped while the model answered, with no retry left), `faulted`, `orphaned`, and `restart_limit` (the run's task could not go on), `turn_limit` (a tool round reached the conversation's `maxTurns`), or `nothing_to_answer` (a continue, below, found nothing to answer). The run's inputs are in it too, settled `unanswered`; the settlement's reason is the same, except that an orphaned run's inputs carry why it was orphaned (`missing_task`, `task_too_old`, `migration_failed`) and `no_model` has no `detail` there (spec §8.2).
564
+
565
+ To resume such a run without typing a message, submit a continue. It starts a run on the context as it is, so the model answers the user message or tool results the stopped run left:
566
+
567
+ ```typescript
568
+ const live = await harness.snapshot(LiveDoc, root.id, context);
569
+ if (live?.run === undefined && live?.runFailure !== undefined) {
570
+ const resumed = await root.submit({ type: "continue" }, context); // throws ConversationBusy while busy
571
+ await resumed.wait(context); // done with the answer, or unanswered like any run
572
+ }
573
+ ```
574
+
575
+ A continue is never queued: it throws `ConversationBusy` while a run is working, since whether there is anything to continue depends on how that run ends. Items a failed run left queued are placed first and join the run. A context with nothing to answer (empty, or ending in an answer, also a text answer cut off at its output limit) ends the run without a request, `unanswered` with `nothing_to_answer`. A cut-off answer with tool calls can be continued: its calls never ran, so the context ends in their error results. Tools can continue the conversations they own through `api.conversation(id)`, as they submit to them.
522
576
 
523
577
  An input is placed as a `pi.user` entry. To show it as something else, give it an `entry` with your kind and data; the model still sees `content` as the user message:
524
578
 
@@ -545,7 +599,9 @@ await root.reset(undefined, context); // start
545
599
  await root.reset("We were fixing the flaky login test. Continue.", context); // start from a handoff note
546
600
  ```
547
601
 
548
- While busy, the reset is queued like a write. When it is placed during a tool round, the current run ends. A tool can request the same with `control: { handoff: "..." }`.
602
+ While busy, the reset is queued like a write. When it is placed during a tool round, the current run ends: its inputs settle `unanswered` with reason `reset`, and queued inputs run in the new context.
603
+
604
+ A tool can return `control: { handoff: "..." }` to write the same reset with its text, but the run continues: the next model request starts in the new context, and the submission settles with that eventual answer. Queued writes and steers land after the handoff text, follow-ups wait for the run's end, and a queued `reset()` still ends the run. When every call of the round also returns `terminate: true`, the run ends after the handoff instead.
549
605
 
550
606
  ## Compaction
551
607
 
@@ -585,23 +641,7 @@ When a provider rejects a request because the context is too long, generation co
585
641
 
586
642
  When a compaction that a generation waits for at the blocking threshold fails, the generation fails too, without sending a request: its inputs settle `unanswered` with `model_error` and detail `Compaction failed: <error>`. One that finds nothing to compact or is declined lets the request go ahead.
587
643
 
588
- Running compactions are listed in `docs["pi.live"].compactions` with their reason, attempt, and retry backoff. The newest failure stays in `docs["pi.live"].compactionFailure` (task, reason, error) until a compaction produces a summary. The agent events add `compaction_start` and `compaction_end` (with `error` when the compaction failed), and `compactions` and `compactionFailure` fields in the snapshot.
589
-
590
- ## Agent Events (Experimental)
591
-
592
- For consumers that want coding-agent style events (`message_start`, `message_update`, `tool_execution_start`, ...) instead of structural state:
593
-
594
- ```typescript
595
- import { watchEvents } from "@earendil-works/pi-durable";
596
-
597
- const stream = await watchEvents(harness, root.id, context);
598
- initialize(stream.snapshot); // entries, run, in-flight generation, tools, compactions, inbox, agent, usage
599
- stream.start(async (events) => {
600
- for (const event of events) console.log(JSON.stringify(event));
601
- });
602
- ```
603
-
604
- Events are derived from commits, one batch per commit, and apply on top of the snapshot. Message and tool updates carry deltas: text and thinking appends, appended tool-call argument text, and output trims and appends. When a consumer falls more than 100 batches behind, it receives a fresh `snapshot` event instead. See `test/examples/19-json.ts` for the full stream of one run.
644
+ Running compactions are listed in `docs["pi.live"].compactions` with their `trigger` (`manual`, `threshold`, or `overflow`), attempt, and retry backoff. The newest failure stays in `docs["pi.live"].compactionFailure` (task, trigger, error) until a compaction produces a summary.
605
645
 
606
646
  ## Hooks
607
647
 
@@ -619,11 +659,47 @@ const Guard = defineExtension({
619
659
  });
620
660
  ```
621
661
 
622
- - **Generation:** `beforeRequest` (replace the messages of one request), `afterResponse`, `onYield` (continue the run with another user message), and `afterTools` (runs once a round's tools are done).
623
- - **Tools:** `beforeTool` (block or rewrite arguments) and `afterTool` (replace the result).
662
+ - **Generation:** `beforeRequest` (replace the messages of one request), `beforeProviderRequest` (replace the provider request body of one request, e.g. to set a provider-specific field), `afterResponse`, `onYield` (continue the run with another user message), `afterTools` (runs once a round's tools are done), and `afterRun` (tasks to run before the run ends, below).
663
+ - **Tools:** `beforeTool` (block or rewrite arguments) and `afterTool` (replace the result). `afterTool` sees every result a tool task writes, also the error results the Harness writes itself (unavailable, invalid, blocked, interrupted, restart limit, aborted), told apart by their diagnostic code; past `settings.restartLimit` it no longer runs. On an abort it runs inside the abort handler, so a slow hook delays `abort()`.
624
664
 
625
665
  To limit a hook to some conversations, select its extension only there, for example with `configure({ extensions: { add: [Guard] } })`.
626
666
 
667
+ Hooks read committed documents (`api.snapshot()`) and the conversation's raw history (`api.entry()`, `api.entries()`), which compaction never changes.
668
+
669
+ ### Work at the end of a run
670
+
671
+ An `afterRun` hook starts durable work after a run answered, without a tool call: for example the project's tests after a run that edited files. It returns tasks, built with `runEndTask()`, and the generation creates them as its children and waits for them before the run ends:
672
+
673
+ ```typescript
674
+ import { defineExtension, GenerationTask, hook, runEndTask, ToolResultEntry } from "@earendil-works/pi-durable";
675
+
676
+ const TestsAfterEdits = defineExtension({
677
+ name: "tests-after-edits",
678
+ tasks: [RunTests], // a task of yours, defineTask(): runs the tests, then commits its result
679
+ hooks: [
680
+ hook(GenerationTask, {
681
+ afterRun: async (run, api, context) => {
682
+ // The run's own entries, also those a compaction summarized.
683
+ const range = { minEntryId: run.first, maxEntryId: run.last, order: "ascending" } as const;
684
+ const { items } = await api.entries(range, 1000, undefined, context); // page with `next` for longer runs
685
+ const edited = items.some((entry) => {
686
+ const message = entry.model?.[0];
687
+ return ToolResultEntry.is(entry) && message?.role === "toolResult" && message.toolName === "edit";
688
+ });
689
+ return edited ? { tasks: [runEndTask(RunTests, { command: "npm test" })] } : undefined;
690
+ },
691
+ }),
692
+ ],
693
+ });
694
+ ```
695
+
696
+ - **When:** after a final answer or a `terminate` round, not while an `onYield` continuation keeps the run going, never after a failure or an abort.
697
+ - **The answer first:** the answer is committed and the run's inputs settle `done` before the hooks are asked, so `Submission.wait()` resolves at the answer.
698
+ - **Busy until done:** `pi.live.run` stays, with `run.afterRun` listing the tasks, so the conversation is busy and `waitForIdle()` includes them; Stop aborts them; a crash resumes them. A crash before they exist asks the hooks again, never twice once they do.
699
+ - **Input meanwhile:** steers and follow-ups queue and start the next run once the tasks are done; `whenBusy: "reject"` and a continue throw `ConversationBusy`.
700
+ - **Results for the model:** a task gets its conversation's handle before its commit, `const handle = (await runtime.conversation(runtime.conversationId, context))!`, and admits a write or a follow-up (say "the tests failed: ...") inside it with `handle.submit(tx, draft)`, before its own table writes. Both are placed when the run ends: a write before any queued user input, a follow-up in queue order with the rest. A follow-up starts a fix run, which asks `afterRun` again with a fresh turn budget, so cap such loops in your extension.
701
+ - **Broken requests** (an uninstalled or built-in task, a non-JSON input, a throwing `initial()`) are reported through `onReport` against the hook's extension and skipped; the run still ends.
702
+
627
703
  ## More Conversations and Forks
628
704
 
629
705
  ```typescript
@@ -646,7 +722,7 @@ const Subagent: Extension = defineExtension({
646
722
  defineTool({
647
723
  name: "subagent",
648
724
  description: "Delegate a self-contained task to a subagent and get its answer back.",
649
- parameters: Type.Object({ task: Type.String() }),
725
+ parameters: obj({ task: str() }),
650
726
  replay: "safe", // a rerun after a crash finds the same child and submission
651
727
  execute: async (args, api, context) => {
652
728
  const child = await api.commit(async (tx) => {
@@ -677,8 +753,8 @@ Owned work belongs to its owner:
677
753
 
678
754
  The examples show both patterns as product code:
679
755
 
680
- - [`22-subagent-foreground.ts`](test/examples/22-subagent-foreground.ts): the tool above, returning the child's answer. The UI finds the child through the call's `details` and prints the child's events indented under the call.
681
- - [`23-subagent-background.ts`](test/examples/23-subagent-background.ts): persistent subagents behind one `subagent` tool that spawns, messages (steer or follow-up), waits for, stops, and lists them. Each child is owned by a background anchor task, so the parent's Esc and idle waits never reach it. Each message is delivered by a background reporter task that posts the answer back to the parent as a follow-up input once it arrives; request IDs keep a restart from sending a message or a report twice.
756
+ - [`21-subagent-foreground.ts`](test/examples/21-subagent-foreground.ts): the tool above, returning the child's answer. The UI finds the child through the call's `details` and prints the child's events indented under the call.
757
+ - [`22-subagent-background.ts`](test/examples/22-subagent-background.ts): persistent subagents behind one `subagent` tool that spawns, messages (steer or follow-up), waits for, stops, and lists them. Each child is owned by a background anchor task, so the parent's Esc and idle waits never reach it. Each message is delivered by a background reporter task that posts the answer back to the parent as a follow-up input once it arrives; request IDs keep a restart from sending a message or a report twice.
682
758
 
683
759
  ## Child Tasks
684
760
 
@@ -705,23 +781,24 @@ decide: async (task, runtime, context) => {
705
781
  - **Finishing:** a task that finishes while work it owns is still running is `completing`: its outcome is decided, but it becomes terminal, and `waitForTask()` returns, only once that work is done. A failed or aborted outcome aborts that work first.
706
782
  - **Aborting:** abort runs bottom-up. Aborting a task aborts the work it owns first, and its own abort handler starts only once that work is done, so each task undoes its own effects.
707
783
 
708
- [`24-child-tasks.ts`](test/examples/24-child-tasks.ts) runs a checkout with four payments: a declined card, a cancelled checkout, and a restart while the payments run.
784
+ [`23-child-tasks.ts`](test/examples/23-child-tasks.ts) runs a checkout with four payments: a declined card, a cancelled checkout, and a restart while the payments run.
709
785
 
710
786
  ## Task Graph
711
787
 
712
- `harness.taskGraph(context)` shows every live task of the Session as one Chord state, for a task panel or debugging. Each node has its owner edge (`owner` task, or none for a task its conversation owns), its status, whether it is `background` or abort-marked, and the conversations it owns. `harness.watchTaskGraph(context)` delivers the same value as a watch, like a conversation's `watch()`.
788
+ `harness.watchTaskGraph(context)` watches every live task of the Session as one value, for a task panel or debugging, like a conversation's `watch()`. Each node has its owner edge (`owner` task, or none for a task its conversation owns), its status, whether it is `background` or abort-marked, and the conversations it owns.
713
789
 
714
790
  ```typescript
715
- const graph = await harness.taskGraph(context);
716
- graph.subscribe((value) => {
717
- for (const node of Object.values(value.tasks)) {
791
+ const graph = await harness.watchTaskGraph(context);
792
+ graph.start(async (call) => {
793
+ if (call.kind !== "value") return;
794
+ for (const node of Object.values(call.value.tasks)) {
718
795
  const status = node.state.status === "waiting" ? `waiting on ${node.state.on.join(", ")}` : node.state.status;
719
796
  console.log(`${node.id} ${node.kind} ${status}`, node.owner ?? `conversation ${node.conversationId}`);
720
797
  }
721
798
  });
722
799
  ```
723
800
 
724
- A task appears with the commit that creates it and leaves with the commit that makes it terminal. Statuses are the committed ones: `pending`, `running`, `waiting` (with `on` and `policy`), and `completing` (with the held outcome's status). After a restart, tasks that were `running` show as `pending` until they run again. Whether a pending task is blocked by a missing definition is not part of the graph; `harness.inspect()` reports that. The graph lists live tasks only: once a subagent's owner task is terminal, a later task in its conversation is a top-level node, and the conversation's `ConversationRecord.owner` (also in its view's `conversation`) links it to its parent. [`24-child-tasks.ts`](test/examples/24-child-tasks.ts) prints the checkout's tree while its payments run.
801
+ A task appears with the commit that creates it and leaves with the commit that makes it terminal. Statuses are the committed ones: `pending`, `running`, `waiting` (with `on` and `policy`), and `completing` (with the held outcome's status). After a restart, tasks that were `running` show as `pending` until they run again. Whether a pending task is blocked by a missing definition is not part of the graph; `harness.inspect()` reports that. The graph lists live tasks only: once a subagent's owner task is terminal, a later task in its conversation is a top-level node, and the conversation's `ConversationRecord.owner` (also in its view's `conversation`) links it to its parent. [`23-child-tasks.ts`](test/examples/23-child-tasks.ts) prints the checkout's tree while its payments run.
725
802
 
726
803
  ## Your Own State
727
804
 
@@ -746,7 +823,21 @@ await root.commit(async (tx) => {
746
823
  console.log(await harness.snapshot(Todos, root.id, context));
747
824
  ```
748
825
 
749
- `harness.watchDoc()` and `harness.documentState()` observe one document like the view above. `HarnessOptions.conversationCreated(tx, conversation)` runs in every commit that creates or forks a conversation, including a tool's raw `tx.createConversation()`, so every conversation gets your documents; `init` in `createConversation()`, `fork()`, and `root()` writes per-call data in the same commit. An extension's tools, sections, and hooks read their own documents through `api` or `input.read`, and treat an absent one as its default ([`11-extension-state.ts`](test/examples/11-extension-state.ts)).
826
+ A commit whose changes are cheap to lose, such as your own streamed progress, can be shown at once and stored later:
827
+
828
+ ```typescript
829
+ await root.commit(
830
+ async (tx) => {
831
+ (await tx.doc(Todos, root.id)).items.push("draft"); // a document like Todos: latest, fork "initial"
832
+ },
833
+ context,
834
+ { persistence: { kind: "deferred", withinMs: 1_000 } }, // without withinMs: settings.progress.persistWithinMs
835
+ );
836
+ ```
837
+
838
+ Watches and the view see it at once. Storage gets it with the next commit that reads or writes the document, before the document leaves memory, at close, and at the latest about `withinMs` later; `observeCommits()` sees it then, with exactly what was stored (a document the commit retires shows as its retirement only). A crash loses what was not stored yet, so derive nothing durable from such a value outside a commit that reads it. Only a commit that changes nothing but existing documents with `history: "latest"` and `fork: "initial"` is deferred; anything else in it, such as an entry, a new document, or a task state, stores it at once.
839
+
840
+ `harness.watchDoc()` watches one document like the view above. A document watch follows its address: a retirement is a call with `retired: true`, the last value the watch was given (a change made in the retiring commit itself is not shown), and no ops, and a document created there later arrives as `[["r", value]]` on the same watch. A task document's watch ends `ended` with its task: once the retirement call of the commit that ends the task has returned, or at the Harness's close if that call was never delivered (a watch never started); an observer of it gets `end({ kind: "ended" })` right after the retirement. `HarnessOptions.conversationCreated(tx, conversation)` runs in every commit that creates or forks a conversation, including a tool's raw `tx.createConversation()`, so every conversation gets your documents; `init` in `createConversation()`, `fork()`, and `root()` writes per-call data in the same commit. An extension's tools, sections, and hooks read their own documents through `api` or `input.read`, and treat an absent one as its default ([`10-extension-state.ts`](test/examples/10-extension-state.ts)).
750
841
 
751
842
  ## Usage and Cost
752
843
 
@@ -798,7 +889,7 @@ await database.transaction(async (transaction) => {
798
889
  });
799
890
  ```
800
891
 
801
- Custom backends can run the shared conformance suite with any Vitest- or Jest-compatible runner:
892
+ A custom backend's `commit(batches, receive, context)` stores `batches` in order, each as one atomic commit with its own seq, checked against the state the earlier ones left, in as few round trips as it can. It stops at the first batch its own checks refuse (global IDs, document actions, copy resolution): the batches before it stay stored, it and the later ones are not. Once they are durable it calls `receive(index, seq)` per stored batch, in order and synchronously, then resolves with `{ stored, refused? }`; it rejects for an unknown outcome (connection lost, a failed statement or `COMMIT`). `receive` is a durability receipt: the Session publishes each batch later, one at a time, so memory and every view never show a partial log. `receive` calls no Storage method and never throws. Custom backends can run the shared conformance suite, which checks this too, with any Vitest- or Jest-compatible runner:
802
893
 
803
894
  ```typescript
804
895
  import { registerStorageConformance } from "@earendil-works/pi-durable/testing";
@@ -814,13 +905,13 @@ registerStorageConformance({ describe, expect, it }, "My Storage", async (use) =
814
905
  });
815
906
  ```
816
907
 
817
- The package root loads TypeBox, because the tool task validates arguments with pi-ai's `validateToolArguments()`. That costs about 23 MB of peak RSS unbundled, about 4 MB in a tree-shaken bundle.
908
+ The package root loads pi-schema, because the tool task validates arguments with pi-ai's `validateToolArguments()`.
818
909
 
819
910
  ## Errors
820
911
 
821
912
  Three rules decide what an error does:
822
913
 
823
- 1. **A storage error ends the Harness.** The call that hit it rejects with the error. Everything after it, and every pending wait, rejects with `SessionFailed` (its `cause` is that error); watches and event streams end with `session_failed`; running tasks get their abort signal; `onReport` gets the error once. The Harness then closes itself. Nothing is retried; a custom storage retries its own transient failures. Reopen to recover: commits are atomic, so the store is consistent, and resubmitting with the same `requestId` finds a submission that was already admitted.
914
+ 1. **A storage error ends the Harness.** The call that hit it rejects with the error. Everything after it, and every pending wait, rejects with `SessionFailed` (its `cause` is that error); watches end with `failed`; running tasks get their abort signal; `onReport` gets the error once. The Harness then closes itself. Nothing is retried; a custom storage retries its own transient failures. Reopen to recover: commits are atomic, so the store is consistent, and resubmitting with the same `requestId` finds a submission that was already admitted.
824
915
  2. **An invalid read fails only that call:** a storage read throws `StorageRequestError` for a bad cursor, an unknown conversation, or history a document does not keep. A read you cancelled fails only itself too. The exemption covers reads only: any error from `commit()` or `mintId()`, `StorageRequestError` included, ends the Harness as in rule 1.
825
916
  3. **Your code fails only its own unit:** a throwing task phase faults that task, a throwing tool gets an error result, a throwing hook is reported and skipped (`beforeTool` blocks the call), and a throwing commit callback rolls back that commit. Listeners that throw, or return a rejected promise, are reported and the others still run. The exception is a host `RegistryReader` whose `snapshot()` throws: the Harness cannot schedule without it, so it fails the Harness like a storage error.
826
917
 
@@ -828,7 +919,7 @@ Three rules decide what an error does:
828
919
 
829
920
  ```typescript
830
921
  harness.closed.then((end) => {
831
- if (end.reason === "failed") reopen(end.error);
922
+ if (end.kind === "failed") reopen(end.error);
832
923
  });
833
924
  ```
834
925
 
@@ -839,29 +930,35 @@ A failure closes the Harness by itself; `closed` settles once that close is done
839
930
  Runnable examples live in [`test/examples`](test/examples). Run one from this package directory with:
840
931
 
841
932
  ```bash
842
- node --conditions=source --experimental-strip-types test/examples/14-chat.ts
933
+ node --conditions=source --experimental-strip-types test/examples/13-chat.ts
843
934
  ```
844
935
 
845
936
  | Example | Shows |
846
937
  |---|---|
847
- | [14-chat](test/examples/14-chat.ts) | One question and answer |
848
- | [16-real-model](test/examples/16-real-model.ts) | Streaming an answer from OpenAI |
849
- | [17-coding-tools](test/examples/17-coding-tools.ts) | A tool-using turn on JSONL storage |
850
- | [18-print](test/examples/18-print.ts) | Print mode: submit a prompt, print the answer |
851
- | [19-json](test/examples/19-json.ts) | JSON mode: agent events or raw view operations, on SQLite, JSONL, or memory |
852
- | [20-inbox](test/examples/20-inbox.ts) | Steers, follow-ups, writes, and withdrawal while busy |
853
- | [21-late-join](test/examples/21-late-join.ts) | Attaching a view and an event stream mid-run |
854
- | [22-subagent-foreground](test/examples/22-subagent-foreground.ts) | A replay-safe subagent tool whose child the call owns, with the child's events under the call |
855
- | [23-subagent-background](test/examples/23-subagent-background.ts) | Persistent subagents: spawn, steer, stop, list, answers reported back, restart-safe |
856
- | [24-child-tasks](test/examples/24-child-tasks.ts) | A checkout that owns and waits for four payments: failFast, abort, restart |
857
- | [25-compaction](test/examples/25-compaction.ts) | A long chat compacted in the background, manually, and after a context overflow |
858
- | [26-coding-agent](test/examples/26-coding-agent.ts) | CodingTools, live settings from a settings object, an environment that follows the conversation's directory |
859
- | [27-plan-mode](test/examples/27-plan-mode.ts) | A read-only plan mode as an extension with its own document, switched with `configure()` |
860
- | [28-reviewer](test/examples/28-reviewer.ts) | A reviewer conversation with its own model, extensions, tools, directory, and review loop |
861
- | [29-sandbox-per-conversation](test/examples/29-sandbox-per-conversation.ts) | An environment per conversation, looked up from an app document |
862
- | [30-tool-override](test/examples/30-tool-override.ts) | A same-name bash for some conversations, and a wrapper that times whichever bash won |
863
- | [31-reload-and-restart](test/examples/31-reload-and-restart.ts) | Reloading an extension mid-call, and stored choices surviving a restart |
864
- | [00](test/examples/00-conversation.ts)–[13](test/examples/13-recovery.ts) | The layers underneath: sessions, documents, forks, watches, the Harness, agent configuration, reload, extension state, tasks, recovery |
938
+ | [13-chat](test/examples/13-chat.ts) | One question and answer |
939
+ | [15-real-model](test/examples/15-real-model.ts) | Streaming an answer from OpenAI |
940
+ | [16-coding-tools](test/examples/16-coding-tools.ts) | A tool-using turn on JSONL storage |
941
+ | [17-print](test/examples/17-print.ts) | Print mode: submit a prompt, print the answer |
942
+ | [18-json](test/examples/18-json.ts) | JSON mode: raw view operations, or appended entries and `pi.live` per change, on SQLite, JSONL, or memory |
943
+ | [19-inbox](test/examples/19-inbox.ts) | Steers, follow-ups, writes, and withdrawal while busy |
944
+ | [20-late-join](test/examples/20-late-join.ts) | Attaching a view mid-run: the running tool's output, then the streamed answer |
945
+ | [21-subagent-foreground](test/examples/21-subagent-foreground.ts) | A replay-safe subagent tool whose child the call owns, with the child's transcript under the call |
946
+ | [22-subagent-background](test/examples/22-subagent-background.ts) | Persistent subagents: spawn, steer, stop, list, answers reported back, restart-safe |
947
+ | [23-child-tasks](test/examples/23-child-tasks.ts) | A checkout that owns and waits for four payments: failFast, abort, restart |
948
+ | [24-compaction](test/examples/24-compaction.ts) | A long chat compacted in the background, manually, and after a context overflow |
949
+ | [25-coding-agent](test/examples/25-coding-agent.ts) | CodingTools, live settings from a settings object, an environment that follows the conversation's directory |
950
+ | [26-plan-mode](test/examples/26-plan-mode.ts) | A read-only plan mode as an extension with its own document, switched with `configure()` |
951
+ | [27-reviewer](test/examples/27-reviewer.ts) | A reviewer conversation with its own model, extensions, tools, directory, and review loop |
952
+ | [28-sandbox-per-conversation](test/examples/28-sandbox-per-conversation.ts) | An environment per conversation, looked up from an app document |
953
+ | [29-tool-override](test/examples/29-tool-override.ts) | A same-name bash for some conversations, and a wrapper that times whichever bash won |
954
+ | [30-reload-and-restart](test/examples/30-reload-and-restart.ts) | Reloading an extension mid-call, and stored choices surviving a restart |
955
+ | [31-transcript](test/examples/31-transcript.ts) | A client's transcript over the windowed view: live appends, a stall with a gap, loading it and earlier pages, and resuming with `from` |
956
+ | [32-tests-after-edits](test/examples/32-tests-after-edits.ts) | An `afterRun` hook that runs the tests after every run that edited files, a fix run when they fail, and input waiting for them |
957
+ | [33-turn-limit-and-continue](test/examples/33-turn-limit-and-continue.ts) | A per-conversation turn limit, `runFailure`, and continuing without a user message |
958
+ | [34-submit-in-commit](test/examples/34-submit-in-commit.ts) | Admitting a visitor's steer inside the host's own commit with `submit(tx, draft)` and `whenIdle: "reject"` |
959
+ | [35-durable-on-the-store](test/examples/35-durable-on-the-store.ts) | A Harness on `StoreStorage` in a Chord host's store, next to a Chord object's own table: one commit queue, one id allocator, one export |
960
+ | [36-durable-views](test/examples/36-durable-views.ts) | A Chord object following a Durable view with `self.follow`: its frames and a member reading the followed value |
961
+ | [00](test/examples/00-conversation.ts)–[12](test/examples/12-recovery.ts) | The layers underneath: sessions, documents, forks, watches, the Harness, agent configuration, reload, extension state, tasks, recovery |
865
962
 
866
963
  Examples that call OpenAI need `OPENAI_API_KEY`; most use the faux provider otherwise.
867
964
 
@@ -35,8 +35,8 @@
35
35
  * call with an error result.
36
36
  */
37
37
  import { type Context, type JsonValue } from "@earendil-works/chord";
38
- import { type CodemodeSandboxBase } from "@earendil-works/pi-codemode/portable";
39
- import { type Static, Type } from "typebox";
38
+ import type { CodemodeSandboxBase } from "@earendil-works/pi-codemode/portable";
39
+ import { type Static } from "@earendil-works/pi-schema";
40
40
  import type { Extension, ToolExecutionApi, ToolRegistration } from "../harness/types.ts";
41
41
  import type { JsonObject } from "../types.ts";
42
42
  import { type CodemodeNamespace } from "./describe.ts";
@@ -147,8 +147,8 @@ export type CodemodeGlobal = {
147
147
  readonly spread?: boolean;
148
148
  execute(args: unknown, api: ToolExecutionApi, context: Context): Promise<JsonValue | undefined>;
149
149
  };
150
- declare const codemodeSchema: Type.TObject<{
151
- code: Type.TString;
150
+ declare const codemodeSchema: import("@earendil-works/pi-schema").Schema<{
151
+ code: string;
152
152
  }>;
153
153
  export type CodemodeToolInput = Static<typeof codemodeSchema>;
154
154
  export type CodemodeToolOptions = {
@@ -1 +1 @@
1
- {"version":3,"file":"codemode.d.ts","sourceRoot":"","sources":["../../src/codemode/codemode.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"codemode.d.ts","sourceRoot":"","sources":["../../src/codemode/codemode.ts"],"names":[],"mappings":"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"}