@earendil-works/pi-durable 0.99.0 → 0.99.2

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 (172) hide show
  1. package/README.md +435 -46
  2. package/dist/entries.d.ts +20 -2
  3. package/dist/entries.d.ts.map +1 -1
  4. package/dist/entries.js +14 -1
  5. package/dist/entries.js.map +1 -1
  6. package/dist/env/index.d.ts +13 -38
  7. package/dist/env/index.d.ts.map +1 -1
  8. package/dist/env/index.js +2 -0
  9. package/dist/env/index.js.map +1 -1
  10. package/dist/env/node.d.ts.map +1 -1
  11. package/dist/env/node.js +51 -58
  12. package/dist/env/node.js.map +1 -1
  13. package/dist/harness/compaction.d.ts +56 -0
  14. package/dist/harness/compaction.d.ts.map +1 -0
  15. package/dist/harness/compaction.js +385 -0
  16. package/dist/harness/compaction.js.map +1 -0
  17. package/dist/harness/config.d.ts +10 -1
  18. package/dist/harness/config.d.ts.map +1 -1
  19. package/dist/harness/config.js +6 -0
  20. package/dist/harness/config.js.map +1 -1
  21. package/dist/harness/context.d.ts +8 -0
  22. package/dist/harness/context.d.ts.map +1 -1
  23. package/dist/harness/context.js +32 -21
  24. package/dist/harness/context.js.map +1 -1
  25. package/dist/harness/events.d.ts +166 -0
  26. package/dist/harness/events.d.ts.map +1 -0
  27. package/dist/harness/events.js +319 -0
  28. package/dist/harness/events.js.map +1 -0
  29. package/dist/harness/generation.d.ts +37 -6
  30. package/dist/harness/generation.d.ts.map +1 -1
  31. package/dist/harness/generation.js +350 -72
  32. package/dist/harness/generation.js.map +1 -1
  33. package/dist/harness/harness.d.ts.map +1 -1
  34. package/dist/harness/harness.js +124 -3
  35. package/dist/harness/harness.js.map +1 -1
  36. package/dist/harness/inbox.d.ts +57 -0
  37. package/dist/harness/inbox.d.ts.map +1 -0
  38. package/dist/harness/inbox.js +99 -0
  39. package/dist/harness/inbox.js.map +1 -0
  40. package/dist/harness/json.d.ts +10 -0
  41. package/dist/harness/json.d.ts.map +1 -0
  42. package/dist/harness/json.js +33 -0
  43. package/dist/harness/json.js.map +1 -0
  44. package/dist/harness/live.d.ts +66 -7
  45. package/dist/harness/live.d.ts.map +1 -1
  46. package/dist/harness/live.js +71 -7
  47. package/dist/harness/live.js.map +1 -1
  48. package/dist/harness/output.d.ts +62 -0
  49. package/dist/harness/output.d.ts.map +1 -0
  50. package/dist/harness/output.js +261 -0
  51. package/dist/harness/output.js.map +1 -0
  52. package/dist/harness/prompt.d.ts +10 -5
  53. package/dist/harness/prompt.d.ts.map +1 -1
  54. package/dist/harness/prompt.js +66 -13
  55. package/dist/harness/prompt.js.map +1 -1
  56. package/dist/harness/registry.d.ts.map +1 -1
  57. package/dist/harness/registry.js +11 -2
  58. package/dist/harness/registry.js.map +1 -1
  59. package/dist/harness/scheduler.d.ts +37 -12
  60. package/dist/harness/scheduler.d.ts.map +1 -1
  61. package/dist/harness/scheduler.js +632 -72
  62. package/dist/harness/scheduler.js.map +1 -1
  63. package/dist/harness/submissions.d.ts +12 -7
  64. package/dist/harness/submissions.d.ts.map +1 -1
  65. package/dist/harness/submissions.js +72 -33
  66. package/dist/harness/submissions.js.map +1 -1
  67. package/dist/harness/tool.d.ts +36 -0
  68. package/dist/harness/tool.d.ts.map +1 -0
  69. package/dist/harness/tool.js +412 -0
  70. package/dist/harness/tool.js.map +1 -0
  71. package/dist/harness/types.d.ts +185 -11
  72. package/dist/harness/types.d.ts.map +1 -1
  73. package/dist/harness/types.js.map +1 -1
  74. package/dist/harness/usage.d.ts +18 -0
  75. package/dist/harness/usage.d.ts.map +1 -0
  76. package/dist/harness/usage.js +57 -0
  77. package/dist/harness/usage.js.map +1 -0
  78. package/dist/harness/view.d.ts +43 -0
  79. package/dist/harness/view.d.ts.map +1 -0
  80. package/dist/harness/view.js +194 -0
  81. package/dist/harness/view.js.map +1 -0
  82. package/dist/index.d.ts +10 -4
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +6 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/session/observation.d.ts +18 -11
  87. package/dist/session/observation.d.ts.map +1 -1
  88. package/dist/session/observation.js +24 -13
  89. package/dist/session/observation.js.map +1 -1
  90. package/dist/session/session.d.ts +10 -1
  91. package/dist/session/session.d.ts.map +1 -1
  92. package/dist/session/session.js +18 -4
  93. package/dist/session/session.js.map +1 -1
  94. package/dist/session/transaction.d.ts +10 -1
  95. package/dist/session/transaction.d.ts.map +1 -1
  96. package/dist/session/transaction.js +83 -27
  97. package/dist/session/transaction.js.map +1 -1
  98. package/dist/storage/jsonl/storage.d.ts +22 -2
  99. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  100. package/dist/storage/jsonl/storage.js +1 -1
  101. package/dist/storage/jsonl/storage.js.map +1 -1
  102. package/dist/storage/memory.js +1 -1
  103. package/dist/storage/memory.js.map +1 -1
  104. package/dist/storage/sqlite/migrations.js +1 -1
  105. package/dist/storage/sqlite/migrations.js.map +1 -1
  106. package/dist/testing/storage-benchmark.d.ts.map +1 -1
  107. package/dist/testing/storage-benchmark.js +0 -1
  108. package/dist/testing/storage-benchmark.js.map +1 -1
  109. package/dist/testing/storage-conformance.d.ts.map +1 -1
  110. package/dist/testing/storage-conformance.js +34 -2
  111. package/dist/testing/storage-conformance.js.map +1 -1
  112. package/dist/tools/bash.d.ts +28 -0
  113. package/dist/tools/bash.d.ts.map +1 -0
  114. package/dist/tools/bash.js +69 -0
  115. package/dist/tools/bash.js.map +1 -0
  116. package/dist/tools/edit-diff.d.ts +89 -0
  117. package/dist/tools/edit-diff.d.ts.map +1 -0
  118. package/dist/tools/edit-diff.js +386 -0
  119. package/dist/tools/edit-diff.js.map +1 -0
  120. package/dist/tools/edit.d.ts +18 -0
  121. package/dist/tools/edit.d.ts.map +1 -0
  122. package/dist/tools/edit.js +116 -0
  123. package/dist/tools/edit.js.map +1 -0
  124. package/dist/tools/env.d.ts +5 -0
  125. package/dist/tools/env.d.ts.map +1 -0
  126. package/dist/tools/env.js +7 -0
  127. package/dist/tools/env.js.map +1 -0
  128. package/dist/tools/file-mutation-queue.d.ts +5 -0
  129. package/dist/tools/file-mutation-queue.d.ts.map +1 -0
  130. package/dist/tools/file-mutation-queue.js +46 -0
  131. package/dist/tools/file-mutation-queue.js.map +1 -0
  132. package/dist/tools/image.d.ts +2 -0
  133. package/dist/tools/image.d.ts.map +1 -0
  134. package/dist/tools/image.js +92 -0
  135. package/dist/tools/image.js.map +1 -0
  136. package/dist/tools/index.d.ts +5 -0
  137. package/dist/tools/index.d.ts.map +1 -0
  138. package/dist/tools/index.js +5 -0
  139. package/dist/tools/index.js.map +1 -0
  140. package/dist/tools/path-utils.d.ts +5 -0
  141. package/dist/tools/path-utils.d.ts.map +1 -0
  142. package/dist/tools/path-utils.js +26 -0
  143. package/dist/tools/path-utils.js.map +1 -0
  144. package/dist/tools/read.d.ts +17 -0
  145. package/dist/tools/read.d.ts.map +1 -0
  146. package/dist/tools/read.js +100 -0
  147. package/dist/tools/read.js.map +1 -0
  148. package/dist/tools/write.d.ts +10 -0
  149. package/dist/tools/write.d.ts.map +1 -0
  150. package/dist/tools/write.js +30 -0
  151. package/dist/tools/write.js.map +1 -0
  152. package/dist/{env/utils/truncate.d.ts → truncate.d.ts} +1 -17
  153. package/dist/truncate.d.ts.map +1 -0
  154. package/dist/truncate.js +146 -0
  155. package/dist/truncate.js.map +1 -0
  156. package/dist/types.d.ts +86 -19
  157. package/dist/types.d.ts.map +1 -1
  158. package/dist/types.js.map +1 -1
  159. package/docs/pico-v5-handoff.md +366 -86
  160. package/docs/pico-v5.md +1373 -283
  161. package/package.json +11 -3
  162. package/dist/env/utils/adaptive-publisher.d.ts +0 -24
  163. package/dist/env/utils/adaptive-publisher.d.ts.map +0 -1
  164. package/dist/env/utils/adaptive-publisher.js +0 -79
  165. package/dist/env/utils/adaptive-publisher.js.map +0 -1
  166. package/dist/env/utils/output-capture.d.ts +0 -31
  167. package/dist/env/utils/output-capture.d.ts.map +0 -1
  168. package/dist/env/utils/output-capture.js +0 -238
  169. package/dist/env/utils/output-capture.js.map +0 -1
  170. package/dist/env/utils/truncate.d.ts.map +0 -1
  171. package/dist/env/utils/truncate.js +0 -294
  172. package/dist/env/utils/truncate.js.map +0 -1
package/README.md CHANGED
@@ -1,86 +1,475 @@
1
1
  # @earendil-works/pi-durable
2
2
 
3
- Durable conversation, task, and document runtime for Pi.
3
+ > **Experimental.** The API changes without notice between releases.
4
4
 
5
- This package contains the Pico runtime. Its current public API provides durable record contracts and memory, JSONL, and SQLite storage implementations:
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.
6
6
 
7
- ```ts
8
- import { MemoryStorage, ROOT_CONVERSATION_ID } from "@earendil-works/pi-durable";
9
- ```
7
+ Built on [`@earendil-works/pi-ai`](../ai/README.md) for model access and `@earendil-works/chord` for document state.
8
+
9
+ ## Table of Contents
10
10
 
11
- The root export is runtime-neutral. Storage implementations also have explicit subpaths:
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [Concepts](#concepts)
14
+ - [Persist and Resume](#persist-and-resume)
15
+ - [Tools](#tools)
16
+ - [System Prompt](#system-prompt)
17
+ - [Conversation Settings](#conversation-settings)
18
+ - [Watching a Conversation](#watching-a-conversation)
19
+ - [Busy Conversations](#busy-conversations)
20
+ - [Reset and Handoff](#reset-and-handoff)
21
+ - [Compaction](#compaction)
22
+ - [Agent Events (Experimental)](#agent-events-experimental)
23
+ - [Hooks](#hooks)
24
+ - [More Conversations and Forks](#more-conversations-and-forks)
25
+ - [Abort and Subagents](#abort-and-subagents)
26
+ - [Child Tasks](#child-tasks)
27
+ - [Your Own State](#your-own-state)
28
+ - [Usage and Cost](#usage-and-cost)
29
+ - [Storage](#storage)
30
+ - [Examples](#examples)
31
+ - [Design Documents](#design-documents)
12
32
 
13
- ```ts
14
- import { MemoryStorage } from "@earendil-works/pi-durable/storage/memory";
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
15
37
  ```
16
38
 
17
- Node applications can open file-backed JSONL or SQLite storage through Node-only subpaths:
39
+ ## Quick Start
18
40
 
19
- ```ts
41
+ ```typescript
20
42
  import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
21
- import { openNodeJsonlStorage } from "@earendil-works/pi-durable/storage/jsonl/node";
43
+ import { createModels } from "@earendil-works/pi-ai/models";
44
+ import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
45
+ import { AssistantEntry, createRegistry, Harness, MemoryStorage } from "@earendil-works/pi-durable";
46
+
47
+ const context = BACKGROUND_CONTEXT;
48
+
49
+ const models = createModels();
50
+ models.setProvider(openaiProvider()); // reads OPENAI_API_KEY
51
+
52
+ const harness = await Harness.open(new MemoryStorage(), { models, registry: createRegistry() }, context);
53
+ const root = await harness.root(context);
54
+ await root.setModel({ provider: "openai", modelId: "gpt-6-sol" }, context);
55
+
56
+ const submission = await root.submit({ type: "input", content: "What is the capital of France?" }, context);
57
+ const settled = await submission.wait(context);
58
+ if (settled.status === "done" && settled.type === "input") {
59
+ const answer = await root.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
60
+ console.log(answer?.model?.[0]);
61
+ }
62
+ await harness.close(context);
63
+ ```
64
+
65
+ What happened:
66
+
67
+ - `Harness.open()` opens a Session over a storage backend. `MemoryStorage` keeps everything in memory.
68
+ - `root()` returns the root conversation, creating it on first use. A conversation is a transcript of immutable entries.
69
+ - `submit()` durably admits your input and returns a `Submission`. A built-in generation task calls the model and appends the answer.
70
+ - `wait()` resolves once the input is answered (`done`) or has failed (`unanswered`, with a reason).
71
+
72
+ Every async call takes a Chord `Context`, which carries cancellation. `BACKGROUND_CONTEXT` never cancels. Cancelling a wait only cancels that wait, never the work.
73
+
74
+ ## Concepts
75
+
76
+ - **Harness**: one open storage plus the machinery that runs agents on it. All changes go through one line of atomic commits, and nothing is shown before its commit is stored.
77
+ - **Conversation**: a transcript. `root()` creates the root conversation on first use; you can create more and fork them. A `Conversation` handle holds no state; compare handles by `id`.
78
+ - **Entry**: one immutable transcript record, such as a user message (`pi.user`), a model response (`pi.assistant`), a tool result (`pi.tool-result`), a system prompt change (`pi.system`), a reset (`pi.reset`), or your own kind. The model sees the entries from the newest reset onward.
79
+ - **Commit**: an atomic write. `conversation.commit((tx) => ...)` can append entries, edit documents, and create tasks together; either all of it is stored or none of it.
80
+ - **Document**: typed JSON state stored next to the transcript and changed in commits. Built-in ones hold each conversation's settings (`pi.conversation.config`), the running generation and tools (`pi.live`), queued submissions (`pi.inbox`), and spend (`pi.usage`).
81
+ - **Task**: a durable state machine that saves a checkpoint at every step, so a restarted process continues from the last one. Every task has an owner: its conversation, or another task. The Harness runs answers as built-in tasks: `pi.generation` calls the model and owns the `pi.tool` tasks of its tool calls, waits for them, and hands the run to the next generation.
82
+ - **Submission**: something you hand to a conversation, either user input or an entry to write, which you can wait for.
83
+ - **Turn and run**: a turn is one model response and its tool calls; a run is the turns from an input to its final answer. A conversation is busy while a run is going.
84
+ - **Registry**: your tools, system prompt sections, hooks, and tasks. It can change while the Harness runs; new work uses the new state.
85
+
86
+ One answered input, as entries and tasks:
87
+
88
+ ```text
89
+ submit(input) → pi.user
90
+ pi.generation → pi.system (only if the prompt or tools changed), pi.assistant (tool calls)
91
+ pi.tool × n → pi.tool-result × n (owned by the generation, which waits for them)
92
+ pi.generation → pi.assistant (answer) → submission done
93
+ ```
94
+
95
+ ## Persist and Resume
96
+
97
+ Use SQLite or JSONL storage to keep conversations across restarts:
98
+
99
+ ```typescript
22
100
  import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
23
101
 
24
- const jsonl = await openNodeJsonlStorage("./session", BACKGROUND_CONTEXT);
25
- const sqlite = await openNodeSqliteStorage("./session.sqlite");
102
+ const harness = await Harness.open(await openNodeSqliteStorage("./session.sqlite"), { models, registry }, context);
103
+ const root = await harness.root(context); // the same root as last time
104
+ harness.resume(); // continue any run the last process left unfinished
105
+ ```
106
+
107
+ Work interrupted by a crash or close stays pending. `resume()` starts the task scheduler; submitting or waiting starts it too. A retried submission with the same `requestId` returns the existing submission instead of submitting twice:
108
+
109
+ ```typescript
110
+ const submission = await root.submit({ type: "input", content: "Hello", requestId: "greeting-1" }, context);
111
+ // After a restart: the same request ID finds the same submission.
112
+ const again = await root.submit({ type: "input", content: "Hello", requestId: "greeting-1" }, context);
113
+ // again.id === submission.id
114
+ ```
115
+
116
+ `harness.submission(id)` reacquires a submission by ID, for example to wait for it after a restart.
117
+
118
+ ## Tools
119
+
120
+ `@earendil-works/pi-durable/tools` provides `read`, `bash`, `edit`, and `write`. They touch files and processes only through the execution environment passed as `env`:
121
+
122
+ ```typescript
123
+ import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
124
+ import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-durable/tools";
125
+
126
+ const registry = createRegistry();
127
+ for (const tool of [createReadTool(), createBashTool(), createEditTool(), createWriteTool()]) registry.tools.add(tool);
128
+ const env = new NodeExecutionEnv({ cwd: process.cwd() });
129
+ const harness = await Harness.open(storage, { models, registry, env }, context);
26
130
  ```
27
131
 
28
- The portable JSONL core accepts the `FileSystem` capability exported from `@earendil-works/pi-durable/env`. Its `fsync` option defaults to `false`; enabling it flushes affected sidecars before appending the main commit marker. One `JsonlStorage` owner must serialize writes to a storage directory; cross-process locking and ID allocation are not supported.
132
+ A new conversation activates every tool registered when it is created. Reading images is not supported yet.
29
133
 
30
- The portable SQLite core, minimal database facade, and ordered schema migrations are exported from `@earendil-works/pi-durable/storage/sqlite`. Adapters for synchronous SQLite environments such as Bun and Cloudflare Durable Objects can implement that facade without importing Node APIs. Remote asynchronous APIs such as Cloudflare D1 cannot implement this synchronous facade; they require a dedicated `Storage` backend.
134
+ Define your own tool with a TypeBox schema. `api.output()` streams running output, which becomes the result when `execute()` returns no `content`:
135
+
136
+ ```typescript
137
+ import { Type } from "@earendil-works/pi-ai";
138
+
139
+ registry.tools.add({
140
+ name: "count",
141
+ description: "Count from 1 to n",
142
+ parameters: Type.Object({ n: Type.Number() }),
143
+ execute: async (args, api) => {
144
+ for (let i = 1; i <= (args as { n: number }).n; i++) api.output(`${i}\n`);
145
+ return {};
146
+ },
147
+ });
148
+ ```
31
149
 
32
- The Node adapter uses WAL mode with `synchronous = NORMAL` and checkpoints the WAL on close. Acknowledged commits survive process crashes, but the newest commits may be lost after a power or host failure. One `SqliteStorage` owner must serialize writes to a database file; cross-process ID allocation is not supported.
150
+ 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.
151
+
152
+ ## System Prompt
153
+
154
+ The system prompt is built from named sections, rendered before each request:
155
+
156
+ ```typescript
157
+ registry.systemPrompt.section("preamble", () => "You are a concise coding assistant.", { tag: false });
158
+ registry.systemPrompt.section("cwd", () => process.cwd()); // rendered as <cwd>\n...\n</cwd>
159
+ ```
160
+
161
+ Sections and tool changes are stored as positional system entries in the transcript. Only what changed is sent again, which keeps provider prompt caches warm. A section that returns something different every time, such as the current time, defeats that.
162
+
163
+ ## Conversation Settings
164
+
165
+ Each conversation has durable settings, readable and writable through the handle:
166
+
167
+ ```typescript
168
+ await root.setModel({ provider: "openai", modelId: "gpt-6-sol" }, context);
169
+ await root.setThinkingLevel("high", context);
170
+ await root.setActiveTools(["read", "bash"], context);
171
+ await root.setToolExecution("sequential", context); // default "parallel"
172
+ await root.setRetryPolicy({ enabled: true, maxRetries: 3, baseDelayMs: 2000 }, context);
173
+ await root.setStreamOptions({ timeoutMs: 120_000 }, context);
174
+ ```
175
+
176
+ Changes apply when the next model request is prepared, not in the middle of one.
177
+
178
+ ## Watching a Conversation
179
+
180
+ 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:
181
+
182
+ ```typescript
183
+ const view = await root.viewState(context);
184
+ view.subscribe((value) => {
185
+ // value.entries: the active transcript
186
+ // value.docs["pi.live"]: the running generation (streamed partial, retry, deferred) and tool calls (output, details)
187
+ // value.docs["pi.inbox"], value.docs["pi.usage"], value.docs["pi.conversation.config"]
188
+ render(value);
189
+ });
190
+ // later: view.dispose();
191
+ ```
192
+
193
+ `watch()` delivers the same view with the exact Chord operations of each commit, one callback at a time:
194
+
195
+ ```typescript
196
+ const watch = await root.watch(context);
197
+ render(watch.value); // the state at attachment
198
+ watch.start(async (value, ops) => {
199
+ await send(ops); // for example to a remote client that applies them
200
+ });
201
+ // later: await watch.stop();
202
+ ```
203
+
204
+ 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.
205
+
206
+ Partial answers and tool output are committed at most every 100 ms, so a crash loses at most that window.
207
+
208
+ ## Busy Conversations
209
+
210
+ A conversation is busy while a run is working on an input. Submitting to a busy conversation queues the submission in the conversation's inbox, `docs["pi.inbox"]` in the view:
211
+
212
+ ```typescript
213
+ await root.submit({ type: "input", content: "Also run the tests" }, context); // follow-up (default)
214
+ await root.submit({ type: "input", content: "Use pnpm, not npm", whenBusy: "steer" }, context);
215
+ await root.submit({ type: "input", content: "Only if idle", whenBusy: "reject" }, context); // throws ConversationBusy
216
+ await root.submit({ type: "write", entry: { kind: "app.note", data: "user opened a file" } }, context);
217
+ ```
218
+
219
+ - **Steers** are placed after the current tool round and join the running work.
220
+ - **Follow-ups** are placed when the run answers, and start the next run.
221
+ - **Writes** append an entry without asking the model anything.
222
+ - `await submission.abort(context)` withdraws a queued submission.
223
+ - `setSteeringMode("all")` and `setFollowUpMode("all")` place every queued item at once instead of one per turn.
224
+
225
+ If a run fails, queued items stay in the inbox until the next submission places them, oldest first.
226
+
227
+ ## Reset and Handoff
228
+
229
+ `reset()` starts a new context. The model no longer sees older entries, but they stay in storage:
230
+
231
+ ```typescript
232
+ await root.reset(undefined, context); // start from nothing
233
+ await root.reset("We were fixing the flaky login test. Continue.", context); // start from a handoff note
234
+ ```
33
235
 
34
- ## Storage conformance
236
+ 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: "..." }`.
35
237
 
36
- Storage adapters can register the runner-independent conformance cases through the testing entry. The convenience adapter accepts Vitest/Jest-compatible runner functions without importing either package:
238
+ ## Compaction
37
239
 
38
- ```ts
240
+ Compaction shrinks what the model sees: it summarizes older entries and appends a `pi.compaction` entry that holds the summary and heads the first entry it keeps. Older entries stay in storage.
241
+
242
+ ```typescript
243
+ const id = await root.compact("Keep the failing test names", context); // manual, with optional instructions
244
+ const { outcome } = (await harness.waitForTask(id, context)).state;
245
+ if (outcome.status === "completed" && outcome.result.submissionId !== undefined) {
246
+ const placed = await (await harness.submission(outcome.result.submissionId, context))!.wait(context);
247
+ console.log(placed.status); // "done", or "unanswered" with reason "stale"
248
+ }
249
+ ```
250
+
251
+ The conversation keeps working while the summary is made. The summary is placed at once when the conversation is idle, otherwise at the next turn boundary. Esc (`abort()`) cancels a manual compaction.
252
+
253
+ Generation also compacts on its own, controlled per conversation:
254
+
255
+ ```typescript
256
+ await root.setCompaction({
257
+ enabled: true, // automatic compaction; manual compact() always works
258
+ reserveTokens: 16384, // above contextWindow - reserveTokens, the next request waits for a compaction
259
+ keepRecentTokens: 20000, // roughly how much recent context stays verbatim
260
+ backgroundTokens: 32768, // this far below that, a compaction starts in the background; 0 disables it
261
+ }, context);
262
+ ```
263
+
264
+ When a provider rejects a request because the context is too long, generation compacts and retries once. A summary that would cut before the start of the current context settles as `stale` when it is placed, so when several are in flight, the furthest cut stays in effect. Summarization spend counts in `pi.usage`. A `beforeCompact` hook on `CompactionTask` can decline or supply its own summary.
265
+
266
+ Running compactions are listed in `docs["pi.live"].compactions` with their reason, attempt, and retry backoff. The agent events add `compaction_start` and `compaction_end`, and a `compactions` field in the snapshot.
267
+
268
+ ## Agent Events (Experimental)
269
+
270
+ For consumers that want coding-agent style events (`message_start`, `message_update`, `tool_execution_start`, ...) instead of structural state:
271
+
272
+ ```typescript
273
+ import { watchEvents } from "@earendil-works/pi-durable";
274
+
275
+ const stream = await watchEvents(harness, root.id, context);
276
+ initialize(stream.snapshot); // entries, run, in-flight generation, tools, compactions, inbox, config, usage
277
+ stream.start(async (events) => {
278
+ for (const event of events) console.log(JSON.stringify(event));
279
+ });
280
+ ```
281
+
282
+ 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.
283
+
284
+ ## Hooks
285
+
286
+ Hooks let extensions observe or adjust the built-in tasks:
287
+
288
+ ```typescript
289
+ import { GenerationTask, ToolTask } from "@earendil-works/pi-durable";
290
+
291
+ registry.hooks.add(ToolTask, {
292
+ beforeTool: (call) => (call.name === "bash" ? { block: "bash is disabled here" } : undefined),
293
+ });
294
+ registry.hooks.add(GenerationTask, {
295
+ onYield: (answer) => (needsMoreWork(answer) ? { continue: "Keep going." } : undefined),
296
+ });
297
+ ```
298
+
299
+ - **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).
300
+ - **Tools:** `beforeTool` (block or rewrite arguments) and `afterTool` (replace the result).
301
+
302
+ Pass `{ scope: { conversationId } }` to limit a hook to one conversation.
303
+
304
+ ## More Conversations and Forks
305
+
306
+ ```typescript
307
+ const other = await harness.createConversation({ ownership: { kind: "ownerless" } }, context);
308
+ const fork = await root.fork(entryId, { ownership: { kind: "ownerless" } }, context);
309
+ ```
310
+
311
+ A fork sees its parent's entries up to `entryId` and continues independently. It keeps the parent's settings as of that entry.
312
+
313
+ ## Abort and Subagents
314
+
315
+ `await root.abort(context)` stops a conversation: queued inputs are withdrawn (queued writes stay), every task of its current work is aborted, and the call resolves once the conversation is idle.
316
+
317
+ A conversation can be **owned** by a task. A subagent tool creates its child inside `api.commit()` with `ownership: { kind: "task", taskId: api.taskId }`, then drives it through `api.conversation(id)`:
318
+
319
+ ```typescript
320
+ registry.tools.add({
321
+ name: "subagent",
322
+ description: "Delegate a self-contained task to a subagent and get its answer back.",
323
+ parameters: Type.Object({ task: Type.String() }),
324
+ replay: "safe", // a rerun after a crash finds the same child and submission
325
+ execute: async (args, api, context) => {
326
+ const child = await api.commit(async (tx) => {
327
+ // The ownership index remembers the child, so a rerun reuses it.
328
+ const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
329
+ if (existing !== undefined) return existing.id;
330
+ const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
331
+ (await tx.doc(ConversationConfig, created.id)).model = { provider: "openai", modelId: "gpt-6-sol" };
332
+ return created.id;
333
+ }, context);
334
+ await api.details({ conversationId: child }, context); // lets a UI attach to the child
335
+ const request = { type: "input", content: (args as { task: string }).task, requestId: `subagent:${api.taskId}` } as const;
336
+ const settled = await (await (await api.conversation(child, context))!.submit(request, context)).wait(context);
337
+ return { content: [{ type: "text", text: settled.status }] };
338
+ },
339
+ });
340
+ ```
341
+
342
+ Owned work belongs to its owner:
343
+
344
+ - Aborting the call aborts the child. So does the call failing: `execute()` throwing, or a crash that interrupts a call that is not replay-safe.
345
+ - The parent is idle only once the child is.
346
+ - A task created with `{ background: true }` is a boundary: work it owns survives the parent's abort and does not keep the parent busy. `root.abort(context, { background: true })` aborts it too.
347
+
348
+ The examples show both patterns as product code:
349
+
350
+ - [`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.
351
+ - [`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.
352
+
353
+ ## Child Tasks
354
+
355
+ A task can own child tasks, created with `ownership: { kind: "task", taskId }`, and wait for them by committing a `waiting` state:
356
+
357
+ ```typescript
358
+ pay: async (task, runtime, context) => {
359
+ await runtime.commit(async (tx) => {
360
+ const payments = [];
361
+ for (const card of task.input.cards) {
362
+ payments.push(await tx.createTask(Payment, { card }, { ownership: { kind: "task", taskId: task.id } }));
363
+ }
364
+ // Resume in `decide` once every payment is done; the first failure aborts the rest.
365
+ return { status: "waiting", checkpoint: { phase: "decide", payments }, on: payments, policy: "failFast" };
366
+ }, context);
367
+ },
368
+ decide: async (task, runtime, context) => {
369
+ const outcomes = await runtime.outcomes(task.state.checkpoint.payments, context);
370
+ // ...commit the checkout's own outcome
371
+ },
372
+ ```
373
+
374
+ - **Waiting:** the task runs no code while it waits. With `allSettled` it resumes once every task in `on` is done; with `failFast` the first failed child also aborts the others. `on` may name other tasks too, with `allSettled`.
375
+ - **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.
376
+ - **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.
377
+
378
+ [`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.
379
+
380
+ ## Your Own State
381
+
382
+ Documents are typed JSON objects committed together with entries. Define one, and edit it in a commit:
383
+
384
+ ```typescript
385
+ import { defineDoc } from "@earendil-works/pi-durable";
386
+
387
+ const Todos = defineDoc<{ items: string[] }>({
388
+ kind: "app.todos",
389
+ version: 1,
390
+ scope: "conversation",
391
+ history: "latest", // or "rewindable" to read old values with snapshotAsOf()
392
+ fork: "initial", // what a fork starts with: "initial", "current", or "asOf"
393
+ initial: () => ({ items: [] }),
394
+ });
395
+
396
+ await root.commit(async (tx) => {
397
+ (await tx.doc(Todos, root.id)).items.push("write docs");
398
+ }, context);
399
+ console.log(await harness.snapshot(Todos, root.id, context));
400
+ ```
401
+
402
+ `harness.watchDoc()` and `harness.documentState()` observe one document like the view above. `registry.conversations.setup()` creates your documents in every new conversation, and a tool reads them through its `api`.
403
+
404
+ ## Usage and Cost
405
+
406
+ Each conversation keeps token and cost totals in `docs["pi.usage"]`: per `provider/model` for model responses, and per tool name for tool results that report usage. Failed and aborted attempts count too. For the whole Session:
407
+
408
+ ```typescript
409
+ const usage = await harness.usage(context); // { models: { "openai/gpt-6-sol": Usage }, tools: {...} }
410
+ ```
411
+
412
+ ## Storage
413
+
414
+ | Backend | Import | Notes |
415
+ |---|---|---|
416
+ | Memory | `MemoryStorage` from the package root | Nothing is persisted. |
417
+ | SQLite | `openNodeSqliteStorage(file)` from `@earendil-works/pi-durable/storage/sqlite/node` | One database file. WAL mode with `synchronous = NORMAL`: commits survive process crashes; the newest may be lost on power or host failure. |
418
+ | JSONL | `openNodeJsonlStorage(directory, context)` from `@earendil-works/pi-durable/storage/jsonl/node` | Append-only files in one directory. Pass `{ fsync: true }` to flush before each commit marker. |
419
+
420
+ One process owns a storage at a time; there is no cross-process locking. The portable SQLite and JSONL cores (`/storage/sqlite`, `/storage/jsonl`) run without Node APIs, for example on Bun or in Cloudflare Durable Objects, given a synchronous SQLite database or a `FileSystem` from `@earendil-works/pi-durable/env`.
421
+
422
+ Custom backends can run the shared conformance suite with any Vitest- or Jest-compatible runner:
423
+
424
+ ```typescript
39
425
  import { registerStorageConformance } from "@earendil-works/pi-durable/testing";
40
426
  import { describe, expect, it } from "vitest";
41
427
 
42
- registerStorageConformance({ describe, expect, it }, "Some Custom Storage", async (use) => {
43
- const storage = await openCustomStorage();
428
+ registerStorageConformance({ describe, expect, it }, "My Storage", async (use) => {
429
+ const storage = await openMyStorage();
44
430
  try {
45
431
  await use(storage);
46
432
  } finally {
47
- await closeStorage(storage);
433
+ await closeMyStorage(storage);
48
434
  }
49
435
  });
50
436
  ```
51
437
 
52
- The provider must call and await `use` exactly once with isolated storage. Other runners can use `createStorageConformance` with their own `StorageConformanceAssertions` implementation.
438
+ 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.
53
439
 
54
- ## Storage benchmarks
440
+ ## Examples
55
441
 
56
- From this package directory:
442
+ Runnable examples live in [`test/examples`](test/examples). Run one from this package directory with:
57
443
 
58
- ```sh
59
- npm run bench:storage
60
- npm run bench:storage:memory
444
+ ```bash
445
+ node --conditions=source --experimental-strip-types test/examples/14-chat.ts
61
446
  ```
62
447
 
63
- The timing suite runs shared deterministic workloads against the built-in memory, JSONL, and SQLite adapters. Third-party adapters can import the same seeds and workload definitions from `@earendil-works/pi-durable/testing` and use their platform's timing runner:
448
+ | Example | Shows |
449
+ |---|---|
450
+ | [14-chat](test/examples/14-chat.ts) | One question and answer |
451
+ | [16-real-model](test/examples/16-real-model.ts) | Streaming an answer from OpenAI |
452
+ | [17-coding-tools](test/examples/17-coding-tools.ts) | A tool-using turn on JSONL storage |
453
+ | [18-print](test/examples/18-print.ts) | Print mode: submit a prompt, print the answer |
454
+ | [19-json](test/examples/19-json.ts) | JSON mode: agent events or raw view operations, on SQLite, JSONL, or memory |
455
+ | [20-inbox](test/examples/20-inbox.ts) | Steers, follow-ups, writes, and withdrawal while busy |
456
+ | [21-late-join](test/examples/21-late-join.ts) | Attaching a view and an event stream mid-run |
457
+ | [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 |
458
+ | [23-subagent-background](test/examples/23-subagent-background.ts) | Persistent subagents: spawn, steer, stop, list, answers reported back, restart-safe |
459
+ | [24-child-tasks](test/examples/24-child-tasks.ts) | A checkout that owns and waits for four payments: failFast, abort, restart |
460
+ | [25-compaction](test/examples/25-compaction.ts) | A long chat compacted in the background, manually, and after a context overflow |
461
+ | [00](test/examples/00-conversation.ts)–[13](test/examples/13-recovery.ts) | The layers underneath: sessions, documents, forks, watches, tasks, recovery |
64
462
 
65
- ```ts
66
- import {
67
- seedStorageBenchmark,
68
- seedStorageWriteBenchmark,
69
- STORAGE_READ_BENCHMARKS,
70
- STORAGE_WRITE_BENCHMARKS,
71
- } from "@earendil-works/pi-durable/testing";
463
+ Examples that call OpenAI need `OPENAI_API_KEY`; most use the faux provider otherwise.
72
464
 
73
- const dataset = await seedStorageBenchmark(readStorage);
74
- await STORAGE_READ_BENCHMARKS[0].run(readStorage, dataset);
465
+ ## Design Documents
75
466
 
76
- await seedStorageWriteBenchmark(freshWriteStorage);
77
- await STORAGE_WRITE_BENCHMARKS[0].run(freshWriteStorage);
78
- ```
467
+ - [`docs/pico-v5.md`](docs/pico-v5.md): the normative specification
468
+ - [`docs/pico-v5-handoff.md`](docs/pico-v5-handoff.md): the implementation plan
469
+ - [`docs/pico-v5-chord-usage.md`](docs/pico-v5-chord-usage.md): how the package uses Chord
79
470
 
80
- Read scenarios reuse one seeded store; each write sample requires a fresh seeded store. The workloads cover representative commits, indexed reads, pagination, fork traversal, document replay, historical reads, and persistent-backend reopen. The footprint suite measures each built-in adapter in a separate process at 1k and 10k scales and reports heap, RSS, external memory, file counts, and on-disk JSONL/SQLite size. These synthetic workloads are baselines for regression analysis, not production capacity limits or CI pass/fail thresholds.
471
+ Benchmarks: `npm run bench:storage`, `npm run bench:storage:memory`, and `npm run bench:tool-output`.
81
472
 
82
- The normative design and implementation sequence are in:
473
+ ## License
83
474
 
84
- - [`docs/pico-v5.md`](docs/pico-v5.md)
85
- - [`docs/pico-v5-handoff.md`](docs/pico-v5-handoff.md)
86
- - [`docs/pico-v5-chord-usage.md`](docs/pico-v5-chord-usage.md)
475
+ MIT
package/dist/entries.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { JsonValue } from "@earendil-works/chord";
2
+ import type { CompactionReason, ToolDiagnostic } from "./harness/types.ts";
2
3
  import type { Entry } from "./types.ts";
3
4
  /** Define a typed entry kind whose `is()` guard narrows by `EntryRecord.kind`. */
4
5
  export declare function defineEntry<D extends JsonValue = never>(kind: string): Entry<D>;
@@ -8,6 +9,23 @@ export declare const UserEntry: Entry<never>;
8
9
  export declare const AssistantEntry: Entry<never>;
9
10
  /** Positional prompt and tool change: `model` is `[SystemMessage]` with empty `content`. */
10
11
  export declare const SystemEntry: Entry<never>;
11
- /** Tool result: `model` is `[ToolResultMessage]`. Written by tool tasks. */
12
- export declare const ToolResultEntry: Entry<never>;
12
+ /**
13
+ * Tool result: `model` is `[ToolResultMessage]`, whose content ends with the rendered diagnostics block; `data` holds
14
+ * the structured diagnostics, possibly none. Written by tool tasks, and by generation for calls it did not offer.
15
+ */
16
+ export declare const ToolResultEntry: Entry<{
17
+ diagnostics: ToolDiagnostic[];
18
+ }>;
19
+ /**
20
+ * Start of a new context: always `head: "self"`, with `model` absent for a plain reset or `[UserMessage]` carrying the
21
+ * handoff text. Written by `Conversation.reset()` and the `handoff` tool control.
22
+ */
23
+ export declare const ResetEntry: Entry<never>;
24
+ /**
25
+ * Compaction summary: `model` is `[UserMessage]` with the wrapped summary, `head` the first kept entry. Written by
26
+ * compaction tasks, directly or through a write submission.
27
+ */
28
+ export declare const CompactionEntry: Entry<{
29
+ reason: CompactionReason;
30
+ }>;
13
31
  //# sourceMappingURL=entries.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../src/entries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,KAAK,EAA2B,MAAM,YAAY,CAAC;AAEjE,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,CAAC,SAAS,SAAS,GAAG,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAM/E;AAED,sEAAsE;AACtE,eAAO,MAAM,SAAS,cAAyB,CAAC;AAChD,oGAAoG;AACpG,eAAO,MAAM,cAAc,cAA8B,CAAC;AAC1D,4FAA4F;AAC5F,eAAO,MAAM,WAAW,cAA2B,CAAC;AACpD,4EAA4E;AAC5E,eAAO,MAAM,eAAe,cAAgC,CAAC"}
1
+ {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../src/entries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,KAAK,EAAE,KAAK,EAA2B,MAAM,YAAY,CAAC;AAEjE,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,CAAC,SAAS,SAAS,GAAG,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAM/E;AAED,sEAAsE;AACtE,eAAO,MAAM,SAAS,cAAyB,CAAC;AAChD,oGAAoG;AACpG,eAAO,MAAM,cAAc,cAA8B,CAAC;AAC1D,4FAA4F;AAC5F,eAAO,MAAM,WAAW,cAA2B,CAAC;AACpD;;;GAGG;AACH,eAAO,MAAM,eAAe;iBAA8B,cAAc,EAAE;EAAqB,CAAC;AAChG;;;GAGG;AACH,eAAO,MAAM,UAAU,cAA0B,CAAC;AAClD;;;GAGG;AACH,eAAO,MAAM,eAAe;YAAyB,gBAAgB;EAAoB,CAAC"}
package/dist/entries.js CHANGED
@@ -13,6 +13,19 @@ export const UserEntry = defineEntry("pi.user");
13
13
  export const AssistantEntry = defineEntry("pi.assistant");
14
14
  /** Positional prompt and tool change: `model` is `[SystemMessage]` with empty `content`. */
15
15
  export const SystemEntry = defineEntry("pi.system");
16
- /** Tool result: `model` is `[ToolResultMessage]`. Written by tool tasks. */
16
+ /**
17
+ * Tool result: `model` is `[ToolResultMessage]`, whose content ends with the rendered diagnostics block; `data` holds
18
+ * the structured diagnostics, possibly none. Written by tool tasks, and by generation for calls it did not offer.
19
+ */
17
20
  export const ToolResultEntry = defineEntry("pi.tool-result");
21
+ /**
22
+ * Start of a new context: always `head: "self"`, with `model` absent for a plain reset or `[UserMessage]` carrying the
23
+ * handoff text. Written by `Conversation.reset()` and the `handoff` tool control.
24
+ */
25
+ export const ResetEntry = defineEntry("pi.reset");
26
+ /**
27
+ * Compaction summary: `model` is `[UserMessage]` with the wrapped summary, `head` the first kept entry. Written by
28
+ * compaction tasks, directly or through a write submission.
29
+ */
30
+ export const CompactionEntry = defineEntry("pi.compaction");
18
31
  //# sourceMappingURL=entries.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"entries.js","sourceRoot":"","sources":["../src/entries.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,MAAM,UAAU,WAAW,CAA8B,IAAY;IACpE,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAC;IAChH,OAAO;QACN,IAAI;QACJ,EAAE,EAAE,CAAC,KAA8B,EAA0B,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;KAC1G,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;AAChD,oGAAoG;AACpG,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAC,cAAc,CAAC,CAAC;AAC1D,4FAA4F;AAC5F,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;AACpD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC,gBAAgB,CAAC,CAAC","sourcesContent":["import type { JsonValue } from \"@earendil-works/chord\";\nimport type { Entry, EntryRecord, TypedEntry } from \"./types.ts\";\n\n/** Define a typed entry kind whose `is()` guard narrows by `EntryRecord.kind`. */\nexport function defineEntry<D extends JsonValue = never>(kind: string): Entry<D> {\n\tif (typeof kind !== \"string\" || kind.length === 0) throw new TypeError(\"Entry kind must be a non-empty string\");\n\treturn {\n\t\tkind,\n\t\tis: (entry: EntryRecord | undefined): entry is TypedEntry<D> => entry !== undefined && entry.kind === kind,\n\t};\n}\n\n/** User input: `model` is `[UserMessage]`. Written by submissions. */\nexport const UserEntry = defineEntry(\"pi.user\");\n/** Provider result with any stop reason: `model` is `[AssistantMessage]`. Written by generation. */\nexport const AssistantEntry = defineEntry(\"pi.assistant\");\n/** Positional prompt and tool change: `model` is `[SystemMessage]` with empty `content`. */\nexport const SystemEntry = defineEntry(\"pi.system\");\n/** Tool result: `model` is `[ToolResultMessage]`. Written by tool tasks. */\nexport const ToolResultEntry = defineEntry(\"pi.tool-result\");\n"]}
1
+ {"version":3,"file":"entries.js","sourceRoot":"","sources":["../src/entries.ts"],"names":[],"mappings":"AAIA,kFAAkF;AAClF,MAAM,UAAU,WAAW,CAA8B,IAAY;IACpE,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAC;IAChH,OAAO;QACN,IAAI;QACJ,EAAE,EAAE,CAAC,KAA8B,EAA0B,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;KAC1G,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;AAChD,oGAAoG;AACpG,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAC,cAAc,CAAC,CAAC;AAC1D,4FAA4F;AAC5F,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;AACpD;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAoC,gBAAgB,CAAC,CAAC;AAChG;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;AAClD;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAA+B,eAAe,CAAC,CAAC","sourcesContent":["import type { JsonValue } from \"@earendil-works/chord\";\nimport type { CompactionReason, ToolDiagnostic } from \"./harness/types.ts\";\nimport type { Entry, EntryRecord, TypedEntry } from \"./types.ts\";\n\n/** Define a typed entry kind whose `is()` guard narrows by `EntryRecord.kind`. */\nexport function defineEntry<D extends JsonValue = never>(kind: string): Entry<D> {\n\tif (typeof kind !== \"string\" || kind.length === 0) throw new TypeError(\"Entry kind must be a non-empty string\");\n\treturn {\n\t\tkind,\n\t\tis: (entry: EntryRecord | undefined): entry is TypedEntry<D> => entry !== undefined && entry.kind === kind,\n\t};\n}\n\n/** User input: `model` is `[UserMessage]`. Written by submissions. */\nexport const UserEntry = defineEntry(\"pi.user\");\n/** Provider result with any stop reason: `model` is `[AssistantMessage]`. Written by generation. */\nexport const AssistantEntry = defineEntry(\"pi.assistant\");\n/** Positional prompt and tool change: `model` is `[SystemMessage]` with empty `content`. */\nexport const SystemEntry = defineEntry(\"pi.system\");\n/**\n * Tool result: `model` is `[ToolResultMessage]`, whose content ends with the rendered diagnostics block; `data` holds\n * the structured diagnostics, possibly none. Written by tool tasks, and by generation for calls it did not offer.\n */\nexport const ToolResultEntry = defineEntry<{ diagnostics: ToolDiagnostic[] }>(\"pi.tool-result\");\n/**\n * Start of a new context: always `head: \"self\"`, with `model` absent for a plain reset or `[UserMessage]` carrying the\n * handoff text. Written by `Conversation.reset()` and the `handoff` tool control.\n */\nexport const ResetEntry = defineEntry(\"pi.reset\");\n/**\n * Compaction summary: `model` is `[UserMessage]` with the wrapped summary, `head` the first kept entry. Written by\n * compaction tasks, directly or through a write submission.\n */\nexport const CompactionEntry = defineEntry<{ reason: CompactionReason }>(\"pi.compaction\");\n"]}