@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.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/AGENTS.md +30 -0
- package/README.md +16 -2
- package/dist/agents.js +11 -0
- package/dist/cli-adapter.js +8 -0
- package/dist/component.js +65 -0
- package/dist/config-schema.js +265 -0
- package/dist/execution/activity.js +78 -0
- package/dist/execution/content.js +184 -0
- package/dist/execution/json-sections.js +231 -0
- package/dist/execution/presentation.js +79 -0
- package/dist/execution/preview.js +13 -0
- package/dist/execution/process.js +125 -0
- package/dist/execution/projection.js +327 -0
- package/dist/execution/query.js +18 -0
- package/dist/execution/routes.js +189 -0
- package/dist/execution/schema.js +322 -0
- package/dist/execution/service.js +1993 -0
- package/dist/features.js +22 -0
- package/dist/http.js +65 -0
- package/dist/performance/analysis.js +330 -0
- package/dist/performance/catalog.js +391 -0
- package/dist/performance/dashboard.js +483 -0
- package/dist/performance/error.js +15 -0
- package/dist/performance/index.js +6 -0
- package/dist/performance/inflight.js +43 -0
- package/dist/performance/projection.js +138 -0
- package/dist/performance/prompt/base.txt +38 -0
- package/dist/performance/prompt/builder.js +21 -0
- package/dist/performance/routes/performance-route.js +439 -0
- package/dist/performance/schema.js +628 -0
- package/dist/performance/timeline.js +98 -0
- package/dist/performance/trace-detail.js +91 -0
- package/dist/presentation/service.js +270 -0
- package/dist/presentation/tools/workbench-view.js +65 -0
- package/dist/presentation/tools.js +10 -0
- package/dist/project/directories.js +363 -0
- package/dist/project/git-health.js +498 -0
- package/dist/project/ide.js +66 -0
- package/dist/project/migration.js +22 -0
- package/dist/project/routes/directories.js +89 -0
- package/dist/project/routes/git.js +30 -0
- package/dist/project/routes/task-defaults.js +42 -0
- package/dist/project/session-health.js +9 -0
- package/dist/project/startup.js +28 -0
- package/dist/project/task-defaults.js +68 -0
- package/dist/project/tools/session-control.js +664 -0
- package/dist/project/tools/session-control.txt +60 -0
- package/dist/project/tools/worktree-archive.js +76 -0
- package/dist/project/tools/worktree-enter.js +175 -0
- package/dist/project/tools/worktree-leave.js +119 -0
- package/dist/project/tools/worktree-list.js +52 -0
- package/dist/project/tools.js +26 -0
- package/dist/project/vcs-branch-watcher.js +76 -0
- package/dist/project/vcs.js +114 -0
- package/dist/project/worktree-janitor.js +140 -0
- package/dist/project/worktrees.js +98 -0
- package/dist/push/bridge.js +171 -0
- package/dist/push/index.js +4 -0
- package/dist/push/routes/push.js +224 -0
- package/dist/push/service.js +93 -0
- package/dist/push/store.js +121 -0
- package/dist/push/types.js +58 -0
- package/dist/review/git.js +226 -0
- package/dist/review/routes.js +45 -0
- package/dist/review/schema.js +32 -0
- package/dist/review/state.js +79 -0
- package/dist/stats/aggregator.js +252 -0
- package/dist/stats/canonical.js +89 -0
- package/dist/stats/cli/stats.js +302 -0
- package/dist/stats/engine.js +176 -0
- package/dist/stats/index.js +5 -0
- package/dist/stats/rollup.js +460 -0
- package/dist/stats/routes/stats.js +94 -0
- package/dist/stats/routes/usage.js +83 -0
- package/dist/stats/storage.js +86 -0
- package/dist/stats/types.js +267 -0
- package/dist/worker.js +6 -0
- package/package.json +232 -4
- package/src/agents.ts +15 -0
- package/src/cli-adapter.ts +11 -0
- package/src/component.ts +73 -0
- package/src/config-schema.ts +289 -0
- package/src/execution/activity.ts +83 -0
- package/src/execution/content.ts +189 -0
- package/src/execution/json-sections.ts +230 -0
- package/src/execution/presentation.ts +91 -0
- package/src/execution/preview.ts +12 -0
- package/src/execution/process.ts +118 -0
- package/src/execution/projection.ts +330 -0
- package/src/execution/query.ts +19 -0
- package/src/execution/routes.ts +324 -0
- package/src/execution/schema.ts +336 -0
- package/src/execution/service.ts +2149 -0
- package/src/features.ts +22 -0
- package/src/http.ts +75 -0
- package/src/performance/analysis.ts +356 -0
- package/src/performance/catalog.ts +902 -0
- package/src/performance/dashboard.ts +521 -0
- package/src/performance/error.ts +32 -0
- package/src/performance/index.ts +6 -0
- package/src/performance/inflight.ts +46 -0
- package/src/performance/projection.ts +143 -0
- package/src/performance/prompt/base.txt +38 -0
- package/src/performance/prompt/builder.ts +24 -0
- package/src/performance/routes/performance-route.ts +541 -0
- package/src/performance/schema.ts +695 -0
- package/src/performance/timeline.ts +100 -0
- package/src/performance/trace-detail.ts +97 -0
- package/src/presentation/service.ts +303 -0
- package/src/presentation/tools/workbench-view.ts +75 -0
- package/src/presentation/tools.ts +11 -0
- package/src/project/directories.ts +391 -0
- package/src/project/git-health.ts +577 -0
- package/src/project/ide.ts +80 -0
- package/src/project/migration.ts +24 -0
- package/src/project/routes/directories.ts +124 -0
- package/src/project/routes/git.ts +42 -0
- package/src/project/routes/task-defaults.ts +50 -0
- package/src/project/session-health.ts +10 -0
- package/src/project/startup.ts +28 -0
- package/src/project/task-defaults.ts +70 -0
- package/src/project/tools/session-control.ts +725 -0
- package/src/project/tools/session-control.txt +60 -0
- package/src/project/tools/worktree-archive.ts +92 -0
- package/src/project/tools/worktree-enter.ts +227 -0
- package/src/project/tools/worktree-leave.ts +140 -0
- package/src/project/tools/worktree-list.ts +68 -0
- package/src/project/tools.ts +29 -0
- package/src/project/vcs-branch-watcher.ts +81 -0
- package/src/project/vcs.ts +124 -0
- package/src/project/worktree-janitor.ts +153 -0
- package/src/project/worktrees.ts +110 -0
- package/src/push/bridge.ts +182 -0
- package/src/push/index.ts +4 -0
- package/src/push/routes/push.ts +267 -0
- package/src/push/service.ts +108 -0
- package/src/push/store.ts +120 -0
- package/src/push/types.ts +67 -0
- package/src/review/git.ts +221 -0
- package/src/review/routes.ts +65 -0
- package/src/review/schema.ts +34 -0
- package/src/review/state.ts +77 -0
- package/src/stats/aggregator.ts +277 -0
- package/src/stats/canonical.ts +93 -0
- package/src/stats/cli/stats.ts +348 -0
- package/src/stats/engine.ts +197 -0
- package/src/stats/index.ts +5 -0
- package/src/stats/rollup.ts +522 -0
- package/src/stats/routes/stats.ts +107 -0
- package/src/stats/routes/usage.ts +103 -0
- package/src/stats/storage.ts +78 -0
- package/src/stats/types.ts +351 -0
- package/src/worker.ts +7 -0
|
@@ -0,0 +1,1993 @@
|
|
|
1
|
+
import { ModelLimit } from "@ericsanchezok/synergy-util/model-limit";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Identifier } from "@ericsanchezok/synergy-harness/id/id";
|
|
4
|
+
import { ObservabilityMetrics } from "@ericsanchezok/synergy-harness/observability/metrics";
|
|
5
|
+
import { SessionInbox } from "@ericsanchezok/synergy-harness/session/inbox";
|
|
6
|
+
import { reconcileContextUsage } from "@ericsanchezok/synergy-harness/context";
|
|
7
|
+
import { Session } from "@ericsanchezok/synergy-harness/session";
|
|
8
|
+
import { SessionEvent } from "@ericsanchezok/synergy-harness/session/event";
|
|
9
|
+
import { MessageV2 } from "@ericsanchezok/synergy-harness/session/message-v2";
|
|
10
|
+
import { SessionHistory } from "@ericsanchezok/synergy-harness/session/history";
|
|
11
|
+
import { RolloutAccounting, RolloutExecution, RolloutEvidence, RolloutEvents, RolloutSnapshot, } from "@ericsanchezok/synergy-harness/rollout";
|
|
12
|
+
import { Usage } from "@ericsanchezok/synergy-harness/usage";
|
|
13
|
+
import { ScopeContext } from "@ericsanchezok/synergy-harness/scope/context";
|
|
14
|
+
import { RuntimeContext } from "@ericsanchezok/synergy-harness/lifecycle/context";
|
|
15
|
+
import { Bus } from "@ericsanchezok/synergy-harness/bus";
|
|
16
|
+
import { Storage } from "@ericsanchezok/synergy-harness/storage/storage";
|
|
17
|
+
import { Log } from "@ericsanchezok/synergy-harness/util/log";
|
|
18
|
+
import { ObservabilitySpans } from "@ericsanchezok/synergy-harness/observability/spans";
|
|
19
|
+
import { ExecutionQueryError, queryCursor, queryValue } from "./query.js";
|
|
20
|
+
import { ExecutionSchema } from "./schema.js";
|
|
21
|
+
import { ExecutionActivity } from "./activity.js";
|
|
22
|
+
import { ExecutionPresentation } from "./presentation.js";
|
|
23
|
+
import { ExecutionContent } from "./content.js";
|
|
24
|
+
import { ExecutionProcess } from "./process.js";
|
|
25
|
+
import { preview } from "./preview.js";
|
|
26
|
+
export var ExecutionService;
|
|
27
|
+
(function (ExecutionService) {
|
|
28
|
+
const log = Log.create({ service: "execution" });
|
|
29
|
+
const changesFor = (ids) => ({
|
|
30
|
+
owners: new Set(ids),
|
|
31
|
+
events: new Map(),
|
|
32
|
+
repair: new Set(),
|
|
33
|
+
});
|
|
34
|
+
function enqueue(changes, ownerID, key, apply) {
|
|
35
|
+
if (changes.repair.has(ownerID))
|
|
36
|
+
return;
|
|
37
|
+
if (changes.events.size >= 4096) {
|
|
38
|
+
changes.repair.add(ownerID);
|
|
39
|
+
for (const [id, event] of changes.events)
|
|
40
|
+
if (event.ownerID === ownerID)
|
|
41
|
+
changes.events.delete(id);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
changes.events.set(key, { ownerID, apply });
|
|
45
|
+
}
|
|
46
|
+
function checkpoint(signal) {
|
|
47
|
+
signal?.throwIfAborted();
|
|
48
|
+
}
|
|
49
|
+
const state = RuntimeContext.state(() => ({
|
|
50
|
+
entries: new Map(),
|
|
51
|
+
subscriptions: new Map(),
|
|
52
|
+
legacyClients: 0,
|
|
53
|
+
pending: new Map(),
|
|
54
|
+
timers: new Map(),
|
|
55
|
+
publishing: new Set(),
|
|
56
|
+
refreshRequested: new Set(),
|
|
57
|
+
dirty: new Set(),
|
|
58
|
+
sequence: 0,
|
|
59
|
+
schedule: undefined,
|
|
60
|
+
dispose: undefined,
|
|
61
|
+
}));
|
|
62
|
+
function entryFor(root) {
|
|
63
|
+
const cache = state();
|
|
64
|
+
let entry = cache.entries.get(root.id);
|
|
65
|
+
if (!entry) {
|
|
66
|
+
entry = {
|
|
67
|
+
root,
|
|
68
|
+
touched: Date.now(),
|
|
69
|
+
metadataChanges: new Map(),
|
|
70
|
+
metadataRepair: false,
|
|
71
|
+
metadataListeners: new Set(),
|
|
72
|
+
progressListeners: new Set(),
|
|
73
|
+
trajectoryReaders: cache.subscriptions.get(root.id)?.trajectory ?? 0,
|
|
74
|
+
};
|
|
75
|
+
cache.entries.set(root.id, entry);
|
|
76
|
+
}
|
|
77
|
+
return entry;
|
|
78
|
+
}
|
|
79
|
+
function evict() {
|
|
80
|
+
const cache = state();
|
|
81
|
+
while (cache.entries.size > 8) {
|
|
82
|
+
const oldest = [...cache.entries.values()]
|
|
83
|
+
.filter((entry) => !cache.pending.has(entry.root.id) &&
|
|
84
|
+
!cache.publishing.has(entry.root.id) &&
|
|
85
|
+
!entry.audit?.readers &&
|
|
86
|
+
!entry.metadataLoading &&
|
|
87
|
+
!entry.metadataListeners.size &&
|
|
88
|
+
!cache.subscriptions.has(entry.root.id))
|
|
89
|
+
.sort((a, b) => a.touched - b.touched)[0];
|
|
90
|
+
if (!oldest)
|
|
91
|
+
return;
|
|
92
|
+
clearTimeout(oldest.metadataTimer);
|
|
93
|
+
oldest.audit?.controller.abort();
|
|
94
|
+
clearTimeout(cache.timers.get(oldest.root.id));
|
|
95
|
+
cache.entries.delete(oldest.root.id);
|
|
96
|
+
cache.dirty.delete(oldest.root.id);
|
|
97
|
+
cache.timers.delete(oldest.root.id);
|
|
98
|
+
cache.refreshRequested.delete(oldest.root.id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const nodeID = (sessionID, kind, id) => Buffer.from(JSON.stringify([sessionID, kind, id])).toString("base64url");
|
|
102
|
+
function rootID(record) {
|
|
103
|
+
return record.kind === "run" ? record.value.id : record.value.runID;
|
|
104
|
+
}
|
|
105
|
+
function nodesFor(snapshot, session, detail = false) {
|
|
106
|
+
const result = [];
|
|
107
|
+
const evidence = new Map();
|
|
108
|
+
const add = (record) => {
|
|
109
|
+
if (record.kind === "segment" ||
|
|
110
|
+
record.kind === "interval" ||
|
|
111
|
+
(record.kind === "run" && record.value.admissionOnly))
|
|
112
|
+
return;
|
|
113
|
+
const value = record.value;
|
|
114
|
+
const runID = rootID(record);
|
|
115
|
+
const parentID = record.kind === "run"
|
|
116
|
+
? session.parentID
|
|
117
|
+
? record.value.parent?.runID
|
|
118
|
+
? nodeID(session.parentID, "run", record.value.parent.runID)
|
|
119
|
+
: null
|
|
120
|
+
: null
|
|
121
|
+
: record.kind === "attempt"
|
|
122
|
+
? nodeID(session.id, "call", record.value.callID)
|
|
123
|
+
: record.kind === "process"
|
|
124
|
+
? nodeID(session.id, "tool", record.value.toolExecutionID)
|
|
125
|
+
: nodeID(session.id, "run", runID);
|
|
126
|
+
const kind = record.kind === "run"
|
|
127
|
+
? session.parentID
|
|
128
|
+
? "subtask"
|
|
129
|
+
: "turn"
|
|
130
|
+
: record.kind === "call"
|
|
131
|
+
? record.value.usageRole === "compaction"
|
|
132
|
+
? "compaction"
|
|
133
|
+
: "model"
|
|
134
|
+
: record.kind === "attempt"
|
|
135
|
+
? record.value.index > 0
|
|
136
|
+
? "retry"
|
|
137
|
+
: "model"
|
|
138
|
+
: record.kind;
|
|
139
|
+
const title = record.kind === "call"
|
|
140
|
+
? record.value.model.modelID
|
|
141
|
+
: record.kind === "attempt"
|
|
142
|
+
? String(record.value.index + 1)
|
|
143
|
+
: record.kind === "tool"
|
|
144
|
+
? record.value.tool
|
|
145
|
+
: record.kind === "process"
|
|
146
|
+
? String(record.value.pid ?? record.value.id)
|
|
147
|
+
: (session.cortex?.description ?? session.title);
|
|
148
|
+
const subtitle = record.kind === "call"
|
|
149
|
+
? record.value.purpose
|
|
150
|
+
: record.kind === "tool"
|
|
151
|
+
? (record.value.error ?? "")
|
|
152
|
+
: record.kind === "attempt"
|
|
153
|
+
? (record.value.error ?? (record.value.httpStatus ? String(record.value.httpStatus) : ""))
|
|
154
|
+
: "";
|
|
155
|
+
const node = ExecutionSchema.Node.parse({
|
|
156
|
+
id: nodeID(session.id, record.kind, value.id),
|
|
157
|
+
sessionID: session.id,
|
|
158
|
+
runID,
|
|
159
|
+
parentID,
|
|
160
|
+
kind,
|
|
161
|
+
title,
|
|
162
|
+
preview: preview(subtitle),
|
|
163
|
+
started: value.started,
|
|
164
|
+
ended: value.ended,
|
|
165
|
+
status: record.kind === "run" ? (record.value.execution?.status ?? value.status) : value.status,
|
|
166
|
+
revision: snapshot.revision,
|
|
167
|
+
source: "recorded",
|
|
168
|
+
evidenceKind: record.kind,
|
|
169
|
+
...(record.kind === "run" && session.parentID
|
|
170
|
+
? {
|
|
171
|
+
attribution: record.value.parent?.runID ? "known" : "unassigned",
|
|
172
|
+
tokens: RolloutAccounting.summarize({
|
|
173
|
+
calls: snapshot.calls.filter((call) => call.runID === runID),
|
|
174
|
+
attempts: snapshot.attempts.filter((attempt) => attempt.runID === runID),
|
|
175
|
+
gaps: snapshot.gaps,
|
|
176
|
+
}).tokens.total,
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
179
|
+
...(record.kind === "tool" ? { messageID: record.value.messageID, tool: record.value.tool } : {}),
|
|
180
|
+
...(record.kind === "call"
|
|
181
|
+
? {
|
|
182
|
+
modelID: record.value.model.modelID,
|
|
183
|
+
modelKind: record.value.kind,
|
|
184
|
+
agent: record.value.agent,
|
|
185
|
+
callID: value.id,
|
|
186
|
+
purpose: record.value.purpose,
|
|
187
|
+
usageRole: record.value.usageRole,
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
190
|
+
...(record.kind === "attempt"
|
|
191
|
+
? { callID: record.value.callID, modelID: record.value.responseModel, attemptIndex: record.value.index }
|
|
192
|
+
: {}),
|
|
193
|
+
});
|
|
194
|
+
result.push(node);
|
|
195
|
+
if (detail)
|
|
196
|
+
evidence.set(node.id, { owner: snapshot.owner, record });
|
|
197
|
+
};
|
|
198
|
+
for (const value of snapshot.runs)
|
|
199
|
+
add({ kind: "run", value });
|
|
200
|
+
for (const value of snapshot.calls)
|
|
201
|
+
add({ kind: "call", value });
|
|
202
|
+
for (const value of snapshot.attempts)
|
|
203
|
+
add({ kind: "attempt", value });
|
|
204
|
+
for (const value of snapshot.tools)
|
|
205
|
+
add({ kind: "tool", value });
|
|
206
|
+
for (const value of snapshot.processes)
|
|
207
|
+
add({ kind: "process", value });
|
|
208
|
+
return { nodes: result, evidence };
|
|
209
|
+
}
|
|
210
|
+
function messageCalls(info, parts) {
|
|
211
|
+
const calls = new Map();
|
|
212
|
+
if (info.role !== "assistant")
|
|
213
|
+
return calls;
|
|
214
|
+
const fallback = info.accounting?.kind === "rollout" && info.accounting.callIDs.length === 1
|
|
215
|
+
? info.accounting.callIDs[0]
|
|
216
|
+
: undefined;
|
|
217
|
+
let next;
|
|
218
|
+
for (let index = parts.length - 1; index >= 0; index--) {
|
|
219
|
+
const part = parts[index];
|
|
220
|
+
const call = next ?? fallback;
|
|
221
|
+
if (call)
|
|
222
|
+
calls.set(part.id, call);
|
|
223
|
+
if (part.type === "step-start")
|
|
224
|
+
next = undefined;
|
|
225
|
+
if (part.type === "step-finish")
|
|
226
|
+
next =
|
|
227
|
+
part.accounting?.kind === "rollout" && part.accounting.callIDs.length === 1
|
|
228
|
+
? part.accounting.callIDs[0]
|
|
229
|
+
: undefined;
|
|
230
|
+
}
|
|
231
|
+
return calls;
|
|
232
|
+
}
|
|
233
|
+
function normalizedNode(info, part, revision, callID) {
|
|
234
|
+
const tool = part.type === "tool" ? part : undefined;
|
|
235
|
+
const kind = tool
|
|
236
|
+
? "tool"
|
|
237
|
+
: part.type === "reasoning"
|
|
238
|
+
? "reasoning"
|
|
239
|
+
: part.type === "compaction"
|
|
240
|
+
? "compaction"
|
|
241
|
+
: part.type === "text"
|
|
242
|
+
? MessageV2.isSystemPart(part) || (info.role === "user" && !info.isRoot)
|
|
243
|
+
? "context"
|
|
244
|
+
: info.role === "user"
|
|
245
|
+
? "input"
|
|
246
|
+
: "output"
|
|
247
|
+
: part.type === "attachment"
|
|
248
|
+
? "context"
|
|
249
|
+
: undefined;
|
|
250
|
+
if (!kind)
|
|
251
|
+
return;
|
|
252
|
+
const runID = info.rootID ?? info.id;
|
|
253
|
+
const started = part.time?.start ?? info.time.created;
|
|
254
|
+
const ended = part.time?.end ?? (info.role === "assistant" ? info.time.completed : info.time.created);
|
|
255
|
+
const status = tool
|
|
256
|
+
? tool.status === "error"
|
|
257
|
+
? "failed"
|
|
258
|
+
: tool.status === "pending" || tool.status === "generating"
|
|
259
|
+
? "unknown"
|
|
260
|
+
: tool.status
|
|
261
|
+
: info.role === "assistant" && info.error
|
|
262
|
+
? "failed"
|
|
263
|
+
: info.role === "assistant" && !info.time.completed
|
|
264
|
+
? "running"
|
|
265
|
+
: "completed";
|
|
266
|
+
return ExecutionSchema.Node.parse({
|
|
267
|
+
id: nodeID(info.sessionID, "message", part.id),
|
|
268
|
+
sessionID: info.sessionID,
|
|
269
|
+
runID,
|
|
270
|
+
parentID: callID ? nodeID(info.sessionID, "call", callID) : nodeID(info.sessionID, "run", runID),
|
|
271
|
+
kind,
|
|
272
|
+
title: tool?.tool ?? part.preview,
|
|
273
|
+
preview: part.preview,
|
|
274
|
+
started,
|
|
275
|
+
ended,
|
|
276
|
+
status,
|
|
277
|
+
revision,
|
|
278
|
+
messageID: info.id,
|
|
279
|
+
tool: tool?.tool,
|
|
280
|
+
agent: info.agent,
|
|
281
|
+
source: "messages",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async function appendMessages(session, snapshot, raw, nodes, contexts, signal) {
|
|
285
|
+
const tools = toolIndex(nodes, [snapshot]);
|
|
286
|
+
let processed = 0;
|
|
287
|
+
for (const message of raw) {
|
|
288
|
+
rememberContext(contexts, message.info);
|
|
289
|
+
const calls = messageCalls(message.info, message.parts);
|
|
290
|
+
for (const part of message.parts) {
|
|
291
|
+
if (++processed % 256 === 0) {
|
|
292
|
+
await Bun.sleep(0);
|
|
293
|
+
checkpoint(signal);
|
|
294
|
+
}
|
|
295
|
+
if (part.type === "tool") {
|
|
296
|
+
const tool = tools.get(JSON.stringify([session.id, message.info.id, part.callID]));
|
|
297
|
+
if (tool) {
|
|
298
|
+
tool.preview = part.preview;
|
|
299
|
+
const callID = calls.get(part.id);
|
|
300
|
+
if (callID && snapshot.calls.some((call) => call.id === callID))
|
|
301
|
+
tool.parentID = nodeID(session.id, "call", callID);
|
|
302
|
+
tool.agent = message.info.agent;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const node = normalizedNode(message.info, part, 0, calls.get(part.id));
|
|
307
|
+
if (!node)
|
|
308
|
+
continue;
|
|
309
|
+
nodes.push(node);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function rememberContext(contexts, info, changed) {
|
|
314
|
+
if (info.role !== "assistant" || !info.contextUsage || info.accounting?.kind !== "rollout")
|
|
315
|
+
return;
|
|
316
|
+
const callID = info.contextUsage.callID ?? info.accounting.callIDs.at(-1);
|
|
317
|
+
if (callID && info.accounting.callIDs.includes(callID)) {
|
|
318
|
+
contexts.set(callID, info.contextUsage);
|
|
319
|
+
changed?.add(nodeID(info.sessionID, "call", callID));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function selected(data, runID) {
|
|
323
|
+
if (!runID)
|
|
324
|
+
return data.nodes;
|
|
325
|
+
const allowed = new Set([data.root.id + ":" + runID]);
|
|
326
|
+
for (const snapshot of data.snapshots) {
|
|
327
|
+
for (const run of snapshot.runs) {
|
|
328
|
+
if (run.parent?.owner.kind === "session" &&
|
|
329
|
+
run.parent.runID &&
|
|
330
|
+
allowed.has(run.parent.owner.sessionID + ":" + run.parent.runID))
|
|
331
|
+
allowed.add(snapshot.owner.kind === "session" ? snapshot.owner.sessionID + ":" + run.id : "");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return data.nodes.filter((node) => allowed.has(node.sessionID + ":" + node.runID));
|
|
335
|
+
}
|
|
336
|
+
async function build(root, changes = changesFor([root.id]), signal, descendants = true) {
|
|
337
|
+
checkpoint(signal);
|
|
338
|
+
ObservabilityMetrics.record({
|
|
339
|
+
name: descendants ? "execution.tree.build" : "execution.owner.build",
|
|
340
|
+
value: 1,
|
|
341
|
+
unit: "count",
|
|
342
|
+
module: "session",
|
|
343
|
+
});
|
|
344
|
+
const sessions = [root];
|
|
345
|
+
const seen = new Set([root.id]);
|
|
346
|
+
for (let index = 0; descendants && index < sessions.length; index++) {
|
|
347
|
+
checkpoint(signal);
|
|
348
|
+
for (const child of await Session.children(sessions[index].id)) {
|
|
349
|
+
if (seen.has(child.id))
|
|
350
|
+
continue;
|
|
351
|
+
seen.add(child.id);
|
|
352
|
+
changes.owners.add(child.id);
|
|
353
|
+
sessions.push(child);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
checkpoint(signal);
|
|
357
|
+
const presentation = await ObservabilitySpans.measure({ name: "execution.snapshot", module: "session", kind: "session" }, () => RolloutSnapshot.presentationAll(sessions.map((session) => ({
|
|
358
|
+
kind: "session",
|
|
359
|
+
scopeID: session.scope.id,
|
|
360
|
+
sessionID: session.id,
|
|
361
|
+
})), signal));
|
|
362
|
+
const snapshots = presentation.map((value) => value.snapshot);
|
|
363
|
+
const nodes = [];
|
|
364
|
+
const contexts = new Map();
|
|
365
|
+
for (const [index, session] of sessions.entries()) {
|
|
366
|
+
checkpoint(signal);
|
|
367
|
+
const snapshot = snapshots[index];
|
|
368
|
+
const projected = nodesFor(snapshot, session);
|
|
369
|
+
nodes.push(...projected.nodes);
|
|
370
|
+
const raw = await ObservabilitySpans.measure({ name: "execution.messages", module: "session", kind: "session" }, () => SessionHistory.executionMessages({
|
|
371
|
+
sessionID: session.id,
|
|
372
|
+
signal,
|
|
373
|
+
progress: (value) => {
|
|
374
|
+
for (const listener of state().entries.get(root.id)?.progressListeners ?? [])
|
|
375
|
+
listener(value);
|
|
376
|
+
},
|
|
377
|
+
}));
|
|
378
|
+
await appendMessages(session, snapshot, raw, nodes, contexts, signal);
|
|
379
|
+
}
|
|
380
|
+
nodes.sort((a, b) => a.started - b.started || a.id.localeCompare(b.id));
|
|
381
|
+
const usage = new Map();
|
|
382
|
+
for (const record of await ObservabilitySpans.measure({ name: "execution.usage", module: "session", kind: "session" }, async () => (await Promise.all(sessions.map((session) => Usage.collect({ scopeID: session.scope.id, sessionID: session.id, includeDescendants: false }, signal)))).flat()))
|
|
383
|
+
usage.set(record.id, record);
|
|
384
|
+
const inboxes = new Map(await Promise.all(sessions.map(async (session) => [session.id, await SessionInbox.list(session.id)])));
|
|
385
|
+
const revision = ++state().sequence;
|
|
386
|
+
const data = {
|
|
387
|
+
root,
|
|
388
|
+
sessions,
|
|
389
|
+
snapshots,
|
|
390
|
+
verified: new Set(sessions.filter((_, index) => presentation[index].verified).map((session) => session.id)),
|
|
391
|
+
nodes,
|
|
392
|
+
sessionsByID: new Map(sessions.map((session) => [session.id, session])),
|
|
393
|
+
snapshotsByID: new Map(snapshots.map((snapshot) => [snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "", snapshot])),
|
|
394
|
+
usage,
|
|
395
|
+
contexts,
|
|
396
|
+
inboxes,
|
|
397
|
+
revision,
|
|
398
|
+
touched: Date.now(),
|
|
399
|
+
pendingMessages: new Map(),
|
|
400
|
+
changed: new Set(),
|
|
401
|
+
removed: new Set(),
|
|
402
|
+
refreshOwners: new Set(),
|
|
403
|
+
addedSessions: new Map(),
|
|
404
|
+
changes,
|
|
405
|
+
selections: new Map(),
|
|
406
|
+
processRows: new Map(),
|
|
407
|
+
recordRows: new Map(),
|
|
408
|
+
contextRows: new Map(),
|
|
409
|
+
publishedRevision: revision,
|
|
410
|
+
derived: new Map(),
|
|
411
|
+
};
|
|
412
|
+
const value = { ...data, summary: summarize(data) };
|
|
413
|
+
value.contextRows = new Map(contextSnapshots(value).map((context) => [context.nodeID, JSON.stringify(context)]));
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
function derived(data, runID) {
|
|
417
|
+
const key = runID ?? "";
|
|
418
|
+
const cached = data.derived.get(key);
|
|
419
|
+
if (cached?.revision === data.revision)
|
|
420
|
+
return cached;
|
|
421
|
+
for (const [id, value] of data.derived)
|
|
422
|
+
if (value.revision !== data.revision)
|
|
423
|
+
data.derived.delete(id);
|
|
424
|
+
const value = { revision: data.revision, nodes: selected(data, runID), modes: new Map() };
|
|
425
|
+
data.derived.set(key, value);
|
|
426
|
+
while (data.derived.size > 5)
|
|
427
|
+
data.derived.delete([...data.derived.keys()].find((id) => id !== ""));
|
|
428
|
+
return value;
|
|
429
|
+
}
|
|
430
|
+
function activityFor(data, runID) {
|
|
431
|
+
const value = derived(data, runID);
|
|
432
|
+
return (value.activity ??= ExecutionActivity.project(value.nodes, data.root.id));
|
|
433
|
+
}
|
|
434
|
+
function processRows(data, mode, runID) {
|
|
435
|
+
const value = derived(data, runID);
|
|
436
|
+
let rows = value.modes.get(mode);
|
|
437
|
+
if (!rows) {
|
|
438
|
+
rows = ExecutionProcess.project(value.nodes, mode, data.root.id);
|
|
439
|
+
value.modes.set(mode, rows);
|
|
440
|
+
}
|
|
441
|
+
return rows;
|
|
442
|
+
}
|
|
443
|
+
function summarize(data, runID) {
|
|
444
|
+
const cached = derived(data, runID);
|
|
445
|
+
const sample = RolloutExecution.clock();
|
|
446
|
+
if (cached.prepared) {
|
|
447
|
+
const { summary, timings } = cached.prepared;
|
|
448
|
+
const timing = (key) => {
|
|
449
|
+
const value = RolloutExecution.summarize(timings.get(key), sample);
|
|
450
|
+
return { ...value, status: ExecutionSchema.Task.shape.status.parse(value.status) };
|
|
451
|
+
};
|
|
452
|
+
return {
|
|
453
|
+
...summary,
|
|
454
|
+
computedAt: Date.now(),
|
|
455
|
+
clockID: sample.clockID,
|
|
456
|
+
sampledAt: sample.now,
|
|
457
|
+
...timing("root"),
|
|
458
|
+
tasks: summary.tasks.map((task) => ({ ...task, ...timing("task:" + task.sessionID) })),
|
|
459
|
+
rounds: summary.rounds.map((round) => timings.has("round:" + round.id) ? { ...round, ...timing("round:" + round.id) } : round),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const nodes = cached.nodes;
|
|
463
|
+
const identities = new Set(nodes.map((node) => node.sessionID + ":" + node.runID));
|
|
464
|
+
const snapshots = data.snapshots.map((snapshot) => ({
|
|
465
|
+
...snapshot,
|
|
466
|
+
runs: snapshot.runs.filter((run) => identities.has((snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "") + ":" + run.id)),
|
|
467
|
+
calls: snapshot.calls.filter((call) => identities.has((snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "") + ":" + call.runID)),
|
|
468
|
+
intervals: snapshot.intervals.filter((interval) => identities.has((snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "") + ":" + interval.runID)),
|
|
469
|
+
attempts: snapshot.attempts.filter((attempt) => identities.has((snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "") + ":" + attempt.runID)),
|
|
470
|
+
}));
|
|
471
|
+
const timings = new Map();
|
|
472
|
+
const projection = (scope, rootRuns = scope[0].runs, selectedRunID, key = "root") => {
|
|
473
|
+
const input = {
|
|
474
|
+
roots: rootRuns,
|
|
475
|
+
runs: scope.flatMap((snapshot) => snapshot.runs),
|
|
476
|
+
intervals: scope.flatMap((snapshot) => snapshot.intervals),
|
|
477
|
+
segments: scope.flatMap((snapshot) => snapshot.segments),
|
|
478
|
+
hasHistory: nodes.some((node) => scope.some((snapshot) => snapshot.owner.kind === "session" && snapshot.owner.sessionID === node.sessionID) &&
|
|
479
|
+
(!selectedRunID || node.rootRunID === selectedRunID || node.runID === selectedRunID) &&
|
|
480
|
+
["output", "model", "tool"].includes(node.kind)),
|
|
481
|
+
paused: data.sessions.some((candidate) => {
|
|
482
|
+
if (!candidate.paused)
|
|
483
|
+
return false;
|
|
484
|
+
const selected = scope.find((snapshot) => snapshot.owner.kind === "session" && snapshot.owner.sessionID === candidate.id);
|
|
485
|
+
if (!selected)
|
|
486
|
+
return false;
|
|
487
|
+
if (!selectedRunID && !runID)
|
|
488
|
+
return true;
|
|
489
|
+
const latest = data.snapshots
|
|
490
|
+
.find((snapshot) => snapshot.owner.kind === "session" && snapshot.owner.sessionID === candidate.id)
|
|
491
|
+
?.runs.filter((run) => !run.admissionOnly)
|
|
492
|
+
.toSorted((a, b) => b.started - a.started || b.id.localeCompare(a.id))[0];
|
|
493
|
+
return (!!latest &&
|
|
494
|
+
["running", "interrupted"].includes(latest.execution?.status ?? latest.status) &&
|
|
495
|
+
selected.runs.some((run) => run.id === latest.id));
|
|
496
|
+
}),
|
|
497
|
+
queued: data.sessions.some((candidate) => scope.some((snapshot) => snapshot.owner.kind === "session" && snapshot.owner.sessionID === candidate.id) &&
|
|
498
|
+
((!selectedRunID && candidate.cortex?.status === "queued") ||
|
|
499
|
+
(data.inboxes.get(candidate.id) ?? []).some((item) => item.mode === "task" && !item.status && (!selectedRunID || item.messageID === selectedRunID)))),
|
|
500
|
+
};
|
|
501
|
+
timings.set(key, input);
|
|
502
|
+
return RolloutExecution.summarize(input, sample);
|
|
503
|
+
};
|
|
504
|
+
const subtree = (sessionID) => {
|
|
505
|
+
const ids = new Set([sessionID]);
|
|
506
|
+
for (let size = -1; size !== ids.size;) {
|
|
507
|
+
size = ids.size;
|
|
508
|
+
for (const session of data.sessions)
|
|
509
|
+
if (session.parentID && ids.has(session.parentID))
|
|
510
|
+
ids.add(session.id);
|
|
511
|
+
}
|
|
512
|
+
return snapshots.filter((snapshot) => snapshot.owner.kind === "session" && ids.has(snapshot.owner.sessionID));
|
|
513
|
+
};
|
|
514
|
+
const accounting = snapshots.map(RolloutAccounting.summarize);
|
|
515
|
+
const usage = Usage.summarize([...data.usage.values()].filter((record) => !runID || (record.owner.kind === "session" && identities.has(record.owner.sessionID + ":" + record.runID))), { sessionID: data.root.id, runID, includeDescendants: true });
|
|
516
|
+
const runs = snapshots.flatMap((snapshot) => snapshot.runs);
|
|
517
|
+
const roots = snapshots[0].runs;
|
|
518
|
+
const context = Usage.summarize([...data.usage.values()].filter((record) => record.owner.kind === "session" &&
|
|
519
|
+
record.owner.sessionID === data.root.id &&
|
|
520
|
+
(!runID || record.runID === runID)), { sessionID: data.root.id, runID, includeDescendants: false });
|
|
521
|
+
const start = nodes.reduce((value, node) => Math.min(value, node.started), Date.now());
|
|
522
|
+
const end = nodes.reduce((value, node) => Math.max(value, node.ended ?? node.started), start + 1);
|
|
523
|
+
const distribution = context.context && data.contexts.get(context.context.callID);
|
|
524
|
+
const activity = activityFor(data, runID);
|
|
525
|
+
const activities = activity.nodes;
|
|
526
|
+
const summary = ExecutionSchema.Summary.parse({
|
|
527
|
+
sessionID: data.root.id,
|
|
528
|
+
revision: data.revision,
|
|
529
|
+
runID,
|
|
530
|
+
computedAt: Date.now(),
|
|
531
|
+
clockID: sample.clockID,
|
|
532
|
+
sampledAt: sample.now,
|
|
533
|
+
...projection(snapshots, roots, runID),
|
|
534
|
+
accounting: usage.accounting,
|
|
535
|
+
cost: ExecutionPresentation.cost(usage.accounting),
|
|
536
|
+
own: usage.own,
|
|
537
|
+
descendants: usage.descendants,
|
|
538
|
+
rates: usage.rates,
|
|
539
|
+
cache: usage.cache,
|
|
540
|
+
latency: usage.latency,
|
|
541
|
+
outcomes: usage.outcomes,
|
|
542
|
+
tools: usage.tools,
|
|
543
|
+
context: context.context,
|
|
544
|
+
contextDistribution: distribution &&
|
|
545
|
+
distribution.modelID === context.context?.modelID &&
|
|
546
|
+
distribution.totalInput === context.context.inputTokens
|
|
547
|
+
? distribution
|
|
548
|
+
: null,
|
|
549
|
+
tasks: data.sessions
|
|
550
|
+
.slice(1)
|
|
551
|
+
.map((session, index) => ({
|
|
552
|
+
sessionID: session.id,
|
|
553
|
+
parentID: session.parentID ?? null,
|
|
554
|
+
title: session.cortex?.description ?? session.title,
|
|
555
|
+
nodeID: nodes.find((node) => node.kind === "subtask" && node.sessionID === session.id)?.id ?? null,
|
|
556
|
+
...projection(subtree(session.id), snapshots[index + 1].runs, undefined, "task:" + session.id),
|
|
557
|
+
tokens: accounting[index + 1].tokens.total,
|
|
558
|
+
runs: snapshots[index + 1].runs.map((run) => run.id),
|
|
559
|
+
interaction: session.interaction,
|
|
560
|
+
cortex: session.cortex && {
|
|
561
|
+
taskID: session.cortex.taskID,
|
|
562
|
+
agent: session.cortex.agent,
|
|
563
|
+
status: session.cortex.status,
|
|
564
|
+
visibility: session.cortex.visibility,
|
|
565
|
+
},
|
|
566
|
+
}))
|
|
567
|
+
.filter((task) => !runID || task.runs.length),
|
|
568
|
+
rounds: [
|
|
569
|
+
...data.snapshots[0].runs
|
|
570
|
+
.filter((run) => !run.admissionOnly)
|
|
571
|
+
.map((run) => {
|
|
572
|
+
const roundIdentities = new Set(selected(data, run.id).map((node) => node.sessionID + ":" + node.runID));
|
|
573
|
+
return {
|
|
574
|
+
id: run.id,
|
|
575
|
+
title: data.nodes.find((node) => node.runID === run.id && node.sessionID === data.root.id && node.kind === "input")?.preview ?? run.id,
|
|
576
|
+
started: run.started,
|
|
577
|
+
...projection(data.snapshots.map((snapshot) => ({
|
|
578
|
+
...snapshot,
|
|
579
|
+
runs: snapshot.runs.filter((candidate) => snapshot.owner.kind === "session" &&
|
|
580
|
+
roundIdentities.has(snapshot.owner.sessionID + ":" + candidate.id)),
|
|
581
|
+
intervals: snapshot.intervals.filter((interval) => snapshot.owner.kind === "session" &&
|
|
582
|
+
roundIdentities.has(snapshot.owner.sessionID + ":" + interval.runID)),
|
|
583
|
+
})), [run], run.id, "round:" + run.id),
|
|
584
|
+
};
|
|
585
|
+
}),
|
|
586
|
+
...data.nodes
|
|
587
|
+
.filter((node) => node.sessionID === data.root.id &&
|
|
588
|
+
node.kind === "input" &&
|
|
589
|
+
!data.snapshots[0].runs.some((run) => run.id === node.runID))
|
|
590
|
+
.filter((node, index, all) => all.findIndex((other) => other.runID === node.runID) === index)
|
|
591
|
+
.map((node) => ({
|
|
592
|
+
id: node.runID,
|
|
593
|
+
title: node.preview,
|
|
594
|
+
started: node.started,
|
|
595
|
+
status: "unknown",
|
|
596
|
+
elapsedMs: 0,
|
|
597
|
+
elapsedActive: false,
|
|
598
|
+
elapsedLowerBound: true,
|
|
599
|
+
})),
|
|
600
|
+
],
|
|
601
|
+
coverage: {
|
|
602
|
+
recorded: nodes.filter((node) => node.source === "recorded").length,
|
|
603
|
+
messages: nodes.filter((node) => node.source === "messages").length,
|
|
604
|
+
gaps: data.verified.size === data.sessions.length
|
|
605
|
+
? data.snapshots.reduce((sum, snapshot) => sum + snapshot.gaps.length, 0)
|
|
606
|
+
: null,
|
|
607
|
+
partial: data.verified.size !== data.sessions.length ||
|
|
608
|
+
runs.some((run) => run.recording !== "complete") ||
|
|
609
|
+
(!runs.length && !!nodes.length),
|
|
610
|
+
},
|
|
611
|
+
activityTotal: activities.length,
|
|
612
|
+
humanInputs: activity.humanInputs,
|
|
613
|
+
taskInstructions: activity.taskInstructions,
|
|
614
|
+
activitySegments: activity.segments,
|
|
615
|
+
lanes: ["input", "model", "tool"].map((kind) => ({
|
|
616
|
+
kind,
|
|
617
|
+
start,
|
|
618
|
+
end,
|
|
619
|
+
...(() => {
|
|
620
|
+
const lane = activities.filter((node) => kind === "input"
|
|
621
|
+
? node.kind === "input" || node.kind === "subtask"
|
|
622
|
+
: kind === "model"
|
|
623
|
+
? node.evidenceKind === "call" || node.evidenceKind === "attempt"
|
|
624
|
+
: node.kind === "tool");
|
|
625
|
+
return { total: lane.length, nodes: ExecutionActivity.aggregate(activities, kind) };
|
|
626
|
+
})(),
|
|
627
|
+
})),
|
|
628
|
+
});
|
|
629
|
+
cached.prepared = { summary, timings };
|
|
630
|
+
return summary;
|
|
631
|
+
}
|
|
632
|
+
async function requireRoot(sessionID) {
|
|
633
|
+
const session = await Session.get(sessionID);
|
|
634
|
+
if (session.scope.id !== ScopeContext.current.scope.id)
|
|
635
|
+
throw new Storage.NotFoundError({ message: "Session is outside the selected Scope" });
|
|
636
|
+
return session;
|
|
637
|
+
}
|
|
638
|
+
function cancelUnused(sessionID, loading) {
|
|
639
|
+
const cache = state();
|
|
640
|
+
if (!loading.readers && !cache.subscriptions.has(sessionID) && !cache.legacyClients)
|
|
641
|
+
loading.controller.abort();
|
|
642
|
+
}
|
|
643
|
+
async function consume(sessionID, loading, signal) {
|
|
644
|
+
checkpoint(signal);
|
|
645
|
+
loading.readers++;
|
|
646
|
+
let abort;
|
|
647
|
+
try {
|
|
648
|
+
if (!signal)
|
|
649
|
+
return await loading.promise;
|
|
650
|
+
return await Promise.race([
|
|
651
|
+
loading.promise,
|
|
652
|
+
new Promise((_, reject) => {
|
|
653
|
+
abort = () => reject(signal.reason);
|
|
654
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
655
|
+
if (signal.aborted)
|
|
656
|
+
abort();
|
|
657
|
+
}),
|
|
658
|
+
]);
|
|
659
|
+
}
|
|
660
|
+
finally {
|
|
661
|
+
if (abort)
|
|
662
|
+
signal?.removeEventListener("abort", abort);
|
|
663
|
+
loading.readers--;
|
|
664
|
+
cancelUnused(sessionID, loading);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function repair(entry, signal) {
|
|
668
|
+
const owners = [...entry.refreshOwners];
|
|
669
|
+
entry.refreshOwners.clear();
|
|
670
|
+
const added = [...entry.addedSessions.values()];
|
|
671
|
+
entry.addedSessions.clear();
|
|
672
|
+
const sessions = new Map(added.map((info) => [info.id, info]));
|
|
673
|
+
for (const id of owners)
|
|
674
|
+
sessions.set(id, await requireRoot(id));
|
|
675
|
+
const repaired = new Set();
|
|
676
|
+
for (const session of sessions.values()) {
|
|
677
|
+
if (repaired.has(session.id))
|
|
678
|
+
continue;
|
|
679
|
+
checkpoint(signal);
|
|
680
|
+
const branch = await build(session, entry.changes, signal, !owners.includes(session.id));
|
|
681
|
+
const replaced = new Set(branch.sessions.map((item) => item.id));
|
|
682
|
+
for (const id of replaced)
|
|
683
|
+
repaired.add(id);
|
|
684
|
+
for (const node of entry.nodes)
|
|
685
|
+
if (replaced.has(node.sessionID)) {
|
|
686
|
+
entry.removed.add(node.id);
|
|
687
|
+
}
|
|
688
|
+
entry.nodes = entry.nodes.filter((node) => !replaced.has(node.sessionID));
|
|
689
|
+
for (const [index, info] of branch.sessions.entries()) {
|
|
690
|
+
if (branch.verified.has(info.id))
|
|
691
|
+
entry.verified.add(info.id);
|
|
692
|
+
else
|
|
693
|
+
entry.verified.delete(info.id);
|
|
694
|
+
const position = entry.sessions.findIndex((item) => item.id === info.id);
|
|
695
|
+
if (position < 0) {
|
|
696
|
+
entry.sessions.push(info);
|
|
697
|
+
entry.snapshots.push(branch.snapshots[index]);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
entry.sessions[position] = info;
|
|
701
|
+
entry.snapshots[position] = branch.snapshots[index];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
for (const [id, record] of entry.usage)
|
|
705
|
+
if (record.owner.kind === "session" && replaced.has(record.owner.sessionID))
|
|
706
|
+
entry.usage.delete(id);
|
|
707
|
+
for (const node of branch.nodes) {
|
|
708
|
+
entry.nodes.push(node);
|
|
709
|
+
entry.changed.add(node.id);
|
|
710
|
+
entry.removed.delete(node.id);
|
|
711
|
+
}
|
|
712
|
+
for (const [id, value] of branch.usage)
|
|
713
|
+
entry.usage.set(id, value);
|
|
714
|
+
for (const [id, value] of branch.contexts)
|
|
715
|
+
entry.contexts.set(id, value);
|
|
716
|
+
for (const [id, value] of branch.inboxes)
|
|
717
|
+
entry.inboxes.set(id, value);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function data(sessionID, access = true, signal) {
|
|
721
|
+
checkpoint(signal);
|
|
722
|
+
const shared = state().pending.get(sessionID);
|
|
723
|
+
if (shared && shared.scopeID === ScopeContext.current.scope.id) {
|
|
724
|
+
if (!shared.controller.signal.aborted)
|
|
725
|
+
return consume(sessionID, shared, signal);
|
|
726
|
+
await consume(sessionID, shared, signal).catch(() => checkpoint(signal));
|
|
727
|
+
return data(sessionID, access, signal);
|
|
728
|
+
}
|
|
729
|
+
const root = await requireRoot(sessionID);
|
|
730
|
+
checkpoint(signal);
|
|
731
|
+
init();
|
|
732
|
+
const cache = state();
|
|
733
|
+
const entry = entryFor(root);
|
|
734
|
+
const existing = entry.tree;
|
|
735
|
+
if (access)
|
|
736
|
+
entry.touched = Date.now();
|
|
737
|
+
if (existing && access)
|
|
738
|
+
existing.touched = entry.touched;
|
|
739
|
+
const pending = cache.pending.get(sessionID);
|
|
740
|
+
if (pending) {
|
|
741
|
+
if (!pending.controller.signal.aborted)
|
|
742
|
+
return consume(sessionID, pending, signal);
|
|
743
|
+
await consume(sessionID, pending, signal).catch(() => checkpoint(signal));
|
|
744
|
+
return data(sessionID, access, signal);
|
|
745
|
+
}
|
|
746
|
+
if (existing && !cache.dirty.has(sessionID))
|
|
747
|
+
return existing;
|
|
748
|
+
const changes = existing?.changes ?? changesFor([sessionID]);
|
|
749
|
+
const controller = new AbortController();
|
|
750
|
+
const loading = {
|
|
751
|
+
scopeID: root.scope.id,
|
|
752
|
+
changes,
|
|
753
|
+
controller,
|
|
754
|
+
readers: 0,
|
|
755
|
+
promise: Promise.resolve()
|
|
756
|
+
.then(async () => {
|
|
757
|
+
const value = existing ?? (await build(root, changes, controller.signal));
|
|
758
|
+
checkpoint(controller.signal);
|
|
759
|
+
// Detach a finite revision cut; arrivals during reads belong to the next refresh.
|
|
760
|
+
const events = changes.events;
|
|
761
|
+
const repairs = changes.repair;
|
|
762
|
+
changes.events = new Map();
|
|
763
|
+
changes.repair = new Set();
|
|
764
|
+
let applied = 0;
|
|
765
|
+
for (const event of events.values()) {
|
|
766
|
+
event.apply(value);
|
|
767
|
+
if (++applied % 256 === 0) {
|
|
768
|
+
await Bun.sleep(0);
|
|
769
|
+
checkpoint(controller.signal);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
for (const owner of repairs)
|
|
773
|
+
value.refreshOwners.add(owner);
|
|
774
|
+
await repair(value, controller.signal);
|
|
775
|
+
if (existing || events.size || repairs.size)
|
|
776
|
+
await flush(value, controller.signal);
|
|
777
|
+
checkpoint(controller.signal);
|
|
778
|
+
entry.tree = value;
|
|
779
|
+
if (changes.events.size || changes.repair.size)
|
|
780
|
+
cache.dirty.add(sessionID);
|
|
781
|
+
else
|
|
782
|
+
cache.dirty.delete(sessionID);
|
|
783
|
+
evict();
|
|
784
|
+
return value;
|
|
785
|
+
})
|
|
786
|
+
.catch((error) => {
|
|
787
|
+
for (const owner of changes.owners)
|
|
788
|
+
changes.repair.add(owner);
|
|
789
|
+
cache.dirty.add(sessionID);
|
|
790
|
+
throw error;
|
|
791
|
+
})
|
|
792
|
+
.finally(() => {
|
|
793
|
+
cache.pending.delete(sessionID);
|
|
794
|
+
if (cache.dirty.has(sessionID))
|
|
795
|
+
cache.schedule?.(sessionID);
|
|
796
|
+
}),
|
|
797
|
+
};
|
|
798
|
+
cache.pending.set(sessionID, loading);
|
|
799
|
+
return consume(sessionID, loading, signal);
|
|
800
|
+
}
|
|
801
|
+
function toolIndex(nodes, snapshots) {
|
|
802
|
+
const result = new Map();
|
|
803
|
+
const records = new Map(snapshots.flatMap((snapshot) => snapshot.tools.map((tool) => [nodeID(tool.owner.kind === "session" ? tool.owner.sessionID : "", "tool", tool.id), tool])));
|
|
804
|
+
for (const node of nodes) {
|
|
805
|
+
const record = records.get(node.id);
|
|
806
|
+
if (record)
|
|
807
|
+
result.set(JSON.stringify([node.sessionID, record.messageID, record.toolCallID]), node);
|
|
808
|
+
}
|
|
809
|
+
return result;
|
|
810
|
+
}
|
|
811
|
+
function update(values, value) {
|
|
812
|
+
const index = values.findIndex((item) => item.id === value.id);
|
|
813
|
+
if (index < 0)
|
|
814
|
+
values.push(value);
|
|
815
|
+
else
|
|
816
|
+
values[index] = value;
|
|
817
|
+
}
|
|
818
|
+
function apply(entry, owner, revision, record) {
|
|
819
|
+
if (owner.kind !== "session")
|
|
820
|
+
return;
|
|
821
|
+
const snapshot = entry.snapshotsByID.get(owner.sessionID);
|
|
822
|
+
const session = entry.sessionsByID.get(owner.sessionID);
|
|
823
|
+
if (!snapshot || !session || revision <= snapshot.revision)
|
|
824
|
+
return;
|
|
825
|
+
if (revision !== snapshot.revision + 1) {
|
|
826
|
+
entry.refreshOwners.add(session.id);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
snapshot.revision = revision;
|
|
830
|
+
entry.detail = undefined;
|
|
831
|
+
if (record.kind === "run")
|
|
832
|
+
update(snapshot.runs, record.value);
|
|
833
|
+
if (record.kind === "call")
|
|
834
|
+
update(snapshot.calls, record.value);
|
|
835
|
+
if (record.kind === "attempt")
|
|
836
|
+
update(snapshot.attempts, record.value);
|
|
837
|
+
if (record.kind === "segment")
|
|
838
|
+
update(snapshot.segments, record.value);
|
|
839
|
+
if (record.kind === "interval") {
|
|
840
|
+
update(snapshot.intervals, record.value);
|
|
841
|
+
entry.changed.add(nodeID(session.id, "run", record.value.runID));
|
|
842
|
+
}
|
|
843
|
+
if (record.kind === "run" && record.value.admissionOnly) {
|
|
844
|
+
const id = nodeID(session.id, "run", record.value.id);
|
|
845
|
+
entry.nodes = entry.nodes.filter((node) => node.id !== id);
|
|
846
|
+
entry.removed.add(id);
|
|
847
|
+
}
|
|
848
|
+
if (record.kind === "tool")
|
|
849
|
+
update(snapshot.tools, record.value);
|
|
850
|
+
if (record.kind === "process")
|
|
851
|
+
update(snapshot.processes, record.value);
|
|
852
|
+
const projected = nodesFor({
|
|
853
|
+
...snapshot,
|
|
854
|
+
runs: record.kind === "run" ? [record.value] : [],
|
|
855
|
+
calls: record.kind === "call" ? [record.value] : [],
|
|
856
|
+
attempts: record.kind === "attempt" ? [record.value] : [],
|
|
857
|
+
tools: record.kind === "tool" ? [record.value] : [],
|
|
858
|
+
processes: record.kind === "process" ? [record.value] : [],
|
|
859
|
+
}, session);
|
|
860
|
+
for (const node of projected.nodes) {
|
|
861
|
+
const position = entry.nodes.findIndex((value) => value.id === node.id);
|
|
862
|
+
const old = position < 0 ? undefined : entry.nodes[position];
|
|
863
|
+
if (node.kind === "tool" && !node.preview && old?.preview)
|
|
864
|
+
node.preview = old.preview;
|
|
865
|
+
if (node.kind === "tool" &&
|
|
866
|
+
old?.parentID &&
|
|
867
|
+
snapshot.calls.some((call) => nodeID(session.id, "call", call.id) === old.parentID))
|
|
868
|
+
node.parentID = old.parentID;
|
|
869
|
+
if (node.kind === "tool" && old?.agent)
|
|
870
|
+
node.agent = old.agent;
|
|
871
|
+
if (position < 0) {
|
|
872
|
+
entry.nodes.push(node);
|
|
873
|
+
}
|
|
874
|
+
else
|
|
875
|
+
entry.nodes[position] = node;
|
|
876
|
+
entry.changed.add(node.id);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
async function flush(entry, signal) {
|
|
880
|
+
entry.detail = undefined;
|
|
881
|
+
const messages = entry.pendingMessages;
|
|
882
|
+
entry.pendingMessages = new Map();
|
|
883
|
+
const tools = toolIndex(entry.nodes, entry.snapshots);
|
|
884
|
+
entry.nodes = entry.nodes.filter((node) => {
|
|
885
|
+
if (node.source !== "messages" || !node.messageID || !messages.has(node.messageID))
|
|
886
|
+
return true;
|
|
887
|
+
entry.removed.add(node.id);
|
|
888
|
+
return false;
|
|
889
|
+
});
|
|
890
|
+
let processed = 0;
|
|
891
|
+
for (const [messageID, sessionID] of messages) {
|
|
892
|
+
checkpoint(signal);
|
|
893
|
+
const message = await SessionHistory.executionMessages({ sessionID, messageID, signal })
|
|
894
|
+
.then((messages) => messages[0])
|
|
895
|
+
.catch((error) => {
|
|
896
|
+
if (error instanceof Storage.NotFoundError)
|
|
897
|
+
return undefined;
|
|
898
|
+
throw error;
|
|
899
|
+
});
|
|
900
|
+
if (!message)
|
|
901
|
+
continue;
|
|
902
|
+
const info = MessageV2.deriveSemantics([message])[0].info;
|
|
903
|
+
rememberContext(entry.contexts, info, entry.changed);
|
|
904
|
+
const calls = messageCalls(info, message.parts);
|
|
905
|
+
for (const part of message.parts) {
|
|
906
|
+
if (++processed % 256 === 0) {
|
|
907
|
+
await Bun.sleep(0);
|
|
908
|
+
checkpoint(signal);
|
|
909
|
+
}
|
|
910
|
+
if (part.type === "tool") {
|
|
911
|
+
const node = tools.get(JSON.stringify([sessionID, messageID, part.callID]));
|
|
912
|
+
if (node) {
|
|
913
|
+
node.preview = part.preview;
|
|
914
|
+
const callID = calls.get(part.id);
|
|
915
|
+
if (callID && entry.snapshotsByID.get(sessionID)?.calls.some((call) => call.id === callID))
|
|
916
|
+
node.parentID = nodeID(sessionID, "call", callID);
|
|
917
|
+
node.agent = info.agent;
|
|
918
|
+
entry.changed.add(node.id);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const node = normalizedNode(info, part, entry.revision + 1, calls.get(part.id));
|
|
923
|
+
if (!node)
|
|
924
|
+
continue;
|
|
925
|
+
entry.nodes.push(node);
|
|
926
|
+
entry.changed.add(node.id);
|
|
927
|
+
entry.removed.delete(node.id);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
checkpoint(signal);
|
|
931
|
+
entry.nodes.sort((a, b) => a.started - b.started || a.id.localeCompare(b.id));
|
|
932
|
+
entry.sessionsByID = new Map(entry.sessions.map((session) => [session.id, session]));
|
|
933
|
+
entry.snapshotsByID = new Map(entry.snapshots.map((snapshot) => [snapshot.owner.kind === "session" ? snapshot.owner.sessionID : "", snapshot]));
|
|
934
|
+
entry.revision = ++state().sequence;
|
|
935
|
+
entry.summary = summarize(entry);
|
|
936
|
+
for (const [runID, selection] of entry.selections) {
|
|
937
|
+
selection.summary = summarize(entry, runID);
|
|
938
|
+
}
|
|
939
|
+
return entry;
|
|
940
|
+
}
|
|
941
|
+
function subscribe(sessionID, options) {
|
|
942
|
+
const cache = state();
|
|
943
|
+
const entry = cache.entries.get(sessionID);
|
|
944
|
+
const trajectory = options?.trajectory !== false;
|
|
945
|
+
if (entry && trajectory)
|
|
946
|
+
entry.trajectoryReaders++;
|
|
947
|
+
const subscription = cache.subscriptions.get(sessionID) ?? { readers: 0, trajectory: 0 };
|
|
948
|
+
subscription.readers++;
|
|
949
|
+
if (trajectory)
|
|
950
|
+
subscription.trajectory++;
|
|
951
|
+
cache.subscriptions.set(sessionID, subscription);
|
|
952
|
+
let released = false;
|
|
953
|
+
return () => {
|
|
954
|
+
if (released)
|
|
955
|
+
return;
|
|
956
|
+
released = true;
|
|
957
|
+
const retained = cache.entries.get(sessionID);
|
|
958
|
+
if (retained && trajectory)
|
|
959
|
+
retained.trajectoryReaders--;
|
|
960
|
+
subscription.readers--;
|
|
961
|
+
if (trajectory)
|
|
962
|
+
subscription.trajectory--;
|
|
963
|
+
if (subscription.readers)
|
|
964
|
+
cache.subscriptions.set(sessionID, subscription);
|
|
965
|
+
else {
|
|
966
|
+
cache.subscriptions.delete(sessionID);
|
|
967
|
+
const loading = cache.pending.get(sessionID);
|
|
968
|
+
if (loading)
|
|
969
|
+
cancelUnused(sessionID, loading);
|
|
970
|
+
if (!cache.legacyClients) {
|
|
971
|
+
clearTimeout(cache.timers.get(sessionID));
|
|
972
|
+
cache.timers.delete(sessionID);
|
|
973
|
+
if (!cache.publishing.has(sessionID))
|
|
974
|
+
cache.refreshRequested.delete(sessionID);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
ExecutionService.subscribe = subscribe;
|
|
980
|
+
function legacyClient() {
|
|
981
|
+
const cache = state();
|
|
982
|
+
cache.legacyClients++;
|
|
983
|
+
let released = false;
|
|
984
|
+
return () => {
|
|
985
|
+
if (released)
|
|
986
|
+
return;
|
|
987
|
+
released = true;
|
|
988
|
+
cache.legacyClients--;
|
|
989
|
+
for (const [sessionID, loading] of cache.pending)
|
|
990
|
+
cancelUnused(sessionID, loading);
|
|
991
|
+
if (!cache.legacyClients)
|
|
992
|
+
for (const [sessionID, timer] of cache.timers)
|
|
993
|
+
if (!cache.subscriptions.has(sessionID)) {
|
|
994
|
+
clearTimeout(timer);
|
|
995
|
+
cache.timers.delete(sessionID);
|
|
996
|
+
if (!cache.publishing.has(sessionID))
|
|
997
|
+
cache.refreshRequested.delete(sessionID);
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
ExecutionService.legacyClient = legacyClient;
|
|
1002
|
+
function init() {
|
|
1003
|
+
const cache = state();
|
|
1004
|
+
if (cache.dispose)
|
|
1005
|
+
return cache.dispose;
|
|
1006
|
+
const schedule = (sessionID) => {
|
|
1007
|
+
for (const owner of cache.entries.values()) {
|
|
1008
|
+
const entry = owner.tree;
|
|
1009
|
+
if (!entry)
|
|
1010
|
+
continue;
|
|
1011
|
+
if (!entry.changes.owners.has(sessionID))
|
|
1012
|
+
continue;
|
|
1013
|
+
cache.dirty.add(entry.root.id);
|
|
1014
|
+
if (!cache.subscriptions.has(entry.root.id) && !cache.legacyClients)
|
|
1015
|
+
continue;
|
|
1016
|
+
cache.refreshRequested.add(entry.root.id);
|
|
1017
|
+
if ((!cache.subscriptions.has(entry.root.id) && Date.now() - entry.touched > 600_000) ||
|
|
1018
|
+
cache.timers.has(entry.root.id) ||
|
|
1019
|
+
cache.publishing.has(entry.root.id))
|
|
1020
|
+
continue;
|
|
1021
|
+
const timer = setTimeout(async () => {
|
|
1022
|
+
cache.timers.delete(entry.root.id);
|
|
1023
|
+
if (!cache.subscriptions.has(entry.root.id) && !cache.legacyClients)
|
|
1024
|
+
return;
|
|
1025
|
+
cache.publishing.add(entry.root.id);
|
|
1026
|
+
cache.refreshRequested.delete(entry.root.id);
|
|
1027
|
+
await ScopeContext.provide({
|
|
1028
|
+
scope: entry.root.scope,
|
|
1029
|
+
fn: async () => {
|
|
1030
|
+
const previousRevision = entry.publishedRevision;
|
|
1031
|
+
const next = await data(entry.root.id, false);
|
|
1032
|
+
const revision = next.revision;
|
|
1033
|
+
const detail = owner.trajectoryReaders > 0 || cache.legacyClients > 0;
|
|
1034
|
+
const process = detail ? processRows(next, "process") : [];
|
|
1035
|
+
const processUpserts = process.filter((node) => JSON.stringify({ ...node, revision: 0 }) !==
|
|
1036
|
+
JSON.stringify({ ...entry.processRows.get(node.id), revision: 0 }));
|
|
1037
|
+
const processIDs = new Set(process.map((node) => node.id));
|
|
1038
|
+
const processRemoved = [...entry.processRows.keys()].filter((id) => !processIDs.has(id));
|
|
1039
|
+
const records = detail ? processRows(next, "records") : [];
|
|
1040
|
+
const upserts = records.filter((node) => JSON.stringify({ ...node, revision: 0 }) !==
|
|
1041
|
+
JSON.stringify({ ...entry.recordRows.get(node.id), revision: 0 }));
|
|
1042
|
+
const ids = new Set(next.nodes.map((node) => node.id));
|
|
1043
|
+
const contexts = contextSnapshots(next).filter((context) => (next !== entry || next.changed.has(context.nodeID)) &&
|
|
1044
|
+
JSON.stringify(context) !== next.contextRows.get(context.nodeID));
|
|
1045
|
+
await Bus.publish(ExecutionSchema.Updated, {
|
|
1046
|
+
sessionID: entry.root.id,
|
|
1047
|
+
revision,
|
|
1048
|
+
previousRevision,
|
|
1049
|
+
summary: next.summary,
|
|
1050
|
+
roundSummaries: [...next.selections.values()].map((selection) => selection.summary),
|
|
1051
|
+
contextUpserts: contexts,
|
|
1052
|
+
upserts: upserts.map((node) => ({ ...node, revision })),
|
|
1053
|
+
processUpserts: processUpserts.map((node) => ({ ...node, revision })),
|
|
1054
|
+
processRemoved,
|
|
1055
|
+
removed: next === entry
|
|
1056
|
+
? [...next.removed]
|
|
1057
|
+
: entry.nodes.filter((node) => !ids.has(node.id)).map((node) => node.id),
|
|
1058
|
+
});
|
|
1059
|
+
next.processRows = new Map(process.map((node) => [node.id, node]));
|
|
1060
|
+
next.recordRows = new Map(records.map((node) => [node.id, node]));
|
|
1061
|
+
for (const context of contexts)
|
|
1062
|
+
next.contextRows.set(context.nodeID, JSON.stringify(context));
|
|
1063
|
+
for (const id of next.contextRows.keys())
|
|
1064
|
+
if (!ids.has(id))
|
|
1065
|
+
next.contextRows.delete(id);
|
|
1066
|
+
next.publishedRevision = revision;
|
|
1067
|
+
if (next.revision === revision) {
|
|
1068
|
+
next.changed.clear();
|
|
1069
|
+
next.removed.clear();
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
})
|
|
1073
|
+
.catch((error) => {
|
|
1074
|
+
if (error instanceof Storage.NotFoundError) {
|
|
1075
|
+
cache.entries.delete(entry.root.id);
|
|
1076
|
+
cache.dirty.delete(entry.root.id);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
log.warn("execution snapshot update failed", { error });
|
|
1080
|
+
const current = cache.entries.get(entry.root.id)?.tree;
|
|
1081
|
+
if (current)
|
|
1082
|
+
for (const owner of current.changes.owners)
|
|
1083
|
+
current.changes.repair.add(owner);
|
|
1084
|
+
cache.dirty.add(entry.root.id);
|
|
1085
|
+
})
|
|
1086
|
+
.finally(() => {
|
|
1087
|
+
cache.publishing.delete(entry.root.id);
|
|
1088
|
+
if (cache.entries.get(entry.root.id) === owner && cache.refreshRequested.delete(entry.root.id))
|
|
1089
|
+
schedule(entry.root.id);
|
|
1090
|
+
});
|
|
1091
|
+
}, 750);
|
|
1092
|
+
timer.unref();
|
|
1093
|
+
cache.timers.set(entry.root.id, timer);
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
cache.schedule = schedule;
|
|
1097
|
+
const deliver = (ownerID, key, apply, parentID) => {
|
|
1098
|
+
const buffers = new Set([
|
|
1099
|
+
...[...cache.entries.values()].flatMap((entry) => (entry.tree ? [entry.tree.changes] : [])),
|
|
1100
|
+
...[...cache.pending.values()].map((loading) => loading.changes),
|
|
1101
|
+
]);
|
|
1102
|
+
for (const changes of buffers) {
|
|
1103
|
+
if (parentID && changes.owners.has(parentID))
|
|
1104
|
+
changes.owners.add(ownerID);
|
|
1105
|
+
if (!changes.owners.has(ownerID))
|
|
1106
|
+
continue;
|
|
1107
|
+
enqueue(changes, ownerID, key, apply);
|
|
1108
|
+
}
|
|
1109
|
+
schedule(parentID ?? ownerID);
|
|
1110
|
+
};
|
|
1111
|
+
const message = (sessionID, messageID) => deliver(sessionID, "message:" + messageID, (entry) => entry.pendingMessages.set(messageID, sessionID));
|
|
1112
|
+
const disposers = [
|
|
1113
|
+
Bus.subscribeGlobal(RolloutEvents.Updated, (event) => {
|
|
1114
|
+
const { owner, revision, record } = event.properties;
|
|
1115
|
+
if (owner.kind !== "session")
|
|
1116
|
+
return;
|
|
1117
|
+
queueRoot(owner.sessionID, "rollout:" + revision, (value) => {
|
|
1118
|
+
const snapshot = value.snapshots[0];
|
|
1119
|
+
if (revision <= snapshot.revision)
|
|
1120
|
+
return;
|
|
1121
|
+
if (revision !== snapshot.revision + 1) {
|
|
1122
|
+
const entry = cache.entries.get(owner.sessionID);
|
|
1123
|
+
if (entry)
|
|
1124
|
+
entry.metadataRepair = true;
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
snapshot.revision = revision;
|
|
1128
|
+
if (record.kind === "run")
|
|
1129
|
+
update(snapshot.runs, record.value);
|
|
1130
|
+
if (record.kind === "call") {
|
|
1131
|
+
const prior = snapshot.calls.find((call) => call.id === record.value.id);
|
|
1132
|
+
value.orderChanged ||=
|
|
1133
|
+
!prior ||
|
|
1134
|
+
prior.started !== record.value.started ||
|
|
1135
|
+
prior.runID !== record.value.runID ||
|
|
1136
|
+
prior.usageRole !== record.value.usageRole ||
|
|
1137
|
+
record.value.usageRole === "compaction";
|
|
1138
|
+
update(snapshot.calls, record.value);
|
|
1139
|
+
value.changed.add(record.value.id);
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
deliver(owner.sessionID, `rollout:${owner.sessionID}:${revision}`, (entry) => apply(entry, owner, revision, record));
|
|
1143
|
+
}),
|
|
1144
|
+
Bus.subscribeGlobal(SessionInbox.Event.Updated, (event) => {
|
|
1145
|
+
const { sessionID, items } = event.properties;
|
|
1146
|
+
deliver(sessionID, "inbox:" + sessionID, (entry) => entry.inboxes.set(sessionID, items));
|
|
1147
|
+
}),
|
|
1148
|
+
Bus.subscribeGlobal(Usage.Updated, (event) => {
|
|
1149
|
+
const { owner, record } = event.properties;
|
|
1150
|
+
if (owner.kind !== "session")
|
|
1151
|
+
return;
|
|
1152
|
+
queueRoot(owner.sessionID, "usage:" + record.id, (value) => {
|
|
1153
|
+
if ((value.usage.get(record.id)?.revision ?? -1) >= record.revision)
|
|
1154
|
+
return;
|
|
1155
|
+
value.usage.set(record.id, record);
|
|
1156
|
+
if (record.kind === "call" || record.kind === "attempt") {
|
|
1157
|
+
const id = record.kind === "call" ? record.entityID : record.callID;
|
|
1158
|
+
value.changed.add(id);
|
|
1159
|
+
const records = value.usageByCall.get(id) ?? [];
|
|
1160
|
+
update(records, record);
|
|
1161
|
+
value.usageByCall.set(id, records);
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
deliver(owner.sessionID, "usage:" + record.id, (entry) => {
|
|
1165
|
+
entry.usage.set(record.id, record);
|
|
1166
|
+
if (record.kind === "call" || record.kind === "attempt")
|
|
1167
|
+
entry.changed.add(nodeID(owner.sessionID, "call", record.kind === "call" ? record.entityID : record.callID));
|
|
1168
|
+
});
|
|
1169
|
+
}),
|
|
1170
|
+
Bus.subscribeGlobal(MessageV2.Event.Updated, (event) => {
|
|
1171
|
+
const info = event.properties.info;
|
|
1172
|
+
queueRoot(info.sessionID, "message:" + info.id, (value) => {
|
|
1173
|
+
value.pendingMessages.add(info.id);
|
|
1174
|
+
});
|
|
1175
|
+
message(info.sessionID, info.id);
|
|
1176
|
+
}),
|
|
1177
|
+
Bus.subscribeGlobal(MessageV2.Event.PartUpdated, (event) => message(event.properties.part.sessionID, event.properties.part.messageID)),
|
|
1178
|
+
Bus.subscribeGlobal(MessageV2.Event.Removed, (event) => {
|
|
1179
|
+
const { sessionID, messageID } = event.properties;
|
|
1180
|
+
queueRoot(sessionID, "message:" + messageID, (value) => {
|
|
1181
|
+
value.pendingMessages.add(messageID);
|
|
1182
|
+
});
|
|
1183
|
+
message(sessionID, messageID);
|
|
1184
|
+
}),
|
|
1185
|
+
Bus.subscribeGlobal(SessionEvent.Deleted, (event) => {
|
|
1186
|
+
const id = event.properties.info.id;
|
|
1187
|
+
const entry = cache.entries.get(id);
|
|
1188
|
+
entry?.metadataLoading?.controller.abort();
|
|
1189
|
+
entry?.audit?.controller.abort();
|
|
1190
|
+
cache.pending.get(id)?.controller.abort();
|
|
1191
|
+
clearTimeout(entry?.metadataTimer);
|
|
1192
|
+
clearTimeout(cache.timers.get(id));
|
|
1193
|
+
cache.entries.delete(id);
|
|
1194
|
+
cache.dirty.delete(id);
|
|
1195
|
+
cache.timers.delete(id);
|
|
1196
|
+
cache.refreshRequested.delete(id);
|
|
1197
|
+
}),
|
|
1198
|
+
Bus.subscribeGlobal(SessionEvent.Updated, (event) => {
|
|
1199
|
+
const info = event.properties.info;
|
|
1200
|
+
queueRoot(info.id, "session", (value) => {
|
|
1201
|
+
value.root = info;
|
|
1202
|
+
});
|
|
1203
|
+
deliver(info.id, "session:" + info.id, (entry) => {
|
|
1204
|
+
const index = entry.sessions.findIndex((session) => session.id === info.id);
|
|
1205
|
+
if (index >= 0) {
|
|
1206
|
+
const previous = entry.sessions[index];
|
|
1207
|
+
const title = info.cortex?.description ?? info.title;
|
|
1208
|
+
if (title !== (previous.cortex?.description ?? previous.title))
|
|
1209
|
+
for (const node of entry.nodes)
|
|
1210
|
+
if (node.sessionID === info.id && (node.kind === "turn" || node.kind === "subtask")) {
|
|
1211
|
+
node.title = title;
|
|
1212
|
+
entry.changed.add(node.id);
|
|
1213
|
+
}
|
|
1214
|
+
entry.sessions[index] = info;
|
|
1215
|
+
entry.sessionsByID.set(info.id, info);
|
|
1216
|
+
}
|
|
1217
|
+
else
|
|
1218
|
+
entry.addedSessions.set(info.id, info);
|
|
1219
|
+
if (info.id === entry.root.id)
|
|
1220
|
+
entry.root = info;
|
|
1221
|
+
}, info.parentID);
|
|
1222
|
+
}),
|
|
1223
|
+
];
|
|
1224
|
+
cache.dispose = () => {
|
|
1225
|
+
for (const dispose of disposers)
|
|
1226
|
+
dispose();
|
|
1227
|
+
for (const timer of cache.timers.values())
|
|
1228
|
+
clearTimeout(timer);
|
|
1229
|
+
cache.timers.clear();
|
|
1230
|
+
cache.refreshRequested.clear();
|
|
1231
|
+
for (const loading of cache.pending.values())
|
|
1232
|
+
loading.controller.abort();
|
|
1233
|
+
for (const entry of cache.entries.values()) {
|
|
1234
|
+
entry.metadataLoading?.controller.abort();
|
|
1235
|
+
entry.audit?.controller.abort();
|
|
1236
|
+
clearTimeout(entry.metadataTimer);
|
|
1237
|
+
entry.metadataListeners.clear();
|
|
1238
|
+
entry.progressListeners.clear();
|
|
1239
|
+
}
|
|
1240
|
+
cache.schedule = undefined;
|
|
1241
|
+
cache.entries.clear();
|
|
1242
|
+
cache.subscriptions.clear();
|
|
1243
|
+
cache.legacyClients = 0;
|
|
1244
|
+
cache.dispose = undefined;
|
|
1245
|
+
};
|
|
1246
|
+
return cache.dispose;
|
|
1247
|
+
}
|
|
1248
|
+
ExecutionService.init = init;
|
|
1249
|
+
function contextSnapshots(value, runID, retained) {
|
|
1250
|
+
const snapshot = value.snapshots.find((entry) => entry.owner.kind === "session" && entry.owner.sessionID === value.root.id);
|
|
1251
|
+
if (!snapshot)
|
|
1252
|
+
return [];
|
|
1253
|
+
const records = retained?.usageByCall ?? new Map();
|
|
1254
|
+
for (const record of retained ? [] : value.usage.values()) {
|
|
1255
|
+
const id = record.kind === "call" ? record.entityID : record.kind === "attempt" ? record.callID : undefined;
|
|
1256
|
+
if (!id)
|
|
1257
|
+
continue;
|
|
1258
|
+
const group = records.get(id) ?? [];
|
|
1259
|
+
group.push(record);
|
|
1260
|
+
records.set(id, group);
|
|
1261
|
+
}
|
|
1262
|
+
const cached = new Map(retained?.rows.map((row) => [row.callID, row]));
|
|
1263
|
+
let previous = -Infinity;
|
|
1264
|
+
const rounds = new Map();
|
|
1265
|
+
const compactions = snapshot.calls.filter((call) => call.usageRole === "compaction" && call.status === "completed");
|
|
1266
|
+
const ordered = retained && !retained.orderChanged
|
|
1267
|
+
? retained.orderedCalls
|
|
1268
|
+
: snapshot.calls
|
|
1269
|
+
.filter((call) => call.usageRole === "conversation")
|
|
1270
|
+
.sort((a, b) => a.started - b.started || a.id.localeCompare(b.id));
|
|
1271
|
+
if (retained)
|
|
1272
|
+
retained.orderedCalls = ordered;
|
|
1273
|
+
const currentCalls = new Map(snapshot.calls.map((call) => [call.id, call]));
|
|
1274
|
+
return ordered
|
|
1275
|
+
.map((call, index) => {
|
|
1276
|
+
call = currentCalls.get(call.id);
|
|
1277
|
+
if (!rounds.has(call.runID))
|
|
1278
|
+
rounds.set(call.runID, rounds.size + 1);
|
|
1279
|
+
const existing = cached.get(call.id);
|
|
1280
|
+
if (existing && retained && !retained.orderChanged && !retained.changed.has(call.id)) {
|
|
1281
|
+
previous = call.started;
|
|
1282
|
+
return existing;
|
|
1283
|
+
}
|
|
1284
|
+
const candidate = value.contexts.get(call.id);
|
|
1285
|
+
const accounting = Usage.summarize(records.get(call.id) ?? [], {
|
|
1286
|
+
sessionID: value.root.id,
|
|
1287
|
+
includeDescendants: false,
|
|
1288
|
+
});
|
|
1289
|
+
const context = accounting.context;
|
|
1290
|
+
const attempt = (records.get(call.id) ?? []).find((entry) => entry.kind === "attempt" && entry.entityID === context?.attemptID);
|
|
1291
|
+
const measured = attempt?.kind === "attempt" ? attempt.usage : undefined;
|
|
1292
|
+
const estimate = call.contextEstimate?.modelID === call.model.modelID &&
|
|
1293
|
+
call.contextEstimate.providerID === call.model.providerID
|
|
1294
|
+
? call.contextEstimate
|
|
1295
|
+
: undefined;
|
|
1296
|
+
const usage = candidate &&
|
|
1297
|
+
candidate.providerID === call.model.providerID &&
|
|
1298
|
+
candidate.modelID === call.model.modelID &&
|
|
1299
|
+
(context?.inputTokens == null || candidate.totalInput === context.inputTokens)
|
|
1300
|
+
? candidate
|
|
1301
|
+
: estimate && context?.inputTokens != null
|
|
1302
|
+
? reconcileContextUsage(estimate, context.inputTokens, estimate.capturedAt)
|
|
1303
|
+
: null;
|
|
1304
|
+
const result = {
|
|
1305
|
+
sessionID: value.root.id,
|
|
1306
|
+
callID: call.id,
|
|
1307
|
+
nodeID: nodeID(value.root.id, "call", call.id),
|
|
1308
|
+
runID: call.runID,
|
|
1309
|
+
started: call.started,
|
|
1310
|
+
requestNumber: index + 1,
|
|
1311
|
+
roundNumber: rounds.get(call.runID),
|
|
1312
|
+
status: call.status,
|
|
1313
|
+
modelID: call.model.modelID,
|
|
1314
|
+
providerID: call.model.providerID,
|
|
1315
|
+
inputTokens: usage?.totalInput ?? context?.inputTokens ?? null,
|
|
1316
|
+
contextLimit: usage?.usableInputLimit ??
|
|
1317
|
+
usage?.contextLimit ??
|
|
1318
|
+
estimate?.usableInputLimit ??
|
|
1319
|
+
estimate?.contextLimit ??
|
|
1320
|
+
(ModelLimit.usableInput(call.model.limits) || null),
|
|
1321
|
+
outputTokens: measured?.output.total ?? null,
|
|
1322
|
+
cacheHit: measured?.input.total && measured.input.cacheRead != null
|
|
1323
|
+
? Math.min(1, measured.input.cacheRead / measured.input.total)
|
|
1324
|
+
: null,
|
|
1325
|
+
elapsedMs: call.ended == null ? null : Math.max(0, call.ended - call.started),
|
|
1326
|
+
retries: accounting.outcomes.retries,
|
|
1327
|
+
usage,
|
|
1328
|
+
estimate,
|
|
1329
|
+
compactedBefore: compactions.some((entry) => (entry.ended ?? entry.started) > previous && (entry.ended ?? entry.started) <= call.started),
|
|
1330
|
+
requestAvailable: !!call.request,
|
|
1331
|
+
};
|
|
1332
|
+
previous = call.started;
|
|
1333
|
+
return result;
|
|
1334
|
+
})
|
|
1335
|
+
.filter((entry) => !runID || entry.runID === runID)
|
|
1336
|
+
.reverse();
|
|
1337
|
+
}
|
|
1338
|
+
function contextPosition(cursor, identity) {
|
|
1339
|
+
if (!cursor)
|
|
1340
|
+
return undefined;
|
|
1341
|
+
const parsed = queryCursor(z.object({ identity: z.string(), id: z.string() }), cursor);
|
|
1342
|
+
if (parsed.identity !== identity)
|
|
1343
|
+
throw new ExecutionQueryError("Context cursor does not match this selection");
|
|
1344
|
+
return parsed.id;
|
|
1345
|
+
}
|
|
1346
|
+
const contextCursor = (identity, id) => Buffer.from(JSON.stringify({ identity, id })).toString("base64url");
|
|
1347
|
+
async function bootstrapRoot(root, signal) {
|
|
1348
|
+
const sessionID = root.id;
|
|
1349
|
+
const [snapshot, records, infos] = await Promise.all([
|
|
1350
|
+
RolloutSnapshot.modelCalls({ kind: "session", scopeID: root.scope.id, sessionID }, signal),
|
|
1351
|
+
Usage.collect({ scopeID: root.scope.id, sessionID, includeDescendants: false }, signal),
|
|
1352
|
+
MessageV2.readInfoList({
|
|
1353
|
+
scopeID: Identifier.asScopeID(root.scope.id),
|
|
1354
|
+
sessionID: Identifier.asSessionID(sessionID),
|
|
1355
|
+
signal,
|
|
1356
|
+
}),
|
|
1357
|
+
]);
|
|
1358
|
+
checkpoint(signal);
|
|
1359
|
+
const contexts = new Map();
|
|
1360
|
+
for (const info of infos)
|
|
1361
|
+
rememberContext(contexts, info);
|
|
1362
|
+
const usageByCall = new Map();
|
|
1363
|
+
for (const record of records) {
|
|
1364
|
+
const id = record.kind === "call" ? record.entityID : record.kind === "attempt" ? record.callID : undefined;
|
|
1365
|
+
if (!id)
|
|
1366
|
+
continue;
|
|
1367
|
+
const group = usageByCall.get(id) ?? [];
|
|
1368
|
+
group.push(record);
|
|
1369
|
+
usageByCall.set(id, group);
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
root,
|
|
1373
|
+
snapshots: [snapshot],
|
|
1374
|
+
usage: new Map(records.map((record) => [record.id, record])),
|
|
1375
|
+
infos: new Map(infos.map((info) => [info.id, info])),
|
|
1376
|
+
contexts,
|
|
1377
|
+
rows: [],
|
|
1378
|
+
revision: 0,
|
|
1379
|
+
changed: new Set(),
|
|
1380
|
+
orderChanged: true,
|
|
1381
|
+
usageByCall,
|
|
1382
|
+
orderedCalls: [],
|
|
1383
|
+
pendingMessages: new Set(),
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function refreshContexts(value) {
|
|
1387
|
+
const next = new Map();
|
|
1388
|
+
for (const info of [...value.infos.values()].sort((a, b) => a.time.created - b.time.created || a.id.localeCompare(b.id)))
|
|
1389
|
+
rememberContext(next, info);
|
|
1390
|
+
for (const id of new Set([...next.keys(), ...value.contexts.keys()]))
|
|
1391
|
+
if (JSON.stringify(next.get(id)) !== JSON.stringify(value.contexts.get(id)))
|
|
1392
|
+
value.changed.add(id);
|
|
1393
|
+
value.contexts = next;
|
|
1394
|
+
}
|
|
1395
|
+
function queueRoot(sessionID, key, apply) {
|
|
1396
|
+
const entry = state().entries.get(sessionID);
|
|
1397
|
+
if (!entry || (!entry.metadata && !entry.metadataLoading))
|
|
1398
|
+
return;
|
|
1399
|
+
if (entry.metadataChanges.size >= 4096) {
|
|
1400
|
+
entry.metadataChanges.clear();
|
|
1401
|
+
entry.metadataRepair = true;
|
|
1402
|
+
}
|
|
1403
|
+
else
|
|
1404
|
+
entry.metadataChanges.set(key, apply);
|
|
1405
|
+
if (!entry.metadataListeners.size || entry.metadataTimer)
|
|
1406
|
+
return;
|
|
1407
|
+
entry.metadataTimer = setTimeout(() => {
|
|
1408
|
+
entry.metadataTimer = undefined;
|
|
1409
|
+
if (!entry.metadataListeners.size)
|
|
1410
|
+
return;
|
|
1411
|
+
void ScopeContext.provide({
|
|
1412
|
+
scope: entry.root.scope,
|
|
1413
|
+
fn: async () => {
|
|
1414
|
+
const value = await contextData(sessionID, undefined, false);
|
|
1415
|
+
for (const listener of entry.metadataListeners)
|
|
1416
|
+
listener(value);
|
|
1417
|
+
},
|
|
1418
|
+
}).catch((error) => log.warn("execution root update failed", { kind: error instanceof Error ? error.name : "unknown" }));
|
|
1419
|
+
}, 50);
|
|
1420
|
+
entry.metadataTimer.unref();
|
|
1421
|
+
}
|
|
1422
|
+
async function contextData(sessionID, signal, access = true) {
|
|
1423
|
+
checkpoint(signal);
|
|
1424
|
+
const root = await requireRoot(sessionID);
|
|
1425
|
+
init();
|
|
1426
|
+
const entry = entryFor(root);
|
|
1427
|
+
if (access)
|
|
1428
|
+
entry.touched = Date.now();
|
|
1429
|
+
if (!entry.metadataLoading && entry.metadata && !entry.metadataChanges.size && !entry.metadataRepair)
|
|
1430
|
+
return entry.metadata;
|
|
1431
|
+
if (entry.metadataLoading?.controller.signal.aborted) {
|
|
1432
|
+
await entry.metadataLoading.promise.catch(() => checkpoint(signal));
|
|
1433
|
+
return contextData(sessionID, signal, access);
|
|
1434
|
+
}
|
|
1435
|
+
if (!entry.metadataLoading) {
|
|
1436
|
+
const controller = new AbortController();
|
|
1437
|
+
const loading = {
|
|
1438
|
+
controller,
|
|
1439
|
+
readers: 0,
|
|
1440
|
+
promise: Promise.resolve()
|
|
1441
|
+
.then(async () => {
|
|
1442
|
+
let value = entry.metadata;
|
|
1443
|
+
do {
|
|
1444
|
+
if (!value || entry.metadataRepair) {
|
|
1445
|
+
entry.metadataRepair = false;
|
|
1446
|
+
value = await bootstrapRoot(root, controller.signal);
|
|
1447
|
+
}
|
|
1448
|
+
const changes = entry.metadataChanges;
|
|
1449
|
+
entry.metadataChanges = new Map();
|
|
1450
|
+
for (const apply of changes.values()) {
|
|
1451
|
+
checkpoint(controller.signal);
|
|
1452
|
+
apply(value);
|
|
1453
|
+
}
|
|
1454
|
+
} while (entry.metadataRepair);
|
|
1455
|
+
for (const messageID of value.pendingMessages) {
|
|
1456
|
+
checkpoint(controller.signal);
|
|
1457
|
+
try {
|
|
1458
|
+
const info = await MessageV2.readInfo({ scopeID: root.scope.id, sessionID, messageID });
|
|
1459
|
+
value.infos.set(messageID, info);
|
|
1460
|
+
}
|
|
1461
|
+
catch (error) {
|
|
1462
|
+
if (!(error instanceof Storage.NotFoundError))
|
|
1463
|
+
throw error;
|
|
1464
|
+
value.infos.delete(messageID);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (value.pendingMessages.size)
|
|
1468
|
+
refreshContexts(value);
|
|
1469
|
+
value.pendingMessages.clear();
|
|
1470
|
+
checkpoint(controller.signal);
|
|
1471
|
+
value.rows = contextSnapshots(value, undefined, value);
|
|
1472
|
+
value.changed.clear();
|
|
1473
|
+
value.orderChanged = false;
|
|
1474
|
+
value.revision = ++state().sequence;
|
|
1475
|
+
entry.metadata = value;
|
|
1476
|
+
return value;
|
|
1477
|
+
})
|
|
1478
|
+
.catch((error) => {
|
|
1479
|
+
entry.metadataRepair = true;
|
|
1480
|
+
throw error;
|
|
1481
|
+
})
|
|
1482
|
+
.finally(() => {
|
|
1483
|
+
if (entry.metadataLoading === loading)
|
|
1484
|
+
entry.metadataLoading = undefined;
|
|
1485
|
+
evict();
|
|
1486
|
+
}),
|
|
1487
|
+
};
|
|
1488
|
+
entry.metadataLoading = loading;
|
|
1489
|
+
}
|
|
1490
|
+
const loading = entry.metadataLoading;
|
|
1491
|
+
loading.readers++;
|
|
1492
|
+
let abort;
|
|
1493
|
+
try {
|
|
1494
|
+
if (!signal)
|
|
1495
|
+
return await loading.promise;
|
|
1496
|
+
return await Promise.race([
|
|
1497
|
+
loading.promise,
|
|
1498
|
+
new Promise((_, reject) => {
|
|
1499
|
+
abort = () => reject(signal.reason);
|
|
1500
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1501
|
+
if (signal.aborted)
|
|
1502
|
+
abort();
|
|
1503
|
+
}),
|
|
1504
|
+
]);
|
|
1505
|
+
}
|
|
1506
|
+
finally {
|
|
1507
|
+
if (abort)
|
|
1508
|
+
signal?.removeEventListener("abort", abort);
|
|
1509
|
+
if (--loading.readers === 0 && !entry.metadataListeners.size)
|
|
1510
|
+
loading.controller.abort();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
async function observeRoot(sessionID, listener, signal) {
|
|
1514
|
+
const root = await requireRoot(sessionID);
|
|
1515
|
+
init();
|
|
1516
|
+
const entry = entryFor(root);
|
|
1517
|
+
const update = (value) => listener({ revision: value.revision, contexts: value.rows });
|
|
1518
|
+
entry.metadataListeners.add(update);
|
|
1519
|
+
let disposed = false;
|
|
1520
|
+
const dispose = () => {
|
|
1521
|
+
if (disposed)
|
|
1522
|
+
return;
|
|
1523
|
+
disposed = true;
|
|
1524
|
+
entry.metadataListeners.delete(update);
|
|
1525
|
+
signal?.removeEventListener("abort", dispose);
|
|
1526
|
+
if (!entry.metadataListeners.size) {
|
|
1527
|
+
clearTimeout(entry.metadataTimer);
|
|
1528
|
+
entry.metadataTimer = undefined;
|
|
1529
|
+
if (entry.metadataLoading && !entry.metadataLoading.readers)
|
|
1530
|
+
entry.metadataLoading.controller.abort();
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
signal?.addEventListener("abort", dispose, { once: true });
|
|
1534
|
+
try {
|
|
1535
|
+
const value = await contextData(sessionID, signal);
|
|
1536
|
+
checkpoint(signal);
|
|
1537
|
+
update(value);
|
|
1538
|
+
return dispose;
|
|
1539
|
+
}
|
|
1540
|
+
catch (error) {
|
|
1541
|
+
dispose();
|
|
1542
|
+
throw error;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
ExecutionService.observeRoot = observeRoot;
|
|
1546
|
+
async function observePreparation(sessionID, listener) {
|
|
1547
|
+
const entry = entryFor(await requireRoot(sessionID));
|
|
1548
|
+
entry.progressListeners.add(listener);
|
|
1549
|
+
return () => {
|
|
1550
|
+
entry.progressListeners.delete(listener);
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
ExecutionService.observePreparation = observePreparation;
|
|
1554
|
+
async function contextHistory(sessionID, input, signal) {
|
|
1555
|
+
const value = await contextData(sessionID, signal);
|
|
1556
|
+
const identity = JSON.stringify([ScopeContext.current.scope.id, sessionID, input.runID ?? null]);
|
|
1557
|
+
if (input.runID && !value.snapshots[0].runs.some((run) => !run.admissionOnly && run.id === input.runID))
|
|
1558
|
+
throw new Storage.NotFoundError({ message: "Context round was not found" });
|
|
1559
|
+
const entries = value.rows.filter((row) => !input.runID || row.runID === input.runID);
|
|
1560
|
+
const id = contextPosition(input.cursor, identity);
|
|
1561
|
+
const position = id ? entries.findIndex((entry) => entry.callID === id) : -1;
|
|
1562
|
+
if (id && position < 0)
|
|
1563
|
+
throw new ExecutionQueryError("Context cursor request was not found");
|
|
1564
|
+
const items = entries.slice(position + 1, position + 1 + input.limit);
|
|
1565
|
+
return ExecutionSchema.ContextHistory.parse({
|
|
1566
|
+
sessionID,
|
|
1567
|
+
revision: value.revision,
|
|
1568
|
+
total: entries.length,
|
|
1569
|
+
items,
|
|
1570
|
+
nextCursor: position + 1 + items.length < entries.length ? contextCursor(identity, items.at(-1).callID) : null,
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
ExecutionService.contextHistory = contextHistory;
|
|
1574
|
+
async function contextSnapshot(sessionID, callID, runID, signal) {
|
|
1575
|
+
const value = await contextData(sessionID, signal);
|
|
1576
|
+
const entry = value.rows.find((entry) => entry.callID === callID && (!runID || entry.runID === runID));
|
|
1577
|
+
if (!entry)
|
|
1578
|
+
throw new Storage.NotFoundError({ message: "Context request was not found in this session" });
|
|
1579
|
+
return ExecutionSchema.ContextSnapshot.parse(entry);
|
|
1580
|
+
}
|
|
1581
|
+
ExecutionService.contextSnapshot = contextSnapshot;
|
|
1582
|
+
async function contextItems(sessionID, callID, input, signal) {
|
|
1583
|
+
const snapshot = await contextSnapshot(sessionID, callID, input.runID, signal);
|
|
1584
|
+
const empty = {
|
|
1585
|
+
callID,
|
|
1586
|
+
nodeID: snapshot.nodeID,
|
|
1587
|
+
contentVersion: null,
|
|
1588
|
+
status: "unavailable",
|
|
1589
|
+
items: [],
|
|
1590
|
+
total: 0,
|
|
1591
|
+
nextCursor: null,
|
|
1592
|
+
truncated: false,
|
|
1593
|
+
};
|
|
1594
|
+
if (!snapshot.requestAvailable)
|
|
1595
|
+
return ExecutionSchema.ContextItems.parse(empty);
|
|
1596
|
+
const source = await contentSource(sessionID, snapshot.nodeID, "request", input.runID, input.version, signal);
|
|
1597
|
+
const manifest = z
|
|
1598
|
+
.array(MessageV2.ContextSource)
|
|
1599
|
+
.safeParse(await ExecutionContent.value(source, ["contextSources"], signal));
|
|
1600
|
+
if (!manifest.success)
|
|
1601
|
+
return ExecutionSchema.ContextItems.parse({ ...empty, contentVersion: source.contentVersion, status: "legacy" });
|
|
1602
|
+
const sections = new Map();
|
|
1603
|
+
let next;
|
|
1604
|
+
let truncated = false;
|
|
1605
|
+
do {
|
|
1606
|
+
const page = await ExecutionContent.sections(source, { field: "request", limit: 500, cursor: next }, signal);
|
|
1607
|
+
for (const section of page.items)
|
|
1608
|
+
sections.set(JSON.stringify(section.path), section);
|
|
1609
|
+
next = page.nextCursor ?? undefined;
|
|
1610
|
+
truncated ||= page.truncated;
|
|
1611
|
+
} while (next);
|
|
1612
|
+
const entries = manifest.data.flatMap((entry, index) => {
|
|
1613
|
+
const section = sections.get(JSON.stringify(entry.path));
|
|
1614
|
+
if (!section) {
|
|
1615
|
+
truncated = true;
|
|
1616
|
+
return [];
|
|
1617
|
+
}
|
|
1618
|
+
if (input.category && input.category !== entry.category)
|
|
1619
|
+
return [];
|
|
1620
|
+
if (input.query && !entry.source.toLocaleLowerCase().includes(input.query.toLocaleLowerCase()))
|
|
1621
|
+
return [];
|
|
1622
|
+
return [{ ...entry, id: String(index), offset: section.offset, bytes: section.bytes }];
|
|
1623
|
+
});
|
|
1624
|
+
const identity = JSON.stringify([
|
|
1625
|
+
ScopeContext.current.scope.id,
|
|
1626
|
+
sessionID,
|
|
1627
|
+
callID,
|
|
1628
|
+
source.contentVersion,
|
|
1629
|
+
input.category,
|
|
1630
|
+
input.query,
|
|
1631
|
+
input.runID,
|
|
1632
|
+
]);
|
|
1633
|
+
const id = contextPosition(input.cursor, identity);
|
|
1634
|
+
const offset = id ? entries.findIndex((entry) => entry.id === id) + 1 : 0;
|
|
1635
|
+
if (id && !offset)
|
|
1636
|
+
throw new ExecutionQueryError("Context item cursor was not found");
|
|
1637
|
+
const items = entries.slice(offset, offset + input.limit);
|
|
1638
|
+
return ExecutionSchema.ContextItems.parse({
|
|
1639
|
+
callID,
|
|
1640
|
+
nodeID: snapshot.nodeID,
|
|
1641
|
+
contentVersion: source.contentVersion,
|
|
1642
|
+
status: "available",
|
|
1643
|
+
items,
|
|
1644
|
+
total: entries.length,
|
|
1645
|
+
nextCursor: offset + items.length < entries.length ? contextCursor(identity, items.at(-1).id) : null,
|
|
1646
|
+
truncated,
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
ExecutionService.contextItems = contextItems;
|
|
1650
|
+
async function summary(sessionID, runID, signal) {
|
|
1651
|
+
const value = await data(sessionID, true, signal);
|
|
1652
|
+
if (!runID)
|
|
1653
|
+
return structuredClone(summarize(value));
|
|
1654
|
+
if (!value.summary.rounds.some((round) => round.id === runID))
|
|
1655
|
+
throw new Storage.NotFoundError({ message: "Execution round was not found" });
|
|
1656
|
+
const summary = summarize(value, runID);
|
|
1657
|
+
value.selections.delete(runID);
|
|
1658
|
+
value.selections.set(runID, { summary, touched: Date.now() });
|
|
1659
|
+
if (value.selections.size > 4)
|
|
1660
|
+
value.selections.delete(value.selections.keys().next().value);
|
|
1661
|
+
return structuredClone(summary);
|
|
1662
|
+
}
|
|
1663
|
+
ExecutionService.summary = summary;
|
|
1664
|
+
async function verifyJournal(sessionID, signal) {
|
|
1665
|
+
const value = await data(sessionID, true, signal);
|
|
1666
|
+
const entry = entryFor(value.root);
|
|
1667
|
+
if (entry.audit?.controller.signal.aborted) {
|
|
1668
|
+
await entry.audit.promise.catch(() => checkpoint(signal));
|
|
1669
|
+
return verifyJournal(sessionID, signal);
|
|
1670
|
+
}
|
|
1671
|
+
if (entry.audit) {
|
|
1672
|
+
await consume(sessionID, entry.audit, signal);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
const owners = value.snapshots.filter((snapshot) => snapshot.owner.kind === "session" && !value.verified.has(snapshot.owner.sessionID));
|
|
1676
|
+
if (!owners.length)
|
|
1677
|
+
return;
|
|
1678
|
+
const controller = new AbortController();
|
|
1679
|
+
const loading = {
|
|
1680
|
+
scopeID: value.root.scope.id,
|
|
1681
|
+
changes: value.changes,
|
|
1682
|
+
readers: 0,
|
|
1683
|
+
controller,
|
|
1684
|
+
promise: Promise.resolve()
|
|
1685
|
+
.then(async () => {
|
|
1686
|
+
const snapshots = await RolloutSnapshot.currentAll(owners.map((snapshot) => snapshot.owner), controller.signal);
|
|
1687
|
+
checkpoint(controller.signal);
|
|
1688
|
+
for (const verified of snapshots) {
|
|
1689
|
+
if (verified.owner.kind !== "session")
|
|
1690
|
+
continue;
|
|
1691
|
+
const current = value.snapshotsByID.get(verified.owner.sessionID);
|
|
1692
|
+
if (!current || current.revision !== verified.revision)
|
|
1693
|
+
continue;
|
|
1694
|
+
current.gaps = verified.gaps;
|
|
1695
|
+
value.verified.add(verified.owner.sessionID);
|
|
1696
|
+
}
|
|
1697
|
+
value.revision = ++state().sequence;
|
|
1698
|
+
value.summary = summarize(value);
|
|
1699
|
+
for (const [runID, selection] of value.selections)
|
|
1700
|
+
selection.summary = summarize(value, runID);
|
|
1701
|
+
return value;
|
|
1702
|
+
})
|
|
1703
|
+
.finally(() => {
|
|
1704
|
+
if (entry.audit === loading)
|
|
1705
|
+
entry.audit = undefined;
|
|
1706
|
+
}),
|
|
1707
|
+
};
|
|
1708
|
+
entry.audit = loading;
|
|
1709
|
+
await consume(sessionID, loading, signal);
|
|
1710
|
+
}
|
|
1711
|
+
ExecutionService.verifyJournal = verifyJournal;
|
|
1712
|
+
async function trajectory(sessionID, input, signal) {
|
|
1713
|
+
const value = await data(sessionID, true, signal);
|
|
1714
|
+
if (input.runID && !value.summary.rounds.some((round) => round.id === input.runID))
|
|
1715
|
+
throw new Storage.NotFoundError({ message: "Execution round was not found" });
|
|
1716
|
+
if (input.session && !value.sessions.some((session) => session.id === input.session))
|
|
1717
|
+
throw new Storage.NotFoundError({ message: "Executor does not belong to this task" });
|
|
1718
|
+
const query = input.query?.trim().toLocaleLowerCase();
|
|
1719
|
+
const kinds = input.kinds
|
|
1720
|
+
?.split(",")
|
|
1721
|
+
.filter(Boolean)
|
|
1722
|
+
.map((kind) => queryValue(ExecutionSchema.Node.shape.kind, kind));
|
|
1723
|
+
const statuses = input.statuses
|
|
1724
|
+
?.split(",")
|
|
1725
|
+
.filter(Boolean)
|
|
1726
|
+
.map((status) => queryValue(ExecutionSchema.Node.shape.status, status));
|
|
1727
|
+
const actualActivities = activityFor(value, input.runID).nodes;
|
|
1728
|
+
const activityNodes = new Map(actualActivities.map((node) => [node.id, node.activity]));
|
|
1729
|
+
const activityPositions = new Map(actualActivities.map((node, index) => [node.id, index]));
|
|
1730
|
+
const rows = processRows(value, input.mode, input.runID)
|
|
1731
|
+
.map((node) => ({ ...node, activity: activityNodes.get(node.id) }))
|
|
1732
|
+
.filter((node) => (input.session
|
|
1733
|
+
? node.sessionID === input.session
|
|
1734
|
+
: !!query ||
|
|
1735
|
+
input.actor === "all" ||
|
|
1736
|
+
node.sessionID === sessionID ||
|
|
1737
|
+
(node.kind === "subtask" && value.sessionsByID.get(node.sessionID)?.parentID === sessionID)) &&
|
|
1738
|
+
(!input.kind || node.kind === input.kind) &&
|
|
1739
|
+
(!input.status || node.status === input.status) &&
|
|
1740
|
+
(!kinds?.length || kinds.includes(node.kind)) &&
|
|
1741
|
+
(!statuses?.length || statuses.includes(node.status)) &&
|
|
1742
|
+
(!input.anomalies || node.kind === "retry" || ["failed", "cancelled", "interrupted"].includes(node.status)) &&
|
|
1743
|
+
(input.activityFrom === undefined || (activityPositions.get(node.id) ?? -1) >= input.activityFrom) &&
|
|
1744
|
+
(input.activityTo === undefined || (activityPositions.get(node.id) ?? Infinity) <= input.activityTo) &&
|
|
1745
|
+
(input.from === undefined || node.started >= input.from) &&
|
|
1746
|
+
(input.to === undefined || node.started <= input.to) &&
|
|
1747
|
+
(!query ||
|
|
1748
|
+
(node.title + " " + node.preview + " " + (node.tool ?? "") + " " + (node.agent ?? ""))
|
|
1749
|
+
.toLocaleLowerCase()
|
|
1750
|
+
.includes(query)));
|
|
1751
|
+
if (input.order !== "time") {
|
|
1752
|
+
const rounds = new Map(value.summary.rounds.map((round, index) => [round.id, index]));
|
|
1753
|
+
rows.sort((a, b) => (rounds.get(a.rootRunID ?? "") ?? Number.MAX_SAFE_INTEGER) -
|
|
1754
|
+
(rounds.get(b.rootRunID ?? "") ?? Number.MAX_SAFE_INTEGER) ||
|
|
1755
|
+
(input.order === "call" ? (a.group?.started ?? a.started) - (b.group?.started ?? b.started) : 0) ||
|
|
1756
|
+
a.started - b.started ||
|
|
1757
|
+
a.id.localeCompare(b.id));
|
|
1758
|
+
}
|
|
1759
|
+
const fingerprint = JSON.stringify({
|
|
1760
|
+
...input,
|
|
1761
|
+
cursor: undefined,
|
|
1762
|
+
anchor: undefined,
|
|
1763
|
+
position: undefined,
|
|
1764
|
+
limit: undefined,
|
|
1765
|
+
});
|
|
1766
|
+
let offset = 0;
|
|
1767
|
+
if (input.cursor) {
|
|
1768
|
+
const parsed = queryCursor(z.object({ filter: z.string(), after: z.string().nullable(), before: z.string().nullable() }), input.cursor);
|
|
1769
|
+
if (parsed.filter !== fingerprint)
|
|
1770
|
+
throw new ExecutionQueryError("Trajectory cursor does not match its filters");
|
|
1771
|
+
if (parsed.after)
|
|
1772
|
+
offset = Math.max(0, rows.findIndex((node) => node.id === parsed.after) + 1);
|
|
1773
|
+
if (parsed.before)
|
|
1774
|
+
offset = Math.max(0, rows.findIndex((node) => node.id === parsed.before) - input.limit);
|
|
1775
|
+
}
|
|
1776
|
+
else if (input.anchor === "latest") {
|
|
1777
|
+
offset = Math.max(0, rows.length - input.limit);
|
|
1778
|
+
}
|
|
1779
|
+
else if (input.anchor) {
|
|
1780
|
+
const index = rows.findIndex((node) => node.id === input.anchor || node.runID === input.anchor || node.messageID === input.anchor);
|
|
1781
|
+
offset = Math.max(0, input.position === "after"
|
|
1782
|
+
? index + 1
|
|
1783
|
+
: index - (input.position === "before" ? input.limit : Math.floor(input.limit / 2)));
|
|
1784
|
+
}
|
|
1785
|
+
const items = rows.slice(offset, offset + input.limit);
|
|
1786
|
+
const cursor = (after, before) => Buffer.from(JSON.stringify({ filter: fingerprint, after, before })).toString("base64url");
|
|
1787
|
+
return ExecutionSchema.Page.parse({
|
|
1788
|
+
sessionID,
|
|
1789
|
+
revision: value.revision,
|
|
1790
|
+
total: rows.length,
|
|
1791
|
+
items,
|
|
1792
|
+
nextCursor: offset + items.length < rows.length && items.length ? cursor(items.at(-1).id, null) : null,
|
|
1793
|
+
previousCursor: offset > 0 && items.length ? cursor(null, items[0].id) : null,
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
ExecutionService.trajectory = trajectory;
|
|
1797
|
+
async function find(sessionID, id, runID, signal) {
|
|
1798
|
+
const value = await data(sessionID, true, signal);
|
|
1799
|
+
if (runID && !value.summary.rounds.some((round) => round.id === runID))
|
|
1800
|
+
throw new Storage.NotFoundError({ message: "Execution round was not found" });
|
|
1801
|
+
const node = selected(value, runID).find((node) => node.id === id);
|
|
1802
|
+
if (!node)
|
|
1803
|
+
throw new Storage.NotFoundError({ message: "Trajectory node was not found in this session" });
|
|
1804
|
+
const detail = details(value);
|
|
1805
|
+
return {
|
|
1806
|
+
value,
|
|
1807
|
+
node,
|
|
1808
|
+
evidence: detail.evidence.get(id),
|
|
1809
|
+
evidenceByID: detail.evidence,
|
|
1810
|
+
message: detail.messages.get(id),
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
function details(value) {
|
|
1814
|
+
if (value.detail)
|
|
1815
|
+
return value.detail;
|
|
1816
|
+
const evidence = new Map();
|
|
1817
|
+
const messages = new Map();
|
|
1818
|
+
for (const [index, snapshot] of value.snapshots.entries())
|
|
1819
|
+
for (const [id, record] of nodesFor(snapshot, value.sessions[index], true).evidence)
|
|
1820
|
+
evidence.set(id, record);
|
|
1821
|
+
for (const node of value.nodes) {
|
|
1822
|
+
if (node.source !== "messages" || !node.messageID)
|
|
1823
|
+
continue;
|
|
1824
|
+
const [, , partID] = JSON.parse(Buffer.from(node.id, "base64url").toString());
|
|
1825
|
+
messages.set(node.id, { sessionID: node.sessionID, messageID: node.messageID, partID });
|
|
1826
|
+
}
|
|
1827
|
+
value.detail = { evidence, messages, nodePositions: new Map(value.nodes.map((node, index) => [node.id, index])) };
|
|
1828
|
+
return value.detail;
|
|
1829
|
+
}
|
|
1830
|
+
async function requestNode(sessionID, id, runID, signal) {
|
|
1831
|
+
if (state().entries.get(sessionID)?.tree)
|
|
1832
|
+
return;
|
|
1833
|
+
let identity;
|
|
1834
|
+
try {
|
|
1835
|
+
identity = JSON.parse(Buffer.from(id, "base64url").toString());
|
|
1836
|
+
}
|
|
1837
|
+
catch {
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
const parsed = z.tuple([z.string(), z.literal("call"), z.string()]).safeParse(identity);
|
|
1841
|
+
if (!parsed.success || parsed.data[0] !== sessionID)
|
|
1842
|
+
return;
|
|
1843
|
+
const metadata = await contextData(sessionID, signal);
|
|
1844
|
+
if (metadata.root.parentID)
|
|
1845
|
+
return;
|
|
1846
|
+
const call = metadata.snapshots[0].calls.find((call) => call.id === parsed.data[2] && (!runID || call.runID === runID));
|
|
1847
|
+
if (!call)
|
|
1848
|
+
throw new Storage.NotFoundError({ message: "Execution call was not found in this session" });
|
|
1849
|
+
const snapshot = await RolloutSnapshot.projected(metadata.snapshots[0].owner, call.runID, () => checkpoint(signal));
|
|
1850
|
+
const projected = nodesFor(snapshot, metadata.root, true);
|
|
1851
|
+
const messages = await SessionHistory.executionMessages({ sessionID, signal });
|
|
1852
|
+
await appendMessages(metadata.root, snapshot, messages.filter((message) => (message.info.rootID ?? message.info.id) === call.runID), projected.nodes, new Map(), signal);
|
|
1853
|
+
projected.nodes.sort((a, b) => a.started - b.started || a.id.localeCompare(b.id));
|
|
1854
|
+
const node = projected.nodes.find((node) => node.id === id);
|
|
1855
|
+
if (!node)
|
|
1856
|
+
throw new Storage.NotFoundError({ message: "Execution call was removed" });
|
|
1857
|
+
return {
|
|
1858
|
+
value: { root: metadata.root, nodes: projected.nodes },
|
|
1859
|
+
node,
|
|
1860
|
+
evidence: projected.evidence.get(id),
|
|
1861
|
+
evidenceByID: projected.evidence,
|
|
1862
|
+
message: undefined,
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
async function node(sessionID, id, runID, signal) {
|
|
1866
|
+
const found = (await requestNode(sessionID, id, runID, signal)) ?? (await find(sessionID, id, runID, signal));
|
|
1867
|
+
const call = found.evidence?.record.kind === "call"
|
|
1868
|
+
? found.evidence
|
|
1869
|
+
: found.node.parentID
|
|
1870
|
+
? found.evidenceByID.get(found.node.parentID)
|
|
1871
|
+
: undefined;
|
|
1872
|
+
const definitions = call?.record.kind === "call" && (found.node.kind === "tool" || found.evidence?.record.kind === "call")
|
|
1873
|
+
? await ExecutionContent.value(await contentSource(sessionID, nodeID(found.node.sessionID, "call", call.record.value.id), "request", runID, undefined, signal), ["tools"], signal)
|
|
1874
|
+
: undefined;
|
|
1875
|
+
const toolDefinitions = found.node.kind === "tool" && Array.isArray(definitions)
|
|
1876
|
+
? definitions.filter((definition) => definition &&
|
|
1877
|
+
typeof definition === "object" &&
|
|
1878
|
+
(("id" in definition && definition.id === found.node.tool) ||
|
|
1879
|
+
("name" in definition && definition.name === found.node.tool) ||
|
|
1880
|
+
("function" in definition &&
|
|
1881
|
+
definition.function &&
|
|
1882
|
+
typeof definition.function === "object" &&
|
|
1883
|
+
"name" in definition.function &&
|
|
1884
|
+
definition.function.name === found.node.tool)))
|
|
1885
|
+
: definitions;
|
|
1886
|
+
const process = ExecutionProcess.project(found.value.nodes, "process", found.value.root.id);
|
|
1887
|
+
const projected = process.find((node) => node.id === id) ??
|
|
1888
|
+
ExecutionProcess.project(found.value.nodes, "records", found.value.root.id).find((node) => node.id === id) ??
|
|
1889
|
+
found.node;
|
|
1890
|
+
const grouped = (projected.group?.callCount ?? 0) > 1 ? ExecutionProcess.auxiliaryMembers(found.value.nodes, projected) : [];
|
|
1891
|
+
return ExecutionSchema.Detail.parse({
|
|
1892
|
+
node: projected,
|
|
1893
|
+
execution: ["turn", "subtask"].includes(found.node.kind)
|
|
1894
|
+
? await summary(found.node.sessionID, found.node.kind === "subtask" ? undefined : found.node.runID, signal)
|
|
1895
|
+
: undefined,
|
|
1896
|
+
record: found.evidence?.record.value ?? found.message ?? null,
|
|
1897
|
+
sources: found.evidence ? RolloutEvidence.sources(found.evidence.record) : [],
|
|
1898
|
+
definitions: toolDefinitions ?? null,
|
|
1899
|
+
related: [
|
|
1900
|
+
...new Map([
|
|
1901
|
+
...grouped,
|
|
1902
|
+
...found.value.nodes.filter((node) => node.parentID === id || node.id === found.node.parentID),
|
|
1903
|
+
].map((node) => [node.id, node])).values(),
|
|
1904
|
+
].slice(0, 500),
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
ExecutionService.node = node;
|
|
1908
|
+
async function content(sessionID, id, field, offset, limit, runID, version, signal) {
|
|
1909
|
+
const source = await contentSource(sessionID, id, field, runID, version, signal);
|
|
1910
|
+
checkpoint(signal);
|
|
1911
|
+
return source.read(offset, limit);
|
|
1912
|
+
}
|
|
1913
|
+
ExecutionService.content = content;
|
|
1914
|
+
async function contentSource(sessionID, id, field, runID, version, signal) {
|
|
1915
|
+
const identity = z.tuple([z.string(), z.string(), z.string()]).safeParse((() => {
|
|
1916
|
+
try {
|
|
1917
|
+
return JSON.parse(Buffer.from(id, "base64url").toString());
|
|
1918
|
+
}
|
|
1919
|
+
catch {
|
|
1920
|
+
return null;
|
|
1921
|
+
}
|
|
1922
|
+
})());
|
|
1923
|
+
const direct = identity.success && identity.data[0] === sessionID && identity.data[1] === "call";
|
|
1924
|
+
const found = direct
|
|
1925
|
+
? await (async () => {
|
|
1926
|
+
const value = await contextData(sessionID, signal);
|
|
1927
|
+
const snapshot = value.snapshots[0];
|
|
1928
|
+
const call = snapshot.calls.find((call) => call.id === identity.data[2] && (!runID || call.runID === runID));
|
|
1929
|
+
if (!call)
|
|
1930
|
+
throw new Storage.NotFoundError({ message: "Execution call was not found in this session" });
|
|
1931
|
+
return {
|
|
1932
|
+
evidence: { owner: snapshot.owner, record: { kind: "call", value: call } },
|
|
1933
|
+
message: undefined,
|
|
1934
|
+
};
|
|
1935
|
+
})()
|
|
1936
|
+
: await find(sessionID, id, runID, signal);
|
|
1937
|
+
if (found.evidence) {
|
|
1938
|
+
const { owner, record } = found.evidence;
|
|
1939
|
+
const { ref, contentVersion } = await RolloutEvidence.source(owner, record, field, version);
|
|
1940
|
+
return {
|
|
1941
|
+
...ref,
|
|
1942
|
+
contentVersion,
|
|
1943
|
+
read: (offset, limit) => RolloutEvidence.content(owner, record, field, offset, limit, contentVersion),
|
|
1944
|
+
stream: async function* () {
|
|
1945
|
+
yield* (await RolloutEvidence.stream(owner, record, field, contentVersion)).chunks;
|
|
1946
|
+
},
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
if (field !== "message")
|
|
1950
|
+
throw new Storage.NotFoundError({ message: "Message content field was not found" });
|
|
1951
|
+
const message = found.message && (await MessageV2.get({ sessionID: found.message.sessionID, messageID: found.message.messageID }));
|
|
1952
|
+
const part = message?.parts.find((value) => value.id === found.message.partID);
|
|
1953
|
+
const body = part && "text" in part && typeof part.text === "string" ? part.text : null;
|
|
1954
|
+
const text = body ?? JSON.stringify(message && { info: message.info, part }, null, 2);
|
|
1955
|
+
if (text === undefined)
|
|
1956
|
+
throw new Storage.NotFoundError({ message: "Message content was not found" });
|
|
1957
|
+
const mediaType = body === null ? "application/json" : "text/plain";
|
|
1958
|
+
const bytes = Buffer.from(text);
|
|
1959
|
+
const sha256 = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
1960
|
+
const contentVersion = "message:" + id + ":" + sha256;
|
|
1961
|
+
if (version && version !== contentVersion)
|
|
1962
|
+
throw new ExecutionQueryError("Execution message version changed");
|
|
1963
|
+
return {
|
|
1964
|
+
mediaType,
|
|
1965
|
+
bytes: bytes.length,
|
|
1966
|
+
status: "complete",
|
|
1967
|
+
sha256,
|
|
1968
|
+
contentVersion,
|
|
1969
|
+
read: async (offset, limit) => {
|
|
1970
|
+
queryValue(z.number().int().nonnegative().safe(), offset);
|
|
1971
|
+
queryValue(z.number().int().min(1).max(65_536), limit);
|
|
1972
|
+
if (offset > bytes.length)
|
|
1973
|
+
throw new ExecutionQueryError("Content offset exceeds the recorded message");
|
|
1974
|
+
const end = offset + RolloutEvidence.textBoundary(bytes.subarray(offset, offset + limit + 4), limit);
|
|
1975
|
+
return ExecutionSchema.Content.parse({
|
|
1976
|
+
mediaType,
|
|
1977
|
+
text: bytes.subarray(offset, end).toString(),
|
|
1978
|
+
offset,
|
|
1979
|
+
nextOffset: end < bytes.length ? end : null,
|
|
1980
|
+
bytes: bytes.length,
|
|
1981
|
+
status: "complete",
|
|
1982
|
+
contentVersion,
|
|
1983
|
+
sha256,
|
|
1984
|
+
});
|
|
1985
|
+
},
|
|
1986
|
+
stream: async function* () {
|
|
1987
|
+
for (let offset = 0; offset < bytes.length; offset += 65_536)
|
|
1988
|
+
yield bytes.subarray(offset, offset + 65_536);
|
|
1989
|
+
},
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
ExecutionService.contentSource = contentSource;
|
|
1993
|
+
})(ExecutionService || (ExecutionService = {}));
|