@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.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 (141) hide show
  1. package/package.json +3 -3
  2. package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
  3. package/src/adapter/cloudflare/agent.ts +1592 -0
  4. package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
  5. package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +25 -35
  6. package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
  7. package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
  8. package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
  9. package/src/{db → adapter/cloudflare}/schema.ts +16 -83
  10. package/src/adapter/cloudflare/subagent/definition.ts +63 -33
  11. package/src/adapter/cloudflare/subagent/runner.ts +174 -120
  12. package/src/adapter/cloudflare/subagent/tools.ts +13 -14
  13. package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
  14. package/src/adapter/cloudflare/universal-agent/tools.ts +10 -10
  15. package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
  16. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
  17. package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
  18. package/src/config/activation.ts +140 -0
  19. package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
  20. package/src/{runtime-assembler.ts → config/assembler.ts} +20 -19
  21. package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
  22. package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
  23. package/src/{runtime-definition.ts → config/definition.ts} +13 -13
  24. package/src/config/harness-runtime.ts +182 -0
  25. package/src/config/index.ts +14 -0
  26. package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +38 -16
  27. package/src/{kernel/bindings.ts → config/ports.ts} +9 -13
  28. package/src/{kernel → config}/profile.ts +7 -0
  29. package/src/{kernel → config}/public-contracts.ts +6 -6
  30. package/src/{kernel → config}/runtime-load.ts +1 -1
  31. package/src/{pi/assembly/context.ts → context/assembly.ts} +4 -206
  32. package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
  33. package/src/context/extension/index.ts +3 -0
  34. package/src/context/index.ts +6 -0
  35. package/src/{layers/context → context/memory}/budget/gate.ts +2 -2
  36. package/src/conversation/harness-resume.ts +288 -0
  37. package/src/conversation/harness-stream.ts +98 -0
  38. package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
  39. package/src/conversation/index.ts +6 -0
  40. package/src/{pi → conversation}/message/contract.ts +3 -0
  41. package/src/{pi → conversation}/message/conversion.ts +75 -0
  42. package/src/{pi → conversation}/message/index.ts +1 -0
  43. package/src/{pi → conversation}/message/model-input.ts +1 -1
  44. package/src/{pi → conversation}/message/projection.ts +76 -10
  45. package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
  46. package/src/conversation/state.ts +175 -0
  47. package/src/harness/index.ts +7 -0
  48. package/src/harness/session-harness.ts +481 -0
  49. package/src/harness/submission-operation.ts +49 -0
  50. package/src/index.ts +38 -43
  51. package/src/lib/mcp.ts +1 -1
  52. package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -18
  53. package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
  54. package/src/{pi/session → session/cloudflare}/migration.ts +334 -8
  55. package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
  56. package/src/session/index.ts +39 -0
  57. package/src/submission/admission.ts +275 -0
  58. package/src/{kernel/receipts.ts → submission/contracts.ts} +14 -0
  59. package/src/submission/delivery.ts +212 -0
  60. package/src/submission/finalization.ts +111 -0
  61. package/src/submission/index.ts +9 -0
  62. package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
  63. package/src/{db/submission.repo.ts → submission/repository.ts} +62 -119
  64. package/src/submission/steer-delivery.ts +167 -0
  65. package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
  66. package/src/telemetry/contract.ts +12 -0
  67. package/src/telemetry/coordinator.ts +65 -0
  68. package/src/telemetry/delivery.ts +2 -2
  69. package/src/telemetry/index.ts +5 -0
  70. package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
  71. package/src/telemetry/recorder.ts +1 -1
  72. package/src/telemetry/runtime-telemetry.ts +12 -0
  73. package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
  74. package/src/telemetry/turn-events.ts +279 -0
  75. package/src/{pi/tool → tools}/ai-adapter.ts +2 -2
  76. package/src/tools/approval/index.ts +1 -0
  77. package/src/{db/approval.repo.ts → tools/approval/repository.ts} +10 -30
  78. package/src/{pi/tool → tools}/base.ts +14 -30
  79. package/src/tools/compiler.ts +327 -0
  80. package/src/{pi/tool → tools}/core-host.ts +3 -3
  81. package/src/{pi/tool → tools}/core.ts +3 -3
  82. package/src/tools/decision-gate.ts +227 -0
  83. package/src/{pi/tool → tools}/gateway.ts +1 -1
  84. package/src/tools/human-decisions.ts +127 -0
  85. package/src/tools/index.ts +30 -0
  86. package/src/tools/interaction/index.ts +1 -0
  87. package/src/{pi/tool → tools}/mcp.ts +1 -1
  88. package/src/{pi/tool → tools}/schedule.ts +3 -3
  89. package/src/{pi/tool → tools}/skill.ts +4 -4
  90. package/src/{kernel → tools}/subagent-runtime.ts +1 -1
  91. package/src/{pi/tool → tools}/subagent.ts +4 -4
  92. package/src/tools/temporary-agent/approvals.ts +126 -0
  93. package/src/{layers/orchestration/temporary-agent/core.ts → tools/temporary-agent/coordinator.ts} +41 -49
  94. package/src/tools/temporary-agent/core.ts +53 -0
  95. package/src/{layers/orchestration → tools}/temporary-agent/runner.ts +1 -1
  96. package/src/{layers/orchestration → tools}/temporary-agent/workspace.ts +1 -1
  97. package/src/{pi/tool → tools}/web-search/api.ts +1 -1
  98. package/src/{pi/tool → tools}/workspace-revision.ts +2 -2
  99. package/src/{pi/tool → tools}/workspace-sandbox.ts +2 -2
  100. package/src/{pi/tool → tools}/workspace-state-connector.ts +1 -1
  101. package/src/db/milestone.repo.ts +0 -104
  102. package/src/db/settlement.repo.ts +0 -104
  103. package/src/kernel/approval-lifecycle.ts +0 -638
  104. package/src/kernel/interaction-lifecycle.ts +0 -433
  105. package/src/kernel/recoverable-chat-agent.ts +0 -1020
  106. package/src/kernel/state.ts +0 -91
  107. package/src/pi/assembly/index.ts +0 -19
  108. package/src/pi/runtime-adapter/execution.ts +0 -1163
  109. package/src/pi/runtime-adapter/index.ts +0 -293
  110. package/src/pi/runtime-adapter/recovery.ts +0 -982
  111. package/src/pi/session/index.ts +0 -24
  112. package/src/pi/tool/compiler.ts +0 -483
  113. package/src/pi/tool/index.ts +0 -38
  114. package/src/pi/turn/approval.ts +0 -181
  115. package/src/pi/turn/index.ts +0 -82
  116. package/src/pi/turn/interaction.ts +0 -181
  117. package/src/runtime.ts +0 -4760
  118. /package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
  119. /package/src/{kernel → config}/degradation.ts +0 -0
  120. /package/src/{kernel → config}/runtime-config.ts +0 -0
  121. /package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
  122. /package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
  123. /package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
  124. /package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
  125. /package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
  126. /package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
  127. /package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
  128. /package/src/{pi/tool → tools}/declared.ts +0 -0
  129. /package/src/{db/interaction.repo.ts → tools/interaction/repository.ts} +0 -0
  130. /package/src/{pi/tool → tools}/nested-tools.ts +0 -0
  131. /package/src/{kernel/submission-authority.ts → tools/subagent-authority.ts} +0 -0
  132. /package/src/{db/agent-tool.repo.ts → tools/subagent-run-repository.ts} +0 -0
  133. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/contract.ts +0 -0
  134. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/extract/index.ts +0 -0
  135. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/fanout/index.ts +0 -0
  136. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/registry.ts +0 -0
  137. /package/src/{pi/tool → tools}/time.ts +0 -0
  138. /package/src/{pi/tool → tools}/web-fetch.ts +0 -0
  139. /package/src/{pi/tool → tools}/web-search/index.ts +0 -0
  140. /package/src/{pi/tool → tools}/web-search/web-search.ts +0 -0
  141. /package/src/{workspace-versioning.ts → tools/workspace-versioning.ts} +0 -0
@@ -2,7 +2,7 @@ import { isEqual } from "lodash-es";
2
2
  import {
3
3
  decodePiToolRecoveryMilestone,
4
4
  replayPiToolRecovery,
5
- } from "../turn/tool-recovery";
5
+ } from "./legacy-tool-recovery";
6
6
  /** Versioned SQL data migration, called before the Runtime opens its transcript. */
7
7
  import {
8
8
  branchTip,
@@ -12,19 +12,36 @@ import {
12
12
  setValue,
13
13
  deleteValue,
14
14
  sessionName,
15
+ laneState,
16
+ operationMeta,
17
+ operationState,
18
+ pendingEntry,
15
19
  sessionEntryToContextMessages,
16
20
  value,
17
21
  type Entry,
18
22
  type JsonValue,
19
23
  type Write,
24
+ type Value,
25
+ type LaneState,
26
+ type ToolExecution,
20
27
  } from "@earendil-works/pi-agent-core/harness/session";
21
- import {
22
- createToolResultMessage,
23
- type AgentToolResult,
24
- type AgentMessage,
28
+ import type {
29
+ AgentToolResult,
30
+ AgentMessage,
25
31
  } from "@earendil-works/pi-agent-core";
26
32
  import type { Usage, ToolResultMessage } from "@earendil-works/pi-ai";
27
- import { CloudflareSessionStorage } from "./storage";
33
+ import {
34
+ CloudflareSessionStorage,
35
+ CURRENT_PI_STORAGE_VERSION,
36
+ } from "./storage";
37
+
38
+ const RETIRED_SCHEDULE_CALLBACKS = [
39
+ "_cleanupStreamBuffers",
40
+ "_piPlannedContinuation",
41
+ "_piContinuationWatchdog",
42
+ "_piApprovalContinuation",
43
+ "_chatRecoveryRetry",
44
+ ] as const;
28
45
 
29
46
  type LegacyRecord = Record<string, unknown> & {
30
47
  id: string;
@@ -76,7 +93,7 @@ function message(input: unknown): AgentMessage {
76
93
  finite(record.timestamp);
77
94
  if (record.role === "assistant" && record.stopReason === "pending")
78
95
  throw new Error("Unsettled legacy assistant message");
79
- return record as unknown as AgentMessage;
96
+ return normalizeLegacyAskUserMessage(record) as AgentMessage;
80
97
  }
81
98
  function hasTable(sql: SqlStorage, name: string): boolean {
82
99
  return (
@@ -89,6 +106,290 @@ function hasTable(sql: SqlStorage, name: string): boolean {
89
106
  );
90
107
  }
91
108
 
109
+ const ASK_USER_RESPONSE_TYPES = new Set([
110
+ "text",
111
+ "single_select",
112
+ "multi_select",
113
+ "attachment",
114
+ ]);
115
+
116
+ function normalizeLegacyAskUserInput(input: unknown): unknown {
117
+ if (!input || typeof input !== "object" || Array.isArray(input)) return input;
118
+ const record = input as Record<string, unknown>;
119
+ if (!Array.isArray(record.questions)) return input;
120
+ return {
121
+ ...record,
122
+ questions: record.questions.map((value) => {
123
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
124
+ const question = value as Record<string, unknown>;
125
+ const { kind, multiSelect, ...current } = question;
126
+ if (ASK_USER_RESPONSE_TYPES.has(question.responseType as string)) {
127
+ return current;
128
+ }
129
+ if (kind !== undefined && kind !== "attachment") return value;
130
+ const responseType = kind === "attachment"
131
+ ? "attachment"
132
+ : Array.isArray(question.options)
133
+ ? multiSelect === true ? "multi_select" : "single_select"
134
+ : "text";
135
+ return { ...current, responseType };
136
+ }),
137
+ };
138
+ }
139
+
140
+ function normalizeLegacyAskUserMessage(value: unknown): unknown {
141
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
142
+ const message = value as Record<string, unknown>;
143
+ if (message.role !== "assistant" || !Array.isArray(message.content)) return value;
144
+ return {
145
+ ...message,
146
+ content: message.content.map((part) => {
147
+ if (!part || typeof part !== "object" || Array.isArray(part)) return part;
148
+ const block = part as Record<string, unknown>;
149
+ return block.type === "toolCall" && block.name === "ask_user"
150
+ ? {
151
+ ...block,
152
+ arguments: normalizeLegacyAskUserInput(block.arguments),
153
+ }
154
+ : part;
155
+ }),
156
+ };
157
+ }
158
+
159
+ function normalizeLegacyAskUserEntry(value: unknown): unknown {
160
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
161
+ const entry = value as Record<string, unknown>;
162
+ if (entry.type === "message") {
163
+ return { ...entry, message: normalizeLegacyAskUserMessage(entry.message) };
164
+ }
165
+ if (entry.type === "compaction" && Array.isArray(entry.retainedTail)) {
166
+ return {
167
+ ...entry,
168
+ retainedTail: entry.retainedTail.map(normalizeLegacyAskUserMessage),
169
+ };
170
+ }
171
+ return value;
172
+ }
173
+
174
+ function migrateLegacyAskUserData(sql: SqlStorage): void {
175
+ if (hasTable(sql, "pi_entries")) {
176
+ for (const row of sql.exec<{ id: string; body: string }>(
177
+ "SELECT id, body FROM pi_entries",
178
+ ).toArray()) {
179
+ sql.exec(
180
+ "UPDATE pi_entries SET body = ? WHERE id = ?",
181
+ JSON.stringify(normalizeLegacyAskUserEntry(JSON.parse(row.body))),
182
+ row.id,
183
+ );
184
+ }
185
+ }
186
+ if (hasTable(sql, "pi_tool_interactions")) {
187
+ for (const row of sql.exec<{ interaction_id: string; input_json: string }>(
188
+ "SELECT interaction_id, input_json FROM pi_tool_interactions WHERE tool_name = 'ask_user'",
189
+ ).toArray()) {
190
+ sql.exec(
191
+ "UPDATE pi_tool_interactions SET input_json = ? WHERE interaction_id = ?",
192
+ JSON.stringify(normalizeLegacyAskUserInput(JSON.parse(row.input_json))),
193
+ row.interaction_id,
194
+ );
195
+ }
196
+ }
197
+ if (hasTable(sql, "pi_values")) {
198
+ for (const row of sql.exec<{ key: string; body: string }>(
199
+ "SELECT key, body FROM pi_values WHERE namespace = 'pi.op.state'",
200
+ ).toArray()) {
201
+ const state = JSON.parse(row.body) as {
202
+ batch?: {
203
+ assistantEntryId?: unknown;
204
+ turnId?: unknown;
205
+ };
206
+ };
207
+ const assistantEntryId = state.batch?.assistantEntryId;
208
+ const turnId = state.batch?.turnId;
209
+ if (typeof assistantEntryId !== "string" || typeof turnId !== "string") {
210
+ continue;
211
+ }
212
+ const entryRow = sql.exec<{ body: string }>(
213
+ "SELECT body FROM pi_entries WHERE id = ?",
214
+ assistantEntryId,
215
+ ).toArray()[0];
216
+ if (!entryRow) continue;
217
+ const entry = JSON.parse(entryRow.body) as {
218
+ type?: unknown;
219
+ message?: { role?: unknown; content?: unknown };
220
+ };
221
+ if (
222
+ entry.type !== "message" || entry.message?.role !== "assistant" ||
223
+ !Array.isArray(entry.message.content)
224
+ ) continue;
225
+ entry.message.content.forEach((part, sourceIndex) => {
226
+ if (!part || typeof part !== "object" || Array.isArray(part)) return;
227
+ const block = part as Record<string, unknown>;
228
+ if (block.type !== "toolCall" || block.name !== "ask_user") return;
229
+ const key = `${row.key}:${turnId}:${sourceIndex}`;
230
+ const args = sql.exec<{ body: string }>(
231
+ "SELECT body FROM pi_values WHERE namespace = 'pi.op.tool_args' AND key = ?",
232
+ key,
233
+ ).toArray()[0];
234
+ if (!args) return;
235
+ sql.exec(
236
+ "UPDATE pi_values SET body = ? WHERE namespace = 'pi.op.tool_args' AND key = ?",
237
+ JSON.stringify(normalizeLegacyAskUserInput(JSON.parse(args.body))),
238
+ key,
239
+ );
240
+ });
241
+ }
242
+ }
243
+ }
244
+
245
+ function removeRetiredRuntimeArtifacts(sql: SqlStorage): void {
246
+ sql.exec(
247
+ "CREATE TABLE IF NOT EXISTS pi_session_entry_submission (entry_id TEXT PRIMARY KEY, submission_id TEXT NOT NULL)",
248
+ );
249
+ if (hasTable(sql, "pi_session_message_submission")) {
250
+ sql.exec(`
251
+ INSERT OR IGNORE INTO pi_session_entry_submission (entry_id, submission_id)
252
+ SELECT message_id, submission_id FROM pi_session_message_submission;
253
+ DROP TABLE pi_session_message_submission;
254
+ `);
255
+ }
256
+ if (hasTable(sql, "cf_agents_schedules")) {
257
+ const placeholders = RETIRED_SCHEDULE_CALLBACKS.map(() => "?").join(", ");
258
+ sql.exec(
259
+ `DELETE FROM cf_agents_schedules WHERE callback IN (${placeholders})`,
260
+ ...RETIRED_SCHEDULE_CALLBACKS,
261
+ );
262
+ }
263
+ migrateLegacyAskUserData(sql);
264
+ }
265
+
266
+ /** Older Harness adapters copied the initial user sidecar onto consumed Steers. */
267
+ function repairDuplicatedUserSidecars(sql: SqlStorage): void {
268
+ if (!hasTable(sql, "pi_message_ui") || !hasTable(sql, "pi_submissions")) return;
269
+ const entries = new Map(sql.exec<{
270
+ id: string;
271
+ parent_id: string | null;
272
+ body: string;
273
+ submission_id: string | null;
274
+ }>(`
275
+ SELECT entry.id, entry.parent_id, entry.body, link.submission_id
276
+ FROM pi_entries AS entry
277
+ LEFT JOIN pi_session_entry_submission AS link ON link.entry_id = entry.id
278
+ `).toArray().map((row) => [row.id, { ...row, entry: JSON.parse(row.body) as Entry }]));
279
+ const candidates = sql.exec<{ message_id: string }>(`
280
+ SELECT ui.message_id
281
+ FROM pi_message_ui AS ui
282
+ JOIN pi_session_entry_submission AS link ON link.entry_id = ui.message_id
283
+ JOIN pi_submissions AS submission ON submission.submission_id = link.submission_id
284
+ WHERE json_extract(ui.body, '$.id') = submission.user_message_id
285
+ `).toArray();
286
+ for (const candidate of candidates) {
287
+ const current = entries.get(candidate.message_id);
288
+ if (current?.entry.type !== "message" || current.entry.message.role !== "user") continue;
289
+ let parentId = current.parent_id;
290
+ const visited = new Set<string>();
291
+ while (parentId !== null) {
292
+ if (visited.has(parentId)) throw new Error(`Cyclic Pi ancestry: ${parentId}`);
293
+ visited.add(parentId);
294
+ const parent = entries.get(parentId);
295
+ if (!parent) throw new Error(`Missing Pi ancestor: ${parentId}`);
296
+ if (parent.submission_id === current.submission_id &&
297
+ parent.entry.type === "message" && parent.entry.message.role === "user") {
298
+ // The copied parts are also wrong: canonical Steer content is authoritative.
299
+ sql.exec("DELETE FROM pi_message_ui WHERE message_id = ?", candidate.message_id);
300
+ break;
301
+ }
302
+ parentId = parent.parent_id;
303
+ }
304
+ }
305
+ }
306
+
307
+ function hasToolExecution(value: ToolExecution | undefined): boolean {
308
+ return value?.status === "blocked" ||
309
+ (value?.status === "executed" && value.args !== null &&
310
+ typeof value.args === "object" && !Array.isArray(value.args));
311
+ }
312
+
313
+ /** Old active tool outcomes cannot be reconstructed from error text or approval facts. */
314
+ function assertActiveToolProvenance(sql: SqlStorage): void {
315
+ const read = <T>(address: Value<T>): T | undefined => {
316
+ const row = sql.exec<{ body: string }>(
317
+ "SELECT body FROM pi_values WHERE namespace = ? AND key = ?",
318
+ address.namespace, address.key,
319
+ ).toArray()[0];
320
+ return row ? JSON.parse(row.body) as T : undefined;
321
+ };
322
+ const lanes = sql.exec<{ key: string; body: string }>(
323
+ "SELECT key, body FROM pi_values WHERE namespace = ?",
324
+ laneState("").namespace,
325
+ ).toArray();
326
+ for (const lane of lanes) {
327
+ const operationId = (JSON.parse(lane.body) as LaneState).currentOperationId;
328
+ if (operationId === null) continue;
329
+ const reject: (reason: string) => never = (reason) => {
330
+ throw new Error(
331
+ `Cannot upgrade Pi storage while operation ${operationId} is active: ${reason}. ` +
332
+ "Complete this operation with the previous runtime version, then retry the upgrade.",
333
+ );
334
+ };
335
+ const meta = read(operationMeta(operationId));
336
+ const state = read(operationState(operationId));
337
+ if (!meta || !state) reject("its durable operation metadata is incomplete");
338
+ let tip = read(branchTip(lane.key));
339
+ if (tip === undefined) reject("its lane branch tip is missing");
340
+ const visited = new Set<string>();
341
+ while (tip !== meta.sourceTipId) {
342
+ if (tip === null || visited.has(tip)) reject("its operation branch cannot be verified");
343
+ visited.add(tip);
344
+ const row = sql.exec<{ body: string }>("SELECT body FROM pi_entries WHERE id = ?", tip).toArray()[0];
345
+ if (!row) reject(`its canonical entry ${tip} is missing`);
346
+ const entry = JSON.parse(row.body) as Entry;
347
+ if (entry.type === "message" && entry.message.role === "toolResult" &&
348
+ !hasToolExecution(entry.toolExecution)) {
349
+ reject(`ToolResult ${entry.id} has no reliable execution provenance`);
350
+ }
351
+ tip = entry.parentId;
352
+ }
353
+ if (state.at !== "tools") continue;
354
+ for (const call of state.batch.calls) {
355
+ if (call.status === "planned" || call.status === "completed") continue;
356
+ if (call.status === "effect_pending") {
357
+ reject(`Tool invocation ${call.resultEntryId} has an unresolved execution outcome`);
358
+ }
359
+ const pending = read(pendingEntry(call.resultEntryId));
360
+ if (pending?.type !== "message" || pending.payload.role !== "toolResult" ||
361
+ !hasToolExecution(pending.toolExecution)) {
362
+ reject(`pending ToolResult ${call.resultEntryId} has no reliable execution provenance`);
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ /** Upgrade an already-Harness-backed Session before normal reads begin. */
369
+ export function migrateCurrentPiSession(
370
+ storage: DurableObjectStorage,
371
+ ): boolean {
372
+ return storage.transactionSync(() => {
373
+ const sql = storage.sql;
374
+ if (!hasTable(sql, "pi_storage_state")) return false;
375
+ const version = sql.exec<{ storage_version: number }>(
376
+ "SELECT storage_version FROM pi_storage_state WHERE singleton = 1",
377
+ ).one().storage_version;
378
+ if (version === CURRENT_PI_STORAGE_VERSION) return false;
379
+ if (version !== 1 && version !== 2) {
380
+ throw new Error(`Unsupported Cloudflare Pi storage version: ${version}`);
381
+ }
382
+ assertActiveToolProvenance(sql);
383
+ if (version === 1) removeRetiredRuntimeArtifacts(sql);
384
+ repairDuplicatedUserSidecars(sql);
385
+ sql.exec(
386
+ "UPDATE pi_storage_state SET storage_version = ? WHERE singleton = 1",
387
+ CURRENT_PI_STORAGE_VERSION,
388
+ );
389
+ return true;
390
+ });
391
+ }
392
+
92
393
  function assertSameToolResult(
93
394
  left: ToolResultMessage,
94
395
  right: ToolResultMessage,
@@ -107,6 +408,28 @@ function assertSameToolResult(
107
408
  }
108
409
  }
109
410
 
411
+ function legacyToolResultMessage(input: {
412
+ toolCallId: string;
413
+ toolName: string;
414
+ result: AgentToolResult<unknown>;
415
+ isError: boolean;
416
+ timestamp: number;
417
+ }): ToolResultMessage {
418
+ return {
419
+ role: "toolResult",
420
+ toolCallId: input.toolCallId,
421
+ toolName: input.toolName,
422
+ content: input.result.content ?? [],
423
+ details: input.result.details,
424
+ ...(input.result.usage ? { usage: input.result.usage } : {}),
425
+ ...(input.result.addedToolNames?.length
426
+ ? { addedToolNames: input.result.addedToolNames }
427
+ : {}),
428
+ isError: input.isError,
429
+ timestamp: input.timestamp,
430
+ };
431
+ }
432
+
110
433
  /**
111
434
  * Called synchronously during DO startup, before admission or recovery can execute.
112
435
  * Durable pending work keeps its IDs and state; no old executor runs across this boundary.
@@ -122,6 +445,7 @@ export function migrateLegacyPiSession(
122
445
  try {
123
446
  storage.transactionSync(() => {
124
447
  if (hasTable(sql, "pi_storage_state")) {
448
+ migrateCurrentPiSession(storage);
125
449
  const store = new CloudflareSessionStorage(storage);
126
450
  report = {
127
451
  sourceEntries: 0,
@@ -567,7 +891,7 @@ export function migrateLegacyPiSession(
567
891
  "SELECT submission_id, tool_call_id, tool_name, result, is_error, created_at FROM pi_tool_settlements",
568
892
  )
569
893
  .toArray()) {
570
- const expected = createToolResultMessage({
894
+ const expected = legacyToolResultMessage({
571
895
  toolCallId: row.tool_call_id,
572
896
  toolName: row.tool_name,
573
897
  result: JSON.parse(row.result) as AgentToolResult<unknown>,
@@ -585,6 +909,7 @@ export function migrateLegacyPiSession(
585
909
  "ALTER TABLE pi_session_entries RENAME TO pi_legacy_entries_backup",
586
910
  );
587
911
  const store = new CloudflareSessionStorage(storage);
912
+ removeRetiredRuntimeArtifacts(sql);
588
913
  for (const entry of converted.values()) {
589
914
  const { seq: _seq, timestamp, ...next } = entry;
590
915
  const writes: Write[] = [insertEntry(next)];
@@ -626,6 +951,7 @@ export function migrateLegacyPiSession(
626
951
  values.push(setValue(branchTip(`history:${entry.id}`), entry.id));
627
952
  }
628
953
  store.commitSync([...values, setValue(branchTip("main"), tip)]);
954
+ repairDuplicatedUserSidecars(sql);
629
955
  sql.exec(
630
956
  "UPDATE pi_storage_state SET session_id = ?, created_at = ? WHERE singleton = 1",
631
957
  metadata.id,
@@ -8,7 +8,6 @@ import {
8
8
  setValue,
9
9
  insertEntry,
10
10
  insertUsage,
11
- type CompactionEntry,
12
11
  type Storage,
13
12
  type Write,
14
13
  type CommitResult,
@@ -26,9 +25,10 @@ import {
26
25
  type SessionStats,
27
26
  type SessionMetadata,
28
27
  } from "@earendil-works/pi-agent-core/harness/session";
29
- import type { Usage } from "@earendil-works/pi-ai";
30
28
  import type { Context } from "@earendil-works/pi-agent-core/harness/context";
31
29
 
30
+ export const CURRENT_PI_STORAGE_VERSION = 3;
31
+
32
32
  /** Pi's Storage contract, with every physical commit confined to one DO transaction. */
33
33
  export class CloudflareSessionStorage implements Storage {
34
34
  private closed = false;
@@ -60,13 +60,13 @@ export class CloudflareSessionStorage implements Storage {
60
60
  storage_version: number;
61
61
  }>("SELECT storage_version FROM pi_storage_state WHERE singleton = 1")
62
62
  .one().storage_version;
63
- if (version !== 1)
63
+ if (version !== CURRENT_PI_STORAGE_VERSION)
64
64
  throw new Error(
65
65
  `Unsupported Cloudflare Pi storage version: ${version}`,
66
66
  );
67
67
  }
68
68
  this.sql.exec(`
69
- CREATE TABLE IF NOT EXISTS pi_session_message_submission (message_id TEXT PRIMARY KEY, submission_id TEXT NOT NULL);
69
+ CREATE TABLE IF NOT EXISTS pi_session_entry_submission (entry_id TEXT PRIMARY KEY, submission_id TEXT NOT NULL);
70
70
  CREATE TABLE IF NOT EXISTS pi_entries (
71
71
  id TEXT PRIMARY KEY, seq INTEGER NOT NULL UNIQUE, parent_id TEXT,
72
72
  type TEXT NOT NULL, custom_type TEXT, timestamp INTEGER NOT NULL, body TEXT NOT NULL
@@ -83,7 +83,8 @@ export class CloudflareSessionStorage implements Storage {
83
83
  CREATE TABLE IF NOT EXISTS pi_storage_state (singleton INTEGER PRIMARY KEY CHECK(singleton = 1), next_seq INTEGER NOT NULL, storage_version INTEGER NOT NULL, session_id TEXT NOT NULL, created_at INTEGER NOT NULL, stats TEXT NOT NULL);
84
84
  `);
85
85
  this.sql.exec(
86
- "INSERT OR IGNORE INTO pi_storage_state VALUES (1, 1, 1, ?, ?, ?)",
86
+ "INSERT OR IGNORE INTO pi_storage_state VALUES (1, 1, ?, ?, ?, ?)",
87
+ CURRENT_PI_STORAGE_VERSION,
87
88
  crypto.randomUUID(),
88
89
  Date.now(),
89
90
  JSON.stringify({
@@ -495,7 +496,7 @@ export class CloudflareSessionStorage implements Storage {
495
496
  ]);
496
497
  if (input.submissionId)
497
498
  this.sql.exec(
498
- "INSERT INTO pi_session_message_submission VALUES (?, ?)",
499
+ "INSERT INTO pi_session_entry_submission VALUES (?, ?)",
499
500
  input.id,
500
501
  input.submissionId,
501
502
  );
@@ -503,35 +504,6 @@ export class CloudflareSessionStorage implements Storage {
503
504
  });
504
505
  }
505
506
 
506
- moveBranchTip(id: string | null): void {
507
- if (id !== null && !this.entry(id))
508
- throw new Error(`Unknown branch target: ${id}`);
509
- this.commitSync([setValue(branchTip("main"), id)]);
510
- }
511
-
512
- recordCompaction(
513
- entry: Omit<CompactionEntry, "id" | "parentId" | "seq" | "timestamp"> | undefined,
514
- usage: Usage | undefined = entry?.usage,
515
- ): string {
516
- const id = crypto.randomUUID();
517
- const writes: Write[] = [];
518
- if (entry) writes.push(
519
- insertEntry({
520
- ...entry, id,
521
- parentId: this.getValueSync(branchTip("main"))?.value ?? null,
522
- }),
523
- setValue(branchTip("main"), id),
524
- );
525
- if (usage) writes.push(insertUsage({
526
- id: entry ? crypto.randomUUID() : id,
527
- ...(entry ? { entryId: id } : {}),
528
- usage,
529
- adjustment: false,
530
- }));
531
- this.commitSync(writes);
532
- return id;
533
- }
534
-
535
507
  entries(): Entry[] {
536
508
  return this.sql
537
509
  .exec<{ body: string }>("SELECT body FROM pi_entries ORDER BY seq")
@@ -542,7 +514,7 @@ export class CloudflareSessionStorage implements Storage {
542
514
  clear(): void {
543
515
  this.storage.transactionSync(() => {
544
516
  this.sql.exec(
545
- "DELETE FROM pi_session_message_submission; DELETE FROM pi_entries; DELETE FROM pi_values; DELETE FROM pi_lists; DELETE FROM pi_usage;",
517
+ "DELETE FROM pi_session_entry_submission; DELETE FROM pi_entries; DELETE FROM pi_values; DELETE FROM pi_lists; DELETE FROM pi_usage;",
546
518
  );
547
519
  const usage = {
548
520
  input: 0,
@@ -0,0 +1,39 @@
1
+ import { StorageBackedSession } from "@earendil-works/pi-agent-core/harness/session";
2
+ import {
3
+ migrateCurrentPiSession,
4
+ migrateLegacyPiSession,
5
+ } from "./cloudflare/migration";
6
+ import { CloudflareSessionStorage } from "./cloudflare/storage";
7
+
8
+ export { migrateLegacyPiSession } from "./cloudflare/migration";
9
+ export { CloudflareSessionStorage } from "./cloudflare/storage";
10
+
11
+ /** Open only after the startup transaction has installed the current database format. */
12
+ export function openPiSessionStorage(
13
+ storage: DurableObjectStorage,
14
+ ): CloudflareSessionStorage {
15
+ return storage.transactionSync(() => {
16
+ const tables = new Set(
17
+ storage.sql
18
+ .exec<{ name: string }>(
19
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('pi_storage_state', 'pi_session_entries')",
20
+ )
21
+ .toArray()
22
+ .map((row) => row.name),
23
+ );
24
+ if (!tables.has("pi_storage_state") && tables.has("pi_session_entries")) {
25
+ migrateLegacyPiSession(storage, { dryRun: false });
26
+ } else if (tables.has("pi_storage_state")) {
27
+ migrateCurrentPiSession(storage);
28
+ }
29
+ return new CloudflareSessionStorage(storage);
30
+ });
31
+ }
32
+
33
+ /** Open the single official Pi Session owned by one Cloudflare Agent facet. */
34
+ export function openCloudflareSession(
35
+ storage: DurableObjectStorage,
36
+ ): StorageBackedSession {
37
+ const store = openPiSessionStorage(storage);
38
+ return new StorageBackedSession(store.metadata, store);
39
+ }