@springbrand/agent-runtime 0.3.0-alpha.2 → 0.3.0-alpha.21
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 +11 -9
- package/src/adapter/cloudflare/agent.ts +233 -179
- package/src/adapter/cloudflare/define-agent.ts +252 -104
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/adapter/cloudflare/schema.ts +21 -2
- package/src/adapter/cloudflare/subagent/definition.ts +25 -13
- package/src/adapter/cloudflare/subagent/runner.ts +43 -50
- package/src/adapter/cloudflare/subagent/tools.ts +4 -4
- package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
- package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
- package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
- package/src/config/activation.ts +13 -15
- package/src/config/agent-context.ts +1 -1
- package/src/config/assembler.ts +48 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/harness-runtime.ts +77 -49
- package/src/config/index.ts +1 -1
- package/src/config/model-guidance.ts +20 -0
- package/src/config/ports.ts +57 -7
- package/src/config/{pinning.ts → prepared-runtime.ts} +59 -123
- package/src/config/public-contracts.ts +2 -2
- package/src/config/runtime-config.ts +2 -1
- package/src/context/assembly.ts +1 -1
- package/src/context/extension/assembly.ts +2 -2
- package/src/context/memory/budget/gate.ts +19 -3
- package/src/conversation/harness-resume.ts +89 -238
- package/src/conversation/harness-stream.ts +87 -71
- package/src/conversation/history.ts +154 -152
- package/src/conversation/message/contract.ts +28 -4
- package/src/conversation/message/conversion.ts +46 -5
- package/src/conversation/message/projection.ts +61 -21
- package/src/conversation/session-entry.ts +29 -0
- package/src/conversation/state.ts +23 -2
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +269 -445
- package/src/harness/submission-operation.ts +15 -40
- package/src/index.ts +18 -8
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/prompt.ts +9 -10
- package/src/lib/token-estimate.ts +8 -0
- package/src/model-protocol/openrouter.ts +41 -0
- package/src/models/index.ts +200 -35
- package/src/models/openrouter/messages.ts +13 -8
- package/src/models/openrouter/openai.ts +200 -0
- package/src/models/openrouter/protocol-response.ts +85 -0
- package/src/models/provider-streams.ts +45 -0
- package/src/session/cloudflare/migration.ts +382 -864
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +20 -30
- package/src/submission/contracts.ts +42 -2
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +36 -15
- package/src/submission/lifecycle.ts +28 -15
- package/src/submission/repository.ts +97 -49
- package/src/submission/steer-delivery.ts +32 -76
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/contract.ts +0 -1
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +0 -4
- package/src/telemetry/subagent-lifecycle.ts +1 -1
- package/src/telemetry/turn-events.ts +35 -25
- package/src/tools/README.md +44 -0
- package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
- package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
- package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
- package/src/tools/assistant/ask-user.ts +319 -0
- package/src/tools/assistant/candidate.ts +15 -0
- package/src/tools/assistant/followups.ts +34 -0
- package/src/tools/assistant/index.ts +19 -0
- package/src/tools/assistant/memory.ts +73 -0
- package/src/tools/assistant/plan.ts +98 -0
- package/src/tools/{time.ts → assistant/time.ts} +1 -1
- package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
- package/src/tools/{core.ts → codemode/index.ts} +36 -92
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +29 -26
- package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
- package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
- package/src/tools/runtime/activation.ts +40 -0
- package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
- package/src/tools/runtime/compiler.ts +246 -0
- package/src/tools/runtime/contracts.ts +140 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
- package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
- package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
- package/src/tools/runtime/result.ts +22 -0
- package/src/tools/runtime/unattended-error.ts +4 -0
- package/src/tools/sandbox/index.ts +228 -0
- package/src/tools/schedule/index.ts +301 -0
- package/src/tools/skill/contracts.ts +20 -0
- package/src/tools/skill/index.ts +99 -0
- package/src/tools/skill/resources.ts +322 -0
- package/src/tools/skill/scripts.ts +168 -0
- package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
- package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
- package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
- package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
- package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
- package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
- package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1322 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
- package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
- package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
- package/src/tools/base.ts +0 -534
- package/src/tools/compiler.ts +0 -327
- package/src/tools/schedule.ts +0 -261
- package/src/tools/skill.ts +0 -565
- package/src/tools/web-search/api.ts +0 -1318
- package/src/tools/web-search/index.ts +0 -2
- package/src/tools/workspace-state-connector.ts +0 -146
- package/src/tools/{approval → runtime/approval}/index.ts +0 -0
- package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
- package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
- /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
|
@@ -1,537 +1,14 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type Entry,
|
|
15
|
-
type EntryStructure,
|
|
16
|
-
type EntryScan,
|
|
17
|
-
type StorageBranchScan,
|
|
18
|
-
type UsageRow,
|
|
19
|
-
type UsageScan,
|
|
20
|
-
type Value,
|
|
21
|
-
type ValueList,
|
|
22
|
-
type StoredValue,
|
|
23
|
-
type ListElement,
|
|
24
|
-
type ListReadOptions,
|
|
25
|
-
type SessionStats,
|
|
26
|
-
type SessionMetadata,
|
|
27
|
-
} from "@earendil-works/pi-agent-core/harness/session";
|
|
28
|
-
import type { Context } from "@earendil-works/pi-agent-core/harness/context";
|
|
29
|
-
|
|
30
|
-
export const CURRENT_PI_STORAGE_VERSION = 3;
|
|
31
|
-
|
|
32
|
-
/** Pi's Storage contract, with every physical commit confined to one DO transaction. */
|
|
33
|
-
export class CloudflareSessionStorage implements Storage {
|
|
34
|
-
private closed = false;
|
|
35
|
-
private readonly sql: SqlStorage;
|
|
36
|
-
readonly metadata: SessionMetadata;
|
|
37
|
-
|
|
38
|
-
constructor(private readonly storage: DurableObjectStorage) {
|
|
39
|
-
this.sql = storage.sql;
|
|
40
|
-
const hasVersion =
|
|
41
|
-
this.sql
|
|
42
|
-
.exec(
|
|
43
|
-
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'pi_storage_state'",
|
|
44
|
-
)
|
|
45
|
-
.toArray().length > 0;
|
|
46
|
-
const hasLegacy =
|
|
47
|
-
this.sql
|
|
48
|
-
.exec(
|
|
49
|
-
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'pi_session_entries'",
|
|
50
|
-
)
|
|
51
|
-
.toArray().length > 0;
|
|
52
|
-
if (!hasVersion && hasLegacy) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
"Pi session requires an explicit legacy-to-0.85 migration",
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
if (hasVersion) {
|
|
58
|
-
const version = this.sql
|
|
59
|
-
.exec<{
|
|
60
|
-
storage_version: number;
|
|
61
|
-
}>("SELECT storage_version FROM pi_storage_state WHERE singleton = 1")
|
|
62
|
-
.one().storage_version;
|
|
63
|
-
if (version !== CURRENT_PI_STORAGE_VERSION)
|
|
64
|
-
throw new Error(
|
|
65
|
-
`Unsupported Cloudflare Pi storage version: ${version}`,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
this.sql.exec(`
|
|
69
|
-
CREATE TABLE IF NOT EXISTS pi_session_entry_submission (entry_id TEXT PRIMARY KEY, submission_id TEXT NOT NULL);
|
|
70
|
-
CREATE TABLE IF NOT EXISTS pi_entries (
|
|
71
|
-
id TEXT PRIMARY KEY, seq INTEGER NOT NULL UNIQUE, parent_id TEXT,
|
|
72
|
-
type TEXT NOT NULL, custom_type TEXT, timestamp INTEGER NOT NULL, body TEXT NOT NULL
|
|
73
|
-
);
|
|
74
|
-
CREATE TABLE IF NOT EXISTS pi_values (
|
|
75
|
-
namespace TEXT NOT NULL, key TEXT NOT NULL, seq INTEGER NOT NULL, body TEXT NOT NULL,
|
|
76
|
-
PRIMARY KEY(namespace, key)
|
|
77
|
-
);
|
|
78
|
-
CREATE TABLE IF NOT EXISTS pi_lists (
|
|
79
|
-
namespace TEXT NOT NULL, key TEXT NOT NULL, seq INTEGER PRIMARY KEY, body TEXT NOT NULL
|
|
80
|
-
);
|
|
81
|
-
CREATE INDEX IF NOT EXISTS pi_lists_address ON pi_lists(namespace, key, seq);
|
|
82
|
-
CREATE TABLE IF NOT EXISTS pi_usage (id TEXT PRIMARY KEY, seq INTEGER NOT NULL UNIQUE, body TEXT NOT NULL);
|
|
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
|
-
`);
|
|
85
|
-
this.sql.exec(
|
|
86
|
-
"INSERT OR IGNORE INTO pi_storage_state VALUES (1, 1, ?, ?, ?, ?)",
|
|
87
|
-
CURRENT_PI_STORAGE_VERSION,
|
|
88
|
-
crypto.randomUUID(),
|
|
89
|
-
Date.now(),
|
|
90
|
-
JSON.stringify({
|
|
91
|
-
messageCount: 0,
|
|
92
|
-
usage: {
|
|
93
|
-
input: 0,
|
|
94
|
-
output: 0,
|
|
95
|
-
cacheRead: 0,
|
|
96
|
-
cacheWrite: 0,
|
|
97
|
-
totalTokens: 0,
|
|
98
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
99
|
-
},
|
|
100
|
-
}),
|
|
101
|
-
);
|
|
102
|
-
const row = this.sql
|
|
103
|
-
.exec<{
|
|
104
|
-
storage_version: number;
|
|
105
|
-
session_id: string;
|
|
106
|
-
created_at: number;
|
|
107
|
-
}>(
|
|
108
|
-
"SELECT storage_version, session_id, created_at FROM pi_storage_state WHERE singleton = 1",
|
|
109
|
-
)
|
|
110
|
-
.one();
|
|
111
|
-
this.metadata = {
|
|
112
|
-
id: row.session_id,
|
|
113
|
-
createdAt: row.created_at,
|
|
114
|
-
storageVersion: row.storage_version,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private assertOpen(context?: Context): void {
|
|
119
|
-
if (this.closed) throw new Error("Session storage is closed");
|
|
120
|
-
context?.abortSignal?.throwIfAborted();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Synchronous boundary also used by the host's admission/settlement transaction. */
|
|
124
|
-
commitSync(writes: Write[], context?: Context): CommitResult {
|
|
125
|
-
this.assertOpen(context);
|
|
126
|
-
return this.storage.transactionSync(() => {
|
|
127
|
-
const firstSeq = this.sql
|
|
128
|
-
.exec<{
|
|
129
|
-
next_seq: number;
|
|
130
|
-
}>("SELECT next_seq FROM pi_storage_state WHERE singleton = 1")
|
|
131
|
-
.one().next_seq;
|
|
132
|
-
const prepared = prepareStorageCommit(writes, firstSeq, Date.now());
|
|
133
|
-
validateCommittedWrites(prepared.writes, firstSeq, {
|
|
134
|
-
hasEntryId: (id) =>
|
|
135
|
-
this.sql.exec("SELECT 1 FROM pi_entries WHERE id = ?", id).toArray()
|
|
136
|
-
.length > 0,
|
|
137
|
-
hasEntryOrUsageId: (id) =>
|
|
138
|
-
this.sql
|
|
139
|
-
.exec(
|
|
140
|
-
"SELECT 1 FROM pi_entries WHERE id = ? UNION ALL SELECT 1 FROM pi_usage WHERE id = ?",
|
|
141
|
-
id,
|
|
142
|
-
id,
|
|
143
|
-
)
|
|
144
|
-
.toArray().length > 0,
|
|
145
|
-
});
|
|
146
|
-
const stats = this.statsSync();
|
|
147
|
-
for (const write of prepared.writes) {
|
|
148
|
-
switch (write.kind) {
|
|
149
|
-
case "entry": {
|
|
150
|
-
const { kind: _kind, ...entry } = write;
|
|
151
|
-
if (entry.type === "message") stats.messageCount += 1;
|
|
152
|
-
this.sql.exec(
|
|
153
|
-
"INSERT INTO pi_entries VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
154
|
-
entry.id,
|
|
155
|
-
entry.seq,
|
|
156
|
-
entry.parentId,
|
|
157
|
-
entry.type,
|
|
158
|
-
entry.customType ?? null,
|
|
159
|
-
entry.timestamp,
|
|
160
|
-
JSON.stringify(entry),
|
|
161
|
-
);
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
case "usage": {
|
|
165
|
-
const { kind: _kind, ...row } = write;
|
|
166
|
-
for (const key of [
|
|
167
|
-
"input",
|
|
168
|
-
"output",
|
|
169
|
-
"cacheRead",
|
|
170
|
-
"cacheWrite",
|
|
171
|
-
"totalTokens",
|
|
172
|
-
"cacheWrite1h",
|
|
173
|
-
"reasoning",
|
|
174
|
-
] as const) {
|
|
175
|
-
if (row.usage[key] !== undefined)
|
|
176
|
-
stats.usage[key] = (stats.usage[key] ?? 0) + row.usage[key];
|
|
177
|
-
}
|
|
178
|
-
for (const key of [
|
|
179
|
-
"input",
|
|
180
|
-
"output",
|
|
181
|
-
"cacheRead",
|
|
182
|
-
"cacheWrite",
|
|
183
|
-
"total",
|
|
184
|
-
] as const)
|
|
185
|
-
stats.usage.cost[key] += row.usage.cost[key];
|
|
186
|
-
this.sql.exec(
|
|
187
|
-
"INSERT INTO pi_usage VALUES (?, ?, ?)",
|
|
188
|
-
row.id,
|
|
189
|
-
row.seq,
|
|
190
|
-
JSON.stringify(row),
|
|
191
|
-
);
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
case "value":
|
|
195
|
-
if (write.op === "delete")
|
|
196
|
-
this.sql.exec(
|
|
197
|
-
"DELETE FROM pi_values WHERE namespace = ? AND key = ?",
|
|
198
|
-
write.namespace,
|
|
199
|
-
write.key,
|
|
200
|
-
);
|
|
201
|
-
else
|
|
202
|
-
this.sql.exec(
|
|
203
|
-
"INSERT INTO pi_values VALUES (?, ?, ?, ?) ON CONFLICT(namespace, key) DO UPDATE SET seq = excluded.seq, body = excluded.body",
|
|
204
|
-
write.namespace,
|
|
205
|
-
write.key,
|
|
206
|
-
write.seq,
|
|
207
|
-
JSON.stringify(write.value),
|
|
208
|
-
);
|
|
209
|
-
break;
|
|
210
|
-
case "list":
|
|
211
|
-
if (write.op === "delete")
|
|
212
|
-
this.sql.exec(
|
|
213
|
-
"DELETE FROM pi_lists WHERE namespace = ? AND key = ?",
|
|
214
|
-
write.namespace,
|
|
215
|
-
write.key,
|
|
216
|
-
);
|
|
217
|
-
else
|
|
218
|
-
this.sql.exec(
|
|
219
|
-
"INSERT INTO pi_lists VALUES (?, ?, ?, ?)",
|
|
220
|
-
write.namespace,
|
|
221
|
-
write.key,
|
|
222
|
-
write.seq,
|
|
223
|
-
JSON.stringify(write.value),
|
|
224
|
-
);
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
this.sql.exec(
|
|
229
|
-
"UPDATE pi_storage_state SET next_seq = ?, stats = ? WHERE singleton = 1",
|
|
230
|
-
firstSeq + writes.length,
|
|
231
|
-
JSON.stringify(stats),
|
|
232
|
-
);
|
|
233
|
-
return { ...prepared.result, stats };
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async commit(writes: Write[], context: Context): Promise<CommitResult> {
|
|
238
|
-
return this.commitSync(writes, context);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async getEntries(
|
|
242
|
-
ids: string[],
|
|
243
|
-
context: Context,
|
|
244
|
-
): Promise<Map<string, Entry>> {
|
|
245
|
-
return this.getEntriesSync(ids, context);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
getEntriesSync(ids: string[], context?: Context): Map<string, Entry> {
|
|
249
|
-
this.assertOpen(context);
|
|
250
|
-
const entries = new Map<string, Entry>();
|
|
251
|
-
for (const id of ids) {
|
|
252
|
-
const row = this.sql
|
|
253
|
-
.exec<{ body: string }>("SELECT body FROM pi_entries WHERE id = ?", id)
|
|
254
|
-
.toArray()[0];
|
|
255
|
-
if (row) entries.set(id, JSON.parse(row.body));
|
|
256
|
-
}
|
|
257
|
-
return entries;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async getValue<T>(
|
|
261
|
-
address: Value<T>,
|
|
262
|
-
context: Context,
|
|
263
|
-
): Promise<StoredValue<T> | undefined> {
|
|
264
|
-
return this.getValueSync(address, context);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
getValueSync<T>(
|
|
268
|
-
address: Value<T>,
|
|
269
|
-
context?: Context,
|
|
270
|
-
): StoredValue<T> | undefined {
|
|
271
|
-
this.assertOpen(context);
|
|
272
|
-
const row = this.sql
|
|
273
|
-
.exec<{
|
|
274
|
-
seq: number;
|
|
275
|
-
body: string;
|
|
276
|
-
}>(
|
|
277
|
-
"SELECT seq, body FROM pi_values WHERE namespace = ? AND key = ?",
|
|
278
|
-
address.namespace,
|
|
279
|
-
address.key,
|
|
280
|
-
)
|
|
281
|
-
.toArray()[0];
|
|
282
|
-
return row
|
|
283
|
-
? { address, seq: row.seq, value: JSON.parse(row.body) }
|
|
284
|
-
: undefined;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async scanValues<T>(
|
|
288
|
-
prefix: Value<T>,
|
|
289
|
-
context: Context,
|
|
290
|
-
): Promise<StoredValue<T>[]> {
|
|
291
|
-
this.assertOpen(context);
|
|
292
|
-
return this.sql
|
|
293
|
-
.exec<{ key: string; seq: number; body: string }>(
|
|
294
|
-
"SELECT key, seq, body FROM pi_values WHERE namespace = ? AND substr(key, 1, length(?)) = ? ORDER BY key COLLATE BINARY",
|
|
295
|
-
prefix.namespace,
|
|
296
|
-
prefix.key,
|
|
297
|
-
prefix.key,
|
|
298
|
-
)
|
|
299
|
-
.toArray()
|
|
300
|
-
.map((row) => ({
|
|
301
|
-
address: value<T>(prefix.namespace, row.key),
|
|
302
|
-
seq: row.seq,
|
|
303
|
-
value: JSON.parse(row.body),
|
|
304
|
-
}));
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
async readList<T>(
|
|
308
|
-
address: ValueList<T>,
|
|
309
|
-
options: ListReadOptions | undefined,
|
|
310
|
-
context: Context,
|
|
311
|
-
): Promise<ListElement<T>[]> {
|
|
312
|
-
this.assertOpen(context);
|
|
313
|
-
const resolved = resolveListReadOptions(options);
|
|
314
|
-
const descending = resolved.order === "desc";
|
|
315
|
-
const comparison = descending ? "<" : ">";
|
|
316
|
-
return this.sql
|
|
317
|
-
.exec<{ seq: number; body: string }>(
|
|
318
|
-
`SELECT seq, body FROM pi_lists WHERE namespace = ? AND key = ? AND (? IS NULL OR seq ${comparison} ?) ORDER BY seq ${descending ? "DESC" : "ASC"} LIMIT ?`,
|
|
319
|
-
address.namespace,
|
|
320
|
-
address.key,
|
|
321
|
-
resolved.cursor?.seq ?? null,
|
|
322
|
-
resolved.cursor?.seq ?? null,
|
|
323
|
-
resolved.limit,
|
|
324
|
-
)
|
|
325
|
-
.toArray()
|
|
326
|
-
.map((row) => ({ seq: row.seq, value: JSON.parse(row.body) }));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private branchRows(query: StorageBranchScan, structure: boolean) {
|
|
330
|
-
// Stops follow the requested traversal direction; filters must not hide a stop entry.
|
|
331
|
-
const oldestFirst = query.order === "oldestFirst";
|
|
332
|
-
const rows = this.sql
|
|
333
|
-
.exec<{
|
|
334
|
-
id: string;
|
|
335
|
-
parentId: string | null;
|
|
336
|
-
seq: number;
|
|
337
|
-
timestamp: number;
|
|
338
|
-
type: Entry["type"];
|
|
339
|
-
customType: string | null;
|
|
340
|
-
body?: string;
|
|
341
|
-
}>(
|
|
342
|
-
`
|
|
343
|
-
WITH RECURSIVE path AS (
|
|
344
|
-
SELECT id, parent_id, seq, timestamp, type, custom_type${structure ? "" : ", body"} FROM pi_entries WHERE id = ?
|
|
345
|
-
UNION ALL
|
|
346
|
-
SELECT e.id, e.parent_id, e.seq, e.timestamp, e.type, e.custom_type${structure ? "" : ", e.body"}
|
|
347
|
-
FROM pi_entries e JOIN path p ON e.id = p.parent_id
|
|
348
|
-
) SELECT id, parent_id AS parentId, seq, timestamp, type, custom_type AS customType${structure ? "" : ", body"}
|
|
349
|
-
FROM path ORDER BY seq ${oldestFirst ? "ASC" : "DESC"}
|
|
350
|
-
`,
|
|
351
|
-
query.start,
|
|
352
|
-
)
|
|
353
|
-
.toArray();
|
|
354
|
-
if (!rows.length) throw new Error(`Unknown branch start: ${query.start}`);
|
|
355
|
-
const root = oldestFirst ? rows[0]! : rows.at(-1)!;
|
|
356
|
-
if (root.parentId !== null)
|
|
357
|
-
throw new Error("Corrupt branch: missing parent");
|
|
358
|
-
const stop = rows.findIndex(
|
|
359
|
-
(row) => row.id === query.stopAtId || row.type === query.stopAtType,
|
|
360
|
-
);
|
|
361
|
-
return (stop < 0 ? rows : rows.slice(0, stop + 1))
|
|
362
|
-
.filter(
|
|
363
|
-
(row) =>
|
|
364
|
-
(query.type === undefined || query.type === row.type) &&
|
|
365
|
-
(query.customType === undefined ||
|
|
366
|
-
query.customType === row.customType) &&
|
|
367
|
-
(query.cursor === undefined ||
|
|
368
|
-
(oldestFirst
|
|
369
|
-
? row.seq > query.cursor.seq
|
|
370
|
-
: row.seq < query.cursor.seq)),
|
|
371
|
-
)
|
|
372
|
-
.slice(
|
|
373
|
-
0,
|
|
374
|
-
query.limit === undefined ? undefined : Math.max(0, query.limit),
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
async scanBranch(
|
|
379
|
-
query: StorageBranchScan,
|
|
380
|
-
context: Context,
|
|
381
|
-
): Promise<Entry[]> {
|
|
382
|
-
return this.scanBranchSync(query, context);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
scanBranchSync(query: StorageBranchScan, context?: Context): Entry[] {
|
|
386
|
-
this.assertOpen(context);
|
|
387
|
-
return this.branchRows(query, false).map((row) => JSON.parse(row.body!));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async scanBranchStructure(
|
|
391
|
-
query: StorageBranchScan,
|
|
392
|
-
context: Context,
|
|
393
|
-
): Promise<EntryStructure[]> {
|
|
394
|
-
this.assertOpen(context);
|
|
395
|
-
return this.branchRows(query, true).map(
|
|
396
|
-
({ customType, body: _body, ...row }) => ({
|
|
397
|
-
...row,
|
|
398
|
-
...(customType === null ? {} : { customType }),
|
|
399
|
-
}),
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async scanEntries(query: EntryScan, context: Context): Promise<Entry[]> {
|
|
404
|
-
this.assertOpen(context);
|
|
405
|
-
return this.sql
|
|
406
|
-
.exec<{ body: string }>(
|
|
407
|
-
`
|
|
408
|
-
SELECT body FROM pi_entries WHERE (? IS NULL OR type = ?) AND (? IS NULL OR custom_type = ?)
|
|
409
|
-
AND (? IS NULL OR seq >= ?) AND (? IS NULL OR seq <= ?) ORDER BY seq ${query.order === "desc" ? "DESC" : "ASC"} LIMIT ?`,
|
|
410
|
-
query.type ?? null,
|
|
411
|
-
query.type ?? null,
|
|
412
|
-
query.customType ?? null,
|
|
413
|
-
query.customType ?? null,
|
|
414
|
-
query.fromSeq ?? null,
|
|
415
|
-
query.fromSeq ?? null,
|
|
416
|
-
query.toSeq ?? null,
|
|
417
|
-
query.toSeq ?? null,
|
|
418
|
-
query.limit === undefined ? -1 : Math.max(0, Math.trunc(query.limit)),
|
|
419
|
-
)
|
|
420
|
-
.toArray()
|
|
421
|
-
.map((row) => JSON.parse(row.body));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
async scanUsage(query: UsageScan, context: Context): Promise<UsageRow[]> {
|
|
425
|
-
this.assertOpen(context);
|
|
426
|
-
return this.sql
|
|
427
|
-
.exec<{ body: string }>(
|
|
428
|
-
`
|
|
429
|
-
SELECT body FROM pi_usage WHERE (? IS NULL OR seq >= ?) AND (? IS NULL OR seq <= ?)
|
|
430
|
-
ORDER BY seq ${query.order === "desc" ? "DESC" : "ASC"} LIMIT ?`,
|
|
431
|
-
query.fromSeq ?? null,
|
|
432
|
-
query.fromSeq ?? null,
|
|
433
|
-
query.toSeq ?? null,
|
|
434
|
-
query.toSeq ?? null,
|
|
435
|
-
query.limit === undefined ? -1 : Math.max(0, Math.trunc(query.limit)),
|
|
436
|
-
)
|
|
437
|
-
.toArray()
|
|
438
|
-
.map((row) => JSON.parse(row.body));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
private statsSync(): SessionStats {
|
|
442
|
-
return JSON.parse(
|
|
443
|
-
this.sql
|
|
444
|
-
.exec<{
|
|
445
|
-
stats: string;
|
|
446
|
-
}>("SELECT stats FROM pi_storage_state WHERE singleton = 1")
|
|
447
|
-
.one().stats,
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async getStats(context: Context): Promise<SessionStats> {
|
|
452
|
-
this.assertOpen(context);
|
|
453
|
-
return this.statsSync();
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/** Host commits use this single branch; off-branch entries remain available by ID. */
|
|
457
|
-
branch(): Entry[] {
|
|
458
|
-
const tip = this.getValueSync(branchTip("main"))?.value;
|
|
459
|
-
return tip ? this.scanBranchSync({ start: tip, order: "oldestFirst" }) : [];
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
entry(id: string): Entry | undefined {
|
|
463
|
-
return this.getEntriesSync([id]).get(id);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
appendMessage(input: {
|
|
467
|
-
id: string;
|
|
468
|
-
message: AgentMessage;
|
|
469
|
-
submissionId?: string;
|
|
470
|
-
}): boolean {
|
|
471
|
-
return this.storage.transactionSync(() => {
|
|
472
|
-
if (this.entry(input.id)) return false;
|
|
473
|
-
const usage =
|
|
474
|
-
input.message.role === "assistant" ||
|
|
475
|
-
input.message.role === "toolResult"
|
|
476
|
-
? input.message.usage
|
|
477
|
-
: undefined;
|
|
478
|
-
this.commitSync([
|
|
479
|
-
insertEntry({
|
|
480
|
-
id: input.id,
|
|
481
|
-
parentId: this.getValueSync(branchTip("main"))?.value ?? null,
|
|
482
|
-
type: "message",
|
|
483
|
-
message: input.message,
|
|
484
|
-
}),
|
|
485
|
-
setValue(branchTip("main"), input.id),
|
|
486
|
-
...(usage
|
|
487
|
-
? [
|
|
488
|
-
insertUsage({
|
|
489
|
-
id: crypto.randomUUID(),
|
|
490
|
-
entryId: input.id,
|
|
491
|
-
usage,
|
|
492
|
-
adjustment: false,
|
|
493
|
-
}),
|
|
494
|
-
]
|
|
495
|
-
: []),
|
|
496
|
-
]);
|
|
497
|
-
if (input.submissionId)
|
|
498
|
-
this.sql.exec(
|
|
499
|
-
"INSERT INTO pi_session_entry_submission VALUES (?, ?)",
|
|
500
|
-
input.id,
|
|
501
|
-
input.submissionId,
|
|
502
|
-
);
|
|
503
|
-
return true;
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
entries(): Entry[] {
|
|
508
|
-
return this.sql
|
|
509
|
-
.exec<{ body: string }>("SELECT body FROM pi_entries ORDER BY seq")
|
|
510
|
-
.toArray()
|
|
511
|
-
.map((row) => JSON.parse(row.body));
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
clear(): void {
|
|
515
|
-
this.storage.transactionSync(() => {
|
|
516
|
-
this.sql.exec(
|
|
517
|
-
"DELETE FROM pi_session_entry_submission; DELETE FROM pi_entries; DELETE FROM pi_values; DELETE FROM pi_lists; DELETE FROM pi_usage;",
|
|
518
|
-
);
|
|
519
|
-
const usage = {
|
|
520
|
-
input: 0,
|
|
521
|
-
output: 0,
|
|
522
|
-
cacheRead: 0,
|
|
523
|
-
cacheWrite: 0,
|
|
524
|
-
totalTokens: 0,
|
|
525
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
526
|
-
};
|
|
527
|
-
this.sql.exec(
|
|
528
|
-
"UPDATE pi_storage_state SET stats = ? WHERE singleton = 1",
|
|
529
|
-
JSON.stringify({ messageCount: 0, usage }),
|
|
530
|
-
);
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
async close(_context?: Context): Promise<void> {
|
|
535
|
-
this.closed = true;
|
|
536
|
-
}
|
|
1
|
+
import { defineDoc, type ConversationId, type Storage } from "@earendil-works/pi-durable";
|
|
2
|
+
import { openDurableObjectSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/cloudflare";
|
|
3
|
+
|
|
4
|
+
/** The host selects a native conversation when regenerating or clearing history. */
|
|
5
|
+
export const RuntimeConversation = defineDoc({
|
|
6
|
+
kind: "springbrand.conversation",
|
|
7
|
+
version: 1,
|
|
8
|
+
scope: "session",
|
|
9
|
+
initial: (): { id: ConversationId | null } => ({ id: null }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export async function openNativeStorage(storage: DurableObjectStorage): Promise<Storage> {
|
|
13
|
+
return openDurableObjectSqliteStorage(storage);
|
|
537
14
|
}
|
package/src/session/index.ts
CHANGED
|
@@ -1,39 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./cloudflare/migration";
|
|
6
|
-
import { CloudflareSessionStorage } from "./cloudflare/storage";
|
|
1
|
+
import { createSession, type Storage } from "@earendil-works/pi-durable";
|
|
2
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
3
|
+
import { migratePiSession } from "./cloudflare/migration";
|
|
4
|
+
import { openNativeStorage } from "./cloudflare/storage";
|
|
7
5
|
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
6
|
+
export { RuntimeConversation } from "./cloudflare/storage";
|
|
7
|
+
export { migratePiSession } from "./cloudflare/migration";
|
|
10
8
|
|
|
11
|
-
|
|
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
|
-
}
|
|
9
|
+
const migrations = new WeakMap<DurableObjectStorage, Promise<void>>();
|
|
32
10
|
|
|
33
|
-
/**
|
|
34
|
-
export function openCloudflareSession(
|
|
35
|
-
storage
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
11
|
+
/** Migration completes before native execution or history reads are admitted. */
|
|
12
|
+
export async function openCloudflareSession(storage: DurableObjectStorage): Promise<Storage> {
|
|
13
|
+
let migrating = migrations.get(storage);
|
|
14
|
+
if (!migrating) {
|
|
15
|
+
migrating = (async () => {
|
|
16
|
+
const native = await openNativeStorage(storage);
|
|
17
|
+
const session = createSession(native);
|
|
18
|
+
try { await migratePiSession(storage, session); }
|
|
19
|
+
finally { await session.close(BACKGROUND_CONTEXT); }
|
|
20
|
+
})();
|
|
21
|
+
migrations.set(storage, migrating);
|
|
22
|
+
migrating.catch(() => migrations.delete(storage));
|
|
23
|
+
}
|
|
24
|
+
await migrating;
|
|
25
|
+
return openNativeStorage(storage);
|
|
39
26
|
}
|
|
@@ -163,7 +163,7 @@ export const SPRINGBRAND_WORKER_WEBSITE_SKILL = {
|
|
|
163
163
|
"5. Call `get_browser_console_logs` for the home page and for every primary internal page. Check navigation responses, missing assets, exceptions, CSP or CORS failures, and client fetch failures.",
|
|
164
164
|
"6. Open the Website with the available browser evidence tools. For a multi-page Website, follow real links from the home page to every primary internal page and verify navigation back to the home page. Check at least the normal desktop viewport and one narrow viewport; capture and inspect a screenshot when visual appearance matters.",
|
|
165
165
|
"7. Fix every failure, redeploy, and repeat the same checks. A clean home page does not prove a multi-page Website works.",
|
|
166
|
-
"8. After browser verification succeeds, call `save_creation` exactly once with the existing entry HTML Working Copy path. For the recommended layout, that is `/creations/<website-name>/public/index.html`.",
|
|
166
|
+
"8. After browser verification succeeds, call `save_creation` exactly once with a user-visible title and the existing entry HTML Working Copy path. For the recommended layout, that path is `/creations/<website-name>/public/index.html`.",
|
|
167
167
|
"9. Do not call the Website complete until `save_creation` returns an `artifactId`. Do not save referenced assets as separate Creations.",
|
|
168
168
|
"",
|
|
169
169
|
"## Publication contract",
|