@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.1
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.
- package/package.json +3 -3
- package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
- package/src/adapter/cloudflare/agent.ts +1539 -0
- package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
- package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +25 -35
- package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
- package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
- package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
- package/src/{db → adapter/cloudflare}/schema.ts +16 -83
- package/src/adapter/cloudflare/subagent/definition.ts +63 -33
- package/src/adapter/cloudflare/subagent/runner.ts +174 -120
- package/src/adapter/cloudflare/subagent/tools.ts +13 -14
- package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
- package/src/adapter/cloudflare/universal-agent/tools.ts +10 -10
- package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
- package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
- package/src/config/activation.ts +140 -0
- package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
- package/src/{runtime-assembler.ts → config/assembler.ts} +20 -19
- package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
- package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
- package/src/{runtime-definition.ts → config/definition.ts} +13 -13
- package/src/config/harness-runtime.ts +182 -0
- package/src/config/index.ts +14 -0
- package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +38 -16
- package/src/{kernel/bindings.ts → config/ports.ts} +9 -13
- package/src/{kernel → config}/profile.ts +7 -0
- package/src/{kernel → config}/public-contracts.ts +6 -6
- package/src/{kernel → config}/runtime-load.ts +1 -1
- package/src/{pi/assembly/context.ts → context/assembly.ts} +4 -206
- package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
- package/src/context/extension/index.ts +3 -0
- package/src/context/index.ts +6 -0
- package/src/{layers/context → context/memory}/budget/gate.ts +2 -2
- package/src/conversation/harness-resume.ts +288 -0
- package/src/conversation/harness-stream.ts +98 -0
- package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
- package/src/conversation/index.ts +6 -0
- package/src/{pi → conversation}/message/contract.ts +3 -0
- package/src/{pi → conversation}/message/conversion.ts +75 -0
- package/src/{pi → conversation}/message/index.ts +1 -0
- package/src/{pi → conversation}/message/model-input.ts +1 -1
- package/src/{pi → conversation}/message/projection.ts +76 -10
- package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
- package/src/conversation/state.ts +175 -0
- package/src/harness/index.ts +7 -0
- package/src/harness/session-harness.ts +481 -0
- package/src/harness/submission-operation.ts +49 -0
- package/src/index.ts +38 -43
- package/src/lib/mcp.ts +1 -1
- package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -18
- package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
- package/src/{pi/session → session/cloudflare}/migration.ts +334 -8
- package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
- package/src/session/index.ts +39 -0
- package/src/submission/admission.ts +275 -0
- package/src/{kernel/receipts.ts → submission/contracts.ts} +14 -0
- package/src/submission/delivery.ts +212 -0
- package/src/submission/finalization.ts +111 -0
- package/src/submission/index.ts +9 -0
- package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
- package/src/{db/submission.repo.ts → submission/repository.ts} +61 -118
- package/src/submission/steer-delivery.ts +167 -0
- package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
- package/src/telemetry/contract.ts +12 -0
- package/src/telemetry/coordinator.ts +65 -0
- package/src/telemetry/delivery.ts +2 -2
- package/src/telemetry/index.ts +5 -0
- package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
- package/src/telemetry/recorder.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +12 -0
- package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
- package/src/telemetry/turn-events.ts +279 -0
- package/src/{pi/tool → tools}/ai-adapter.ts +2 -2
- package/src/tools/approval/index.ts +1 -0
- package/src/{db/approval.repo.ts → tools/approval/repository.ts} +10 -30
- package/src/{pi/tool → tools}/base.ts +14 -30
- package/src/tools/compiler.ts +327 -0
- package/src/{pi/tool → tools}/core-host.ts +3 -3
- package/src/{pi/tool → tools}/core.ts +3 -3
- package/src/tools/decision-gate.ts +227 -0
- package/src/{pi/tool → tools}/gateway.ts +1 -1
- package/src/tools/human-decisions.ts +127 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/interaction/index.ts +1 -0
- package/src/{pi/tool → tools}/mcp.ts +1 -1
- package/src/{pi/tool → tools}/schedule.ts +3 -3
- package/src/{pi/tool → tools}/skill.ts +4 -4
- package/src/{kernel → tools}/subagent-runtime.ts +1 -1
- package/src/{pi/tool → tools}/subagent.ts +4 -4
- package/src/tools/temporary-agent/approvals.ts +126 -0
- package/src/{layers/orchestration/temporary-agent/core.ts → tools/temporary-agent/coordinator.ts} +41 -49
- package/src/tools/temporary-agent/core.ts +53 -0
- package/src/{layers/orchestration → tools}/temporary-agent/runner.ts +1 -1
- package/src/{layers/orchestration → tools}/temporary-agent/workspace.ts +1 -1
- package/src/{pi/tool → tools}/web-search/api.ts +1 -1
- package/src/{pi/tool → tools}/workspace-revision.ts +2 -2
- package/src/{pi/tool → tools}/workspace-sandbox.ts +2 -2
- package/src/{pi/tool → tools}/workspace-state-connector.ts +1 -1
- package/src/db/milestone.repo.ts +0 -104
- package/src/db/settlement.repo.ts +0 -104
- package/src/kernel/approval-lifecycle.ts +0 -638
- package/src/kernel/interaction-lifecycle.ts +0 -433
- package/src/kernel/recoverable-chat-agent.ts +0 -1020
- package/src/kernel/state.ts +0 -91
- package/src/pi/assembly/index.ts +0 -19
- package/src/pi/runtime-adapter/execution.ts +0 -1163
- package/src/pi/runtime-adapter/index.ts +0 -293
- package/src/pi/runtime-adapter/recovery.ts +0 -982
- package/src/pi/session/index.ts +0 -24
- package/src/pi/tool/compiler.ts +0 -483
- package/src/pi/tool/index.ts +0 -38
- package/src/pi/turn/approval.ts +0 -181
- package/src/pi/turn/index.ts +0 -82
- package/src/pi/turn/interaction.ts +0 -181
- package/src/runtime.ts +0 -4760
- /package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
- /package/src/{kernel → config}/degradation.ts +0 -0
- /package/src/{kernel → config}/runtime-config.ts +0 -0
- /package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
- /package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
- /package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
- /package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
- /package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
- /package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
- /package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
- /package/src/{pi/tool → tools}/declared.ts +0 -0
- /package/src/{db/interaction.repo.ts → tools/interaction/repository.ts} +0 -0
- /package/src/{pi/tool → tools}/nested-tools.ts +0 -0
- /package/src/{kernel/submission-authority.ts → tools/subagent-authority.ts} +0 -0
- /package/src/{db/agent-tool.repo.ts → tools/subagent-run-repository.ts} +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/contract.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/extract/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/fanout/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/registry.ts +0 -0
- /package/src/{pi/tool → tools}/time.ts +0 -0
- /package/src/{pi/tool → tools}/web-fetch.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/index.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/web-search.ts +0 -0
- /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 "
|
|
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
|
-
|
|
23
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
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 !==
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
+
}
|