pi-loop-graph-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README-zh.md +416 -0
  3. package/README.md +414 -0
  4. package/ROADMAP.md +60 -0
  5. package/dist/adapter/complete-tool.d.ts +3 -0
  6. package/dist/adapter/complete-tool.js +53 -0
  7. package/dist/adapter/debug-log.d.ts +38 -0
  8. package/dist/adapter/debug-log.js +151 -0
  9. package/dist/adapter/extension.d.ts +2 -0
  10. package/dist/adapter/extension.js +11 -0
  11. package/dist/adapter/graph-execution-host.d.ts +73 -0
  12. package/dist/adapter/graph-execution-host.js +181 -0
  13. package/dist/adapter/isolated-graph-session.d.ts +75 -0
  14. package/dist/adapter/isolated-graph-session.js +313 -0
  15. package/dist/adapter/loop-graph-extension.d.ts +96 -0
  16. package/dist/adapter/loop-graph-extension.js +487 -0
  17. package/dist/adapter/mechanism-runtime.d.ts +97 -0
  18. package/dist/adapter/mechanism-runtime.js +670 -0
  19. package/dist/adapter/model-messages.d.ts +17 -0
  20. package/dist/adapter/model-messages.js +11 -0
  21. package/dist/adapter/observability.d.ts +88 -0
  22. package/dist/adapter/observability.js +31 -0
  23. package/dist/adapter/output-contract.d.ts +12 -0
  24. package/dist/adapter/output-contract.js +87 -0
  25. package/dist/adapter/pi-node-context.d.ts +132 -0
  26. package/dist/adapter/pi-node-context.js +619 -0
  27. package/dist/adapter/projection.d.ts +121 -0
  28. package/dist/adapter/projection.js +169 -0
  29. package/dist/adapter/skill-content.d.ts +16 -0
  30. package/dist/adapter/skill-content.js +16 -0
  31. package/dist/advanced.d.ts +32 -0
  32. package/dist/advanced.js +17 -0
  33. package/dist/builders/graph.d.ts +27 -0
  34. package/dist/builders/graph.js +39 -0
  35. package/dist/builders/node.d.ts +8 -0
  36. package/dist/builders/node.js +9 -0
  37. package/dist/builders/refs.d.ts +5 -0
  38. package/dist/builders/refs.js +10 -0
  39. package/dist/builders/route.d.ts +11 -0
  40. package/dist/builders/route.js +18 -0
  41. package/dist/core/context.d.ts +73 -0
  42. package/dist/core/context.js +229 -0
  43. package/dist/core/graph.d.ts +172 -0
  44. package/dist/core/graph.js +57 -0
  45. package/dist/core/json.d.ts +8 -0
  46. package/dist/core/json.js +46 -0
  47. package/dist/core/limits.d.ts +7 -0
  48. package/dist/core/limits.js +14 -0
  49. package/dist/core/mechanism.d.ts +88 -0
  50. package/dist/core/mechanism.js +5 -0
  51. package/dist/core/result.d.ts +41 -0
  52. package/dist/core/result.js +1 -0
  53. package/dist/core/schema.d.ts +8 -0
  54. package/dist/core/schema.js +23 -0
  55. package/dist/core/skill.d.ts +12 -0
  56. package/dist/core/skill.js +1 -0
  57. package/dist/host/baseline.d.ts +11 -0
  58. package/dist/host/baseline.js +4 -0
  59. package/dist/host/graph-catalog.d.ts +8 -0
  60. package/dist/host/graph-catalog.js +24 -0
  61. package/dist/host/graph-host.d.ts +53 -0
  62. package/dist/host/graph-host.js +181 -0
  63. package/dist/host/preflight.d.ts +17 -0
  64. package/dist/host/preflight.js +81 -0
  65. package/dist/host/skill-catalog.d.ts +24 -0
  66. package/dist/host/skill-catalog.js +92 -0
  67. package/dist/host/tool-catalog.d.ts +27 -0
  68. package/dist/host/tool-catalog.js +33 -0
  69. package/dist/index.d.ts +21 -0
  70. package/dist/index.js +10 -0
  71. package/dist/replay/checkpoint.d.ts +40 -0
  72. package/dist/replay/checkpoint.js +57 -0
  73. package/dist/replay/events.d.ts +40 -0
  74. package/dist/replay/events.js +1 -0
  75. package/dist/replay/finalizer.d.ts +26 -0
  76. package/dist/replay/finalizer.js +117 -0
  77. package/dist/replay/html.d.ts +3 -0
  78. package/dist/replay/html.js +270 -0
  79. package/dist/replay/index.d.ts +13 -0
  80. package/dist/replay/index.js +7 -0
  81. package/dist/replay/model.d.ts +81 -0
  82. package/dist/replay/model.js +1 -0
  83. package/dist/replay/parser.d.ts +3 -0
  84. package/dist/replay/parser.js +332 -0
  85. package/dist/replay/recorder.d.ts +30 -0
  86. package/dist/replay/recorder.js +195 -0
  87. package/dist/replay/store.d.ts +41 -0
  88. package/dist/replay/store.js +94 -0
  89. package/dist/router.d.ts +4 -0
  90. package/dist/router.js +61 -0
  91. package/dist/runtime/event-bus.d.ts +101 -0
  92. package/dist/runtime/event-bus.js +18 -0
  93. package/dist/runtime/graph-runtime.d.ts +173 -0
  94. package/dist/runtime/graph-runtime.js +1293 -0
  95. package/dist/runtime/invocation-budget.d.ts +22 -0
  96. package/dist/runtime/invocation-budget.js +52 -0
  97. package/dist/runtime/mechanism-runtime.d.ts +92 -0
  98. package/dist/runtime/mechanism-runtime.js +387 -0
  99. package/dist/runtime.d.ts +91 -0
  100. package/dist/runtime.js +258 -0
  101. package/dist/tools-resolve.d.ts +20 -0
  102. package/dist/tools-resolve.js +52 -0
  103. package/dist/type.d.ts +593 -0
  104. package/dist/type.js +30 -0
  105. package/dist/validate.d.ts +25 -0
  106. package/dist/validate.js +203 -0
  107. package/package.json +69 -0
@@ -0,0 +1,81 @@
1
+ import type { ReplayDocument } from "./finalizer.js";
2
+ import type { ReplayEventEnvelope } from "./events.js";
3
+ export interface ReplayInvocationModel {
4
+ readonly id: string;
5
+ readonly parentId?: string;
6
+ readonly graphId?: string;
7
+ readonly graphVersion?: string;
8
+ readonly boundary?: string;
9
+ readonly events: readonly ReplayEventEnvelope[];
10
+ readonly children: readonly ReplayInvocationModel[];
11
+ }
12
+ export interface ExtractedContextBlock {
13
+ readonly text: string;
14
+ }
15
+ export interface ExtractedContextSnapshot {
16
+ readonly agentRunId: string;
17
+ readonly nodeVisitId: string;
18
+ readonly timestamp: string;
19
+ readonly blocks: readonly ExtractedContextBlock[];
20
+ }
21
+ export interface ExtractedToolCall {
22
+ readonly toolCallId: string;
23
+ readonly sequence: number;
24
+ readonly toolName: string;
25
+ readonly args?: unknown;
26
+ readonly result?: unknown;
27
+ readonly isError?: boolean;
28
+ readonly timestamp: string;
29
+ }
30
+ export interface ExtractedTurn {
31
+ readonly turnIndex: number;
32
+ readonly startedSequence: number;
33
+ readonly provider?: string;
34
+ readonly model?: string;
35
+ readonly usage?: {
36
+ readonly inputTokens?: number;
37
+ readonly outputTokens?: number;
38
+ };
39
+ readonly durationMs?: number;
40
+ readonly assistantTexts: readonly string[];
41
+ readonly toolCalls: readonly ExtractedToolCall[];
42
+ }
43
+ export interface ExtractedCompletionAttempt {
44
+ readonly timestamp: string;
45
+ readonly schemaFingerprint?: string;
46
+ readonly outcome: "accepted" | "rejected" | "failed";
47
+ readonly reason?: string;
48
+ readonly validatorStage?: string;
49
+ readonly durationMs?: number;
50
+ readonly validationStages: readonly string[];
51
+ }
52
+ export interface ExtractedAgentRun {
53
+ readonly agentRunId: string;
54
+ readonly nodeVisitId: string;
55
+ readonly stageId: string;
56
+ readonly graphInvocationId: string;
57
+ readonly contextSnapshot?: ExtractedContextSnapshot;
58
+ readonly turns: readonly ExtractedTurn[];
59
+ readonly completions: readonly ExtractedCompletionAttempt[];
60
+ }
61
+ export interface ExtractedNodeVisit {
62
+ readonly nodeVisitId: string;
63
+ readonly stageId: string;
64
+ readonly enteredAt: string;
65
+ readonly exitedAt?: string;
66
+ readonly agentRuns: readonly ExtractedAgentRun[];
67
+ }
68
+ export interface ReplayModel {
69
+ readonly schemaVersion: 1;
70
+ readonly rootRunId: string;
71
+ readonly mode: ReplayDocument["mode"];
72
+ readonly createdAt: string;
73
+ readonly recording: ReplayDocument["recording"];
74
+ readonly result: ReplayDocument["result"];
75
+ readonly totalCost?: number;
76
+ readonly summary: Readonly<Record<string, number>>;
77
+ readonly invocations: readonly ReplayInvocationModel[];
78
+ readonly unscopedEvents: readonly ReplayEventEnvelope[];
79
+ readonly nodes: readonly ExtractedNodeVisit[];
80
+ readonly contextSnapshots: readonly ExtractedContextSnapshot[];
81
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { ReplayDocument } from "./finalizer.js";
2
+ import type { ReplayModel } from "./model.js";
3
+ export declare function parseReplay(input: string | ReplayDocument): ReplayModel;
@@ -0,0 +1,332 @@
1
+ export function parseReplay(input) {
2
+ const document = typeof input === "string" ? JSON.parse(input) : input;
3
+ if (!document || document.schemaVersion !== 1 || !Array.isArray(document.events))
4
+ throw new TypeError("Unsupported replay document");
5
+ // ── Bucket events by graphInvocationId ──
6
+ const buckets = new Map();
7
+ const unscoped = [];
8
+ const summary = {};
9
+ for (const envelope of document.events) {
10
+ summary[envelope.event.domain] = (summary[envelope.event.domain] ?? 0) + 1;
11
+ if (!envelope.graphInvocationId) {
12
+ unscoped.push(envelope);
13
+ continue;
14
+ }
15
+ const bucket = buckets.get(envelope.graphInvocationId) ?? { events: [] };
16
+ bucket.events.push(envelope);
17
+ if (envelope.event.type === "graph_entered" && isObject(envelope.event.data)) {
18
+ bucket.parentId = stringValue(envelope.event.data.parentGraphInvocationId);
19
+ bucket.graphId = stringValue(envelope.event.data.graphId);
20
+ bucket.graphVersion = stringValue(envelope.event.data.graphVersion);
21
+ bucket.boundary = stringValue(envelope.event.data.boundary);
22
+ }
23
+ buckets.set(envelope.graphInvocationId, bucket);
24
+ }
25
+ // ── Build invocation tree ──
26
+ const build = (id, stack = new Set()) => {
27
+ if (stack.has(id))
28
+ throw new TypeError("Replay invocation cycle");
29
+ const bucket = buckets.get(id);
30
+ const next = new Set(stack);
31
+ next.add(id);
32
+ const children = [...buckets].filter(([, v]) => v.parentId === id).map(([child]) => build(child, next));
33
+ return Object.freeze({ id, ...bucket, events: Object.freeze(bucket.events), children: Object.freeze(children) });
34
+ };
35
+ for (const id of buckets.keys())
36
+ build(id);
37
+ const roots = [...buckets].filter(([, v]) => !v.parentId || !buckets.has(v.parentId)).map(([id]) => build(id));
38
+ if (buckets.size > 0 && roots.length === 0)
39
+ throw new TypeError("Replay invocation cycle");
40
+ // ── Structured extraction ──
41
+ const allEvents = document.events;
42
+ // Collect context snapshots
43
+ const contextSnapshots = [];
44
+ for (const ev of allEvents) {
45
+ if (ev.event.type !== "context_snapshot_projected")
46
+ continue;
47
+ const data = isObject(ev.event.data) ? ev.event.data : null;
48
+ if (!data)
49
+ continue;
50
+ const message = data;
51
+ const blocks = [];
52
+ const content = message.content;
53
+ if (Array.isArray(content)) {
54
+ for (const block of content) {
55
+ if (isObject(block) && block.type === "text" && typeof block.text === "string") {
56
+ blocks.push(Object.freeze({ text: block.text }));
57
+ }
58
+ }
59
+ }
60
+ if (blocks.length > 0) {
61
+ contextSnapshots.push(Object.freeze({
62
+ agentRunId: stringValue(ev.agentRunId) ?? "",
63
+ nodeVisitId: stringValue(ev.nodeVisitId) ?? "",
64
+ timestamp: ev.timestamp,
65
+ blocks: Object.freeze(blocks),
66
+ }));
67
+ }
68
+ }
69
+ // Collect node visits
70
+ const nodeVisits = new Map();
71
+ for (const ev of allEvents) {
72
+ if (ev.event.type !== "node_entered")
73
+ continue;
74
+ const nvId = stringValue(ev.nodeVisitId);
75
+ if (!nvId)
76
+ continue;
77
+ if (!nodeVisits.has(nvId)) {
78
+ const data = isObject(ev.event.data) ? ev.event.data : null;
79
+ nodeVisits.set(nvId, {
80
+ nodeVisitId: nvId,
81
+ stageId: stringValue(data?.stageId) ?? "",
82
+ enteredAt: ev.timestamp,
83
+ agentRuns: [],
84
+ });
85
+ }
86
+ }
87
+ for (const ev of allEvents) {
88
+ if (ev.event.type !== "node_exited")
89
+ continue;
90
+ const nvId = stringValue(ev.nodeVisitId);
91
+ if (!nvId)
92
+ continue;
93
+ const nv = nodeVisits.get(nvId);
94
+ if (nv)
95
+ nodeVisits.set(nvId, { ...nv, exitedAt: ev.timestamp });
96
+ }
97
+ // Collect agent runs
98
+ const agentRuns = new Map();
99
+ for (const ev of allEvents) {
100
+ if (ev.event.type !== "agent_started")
101
+ continue;
102
+ const arId = stringValue(ev.agentRunId);
103
+ if (!arId)
104
+ continue;
105
+ const data = isObject(ev.event.data) ? ev.event.data : null;
106
+ const nvId = stringValue(ev.nodeVisitId) ?? "";
107
+ if (!agentRuns.has(arId)) {
108
+ // Determine stageId from enclosing node
109
+ const nv = nodeVisits.get(nvId);
110
+ agentRuns.set(arId, {
111
+ agentRunId: arId,
112
+ nodeVisitId: nvId,
113
+ stageId: nv?.stageId ?? "",
114
+ graphInvocationId: stringValue(ev.graphInvocationId) ?? "",
115
+ turns: [],
116
+ completions: [],
117
+ });
118
+ }
119
+ }
120
+ // Attach context snapshots to agent runs
121
+ for (const cs of contextSnapshots) {
122
+ const ar = agentRuns.get(cs.agentRunId);
123
+ if (ar)
124
+ agentRuns.set(ar.agentRunId, { ...ar, contextSnapshot: cs });
125
+ }
126
+ // Collect turns
127
+ const turns = new Map(); // keyed by agentRunId
128
+ let currentArId = null;
129
+ let currentTurnIdx = -1;
130
+ for (const ev of allEvents) {
131
+ if (ev.event.type === "agent_started") {
132
+ currentArId = stringValue(ev.agentRunId) ?? null;
133
+ currentTurnIdx = -1;
134
+ continue;
135
+ }
136
+ if (ev.event.type === "model_turn_started") {
137
+ const data = isObject(ev.event.data) ? ev.event.data : null;
138
+ currentTurnIdx = typeof data?.turn === "number" ? data.turn : currentTurnIdx + 1;
139
+ if (!currentArId)
140
+ continue;
141
+ const list = turns.get(currentArId) ?? [];
142
+ list.push({
143
+ turnIndex: currentTurnIdx,
144
+ startedSequence: ev.sequence,
145
+ assistantTexts: [],
146
+ toolCalls: [],
147
+ });
148
+ turns.set(currentArId, list);
149
+ continue;
150
+ }
151
+ if (ev.event.type === "model_turn_finished") {
152
+ const data = isObject(ev.event.data) ? ev.event.data : null;
153
+ if (!currentArId)
154
+ continue;
155
+ const list = turns.get(currentArId) ?? [];
156
+ const last = list[list.length - 1];
157
+ if (!last)
158
+ continue;
159
+ const message = data?.message;
160
+ if (isObject(message)) {
161
+ const provider = stringValue(data.provider);
162
+ const model = stringValue(data.model);
163
+ const usage = isObject(data.usage)
164
+ ? Object.freeze({ inputTokens: numberValue(data.usage.inputTokens), outputTokens: numberValue(data.usage.outputTokens) })
165
+ : undefined;
166
+ const durationMs = typeof data.durationMs === "number" ? data.durationMs : undefined;
167
+ const texts = [];
168
+ const content = message.content;
169
+ if (Array.isArray(content)) {
170
+ for (const block of content) {
171
+ if (isObject(block) && block.type === "text" && typeof block.text === "string") {
172
+ texts.push(block.text);
173
+ }
174
+ }
175
+ }
176
+ list[list.length - 1] = Object.freeze({
177
+ ...last,
178
+ provider, model, usage, durationMs,
179
+ assistantTexts: Object.freeze(texts),
180
+ });
181
+ turns.set(currentArId, list);
182
+ }
183
+ }
184
+ }
185
+ // Collect tool calls (link by toolCallId)
186
+ const toolCallsById = new Map();
187
+ const toolCallsByArId = new Map();
188
+ for (const ev of allEvents) {
189
+ if (ev.event.type === "tool_execution_started" || ev.event.type === "tool_execution_finished") {
190
+ const tcId = stringValue(ev.toolCallId);
191
+ const arId = stringValue(ev.agentRunId);
192
+ const data = isObject(ev.event.data) ? ev.event.data : null;
193
+ if (!tcId)
194
+ continue;
195
+ const existing = toolCallsById.get(tcId);
196
+ if (ev.event.type === "tool_execution_started") {
197
+ const tc = {
198
+ toolCallId: tcId,
199
+ sequence: ev.sequence,
200
+ toolName: stringValue(data?.toolName) ?? "unknown",
201
+ args: data?.args,
202
+ timestamp: ev.timestamp,
203
+ };
204
+ toolCallsById.set(tcId, tc);
205
+ if (arId) {
206
+ const list = toolCallsByArId.get(arId) ?? [];
207
+ list.push(tc);
208
+ toolCallsByArId.set(arId, list);
209
+ }
210
+ }
211
+ else if (existing) {
212
+ const updated = Object.freeze({
213
+ ...existing,
214
+ result: data?.result,
215
+ isError: data?.isError === true,
216
+ });
217
+ toolCallsById.set(tcId, updated);
218
+ if (arId) {
219
+ const list = toolCallsByArId.get(arId) ?? [];
220
+ const idx = list.findIndex(t => t.toolCallId === tcId);
221
+ if (idx >= 0)
222
+ list[idx] = updated;
223
+ }
224
+ }
225
+ }
226
+ }
227
+ // Attach each tool call to the turn that was active when it started.
228
+ for (const [arId, arTurns] of turns) {
229
+ const tcList = toolCallsByArId.get(arId) ?? [];
230
+ if (tcList.length > 0 && arTurns.length > 0) {
231
+ const byTurn = arTurns.map((turn, index) => {
232
+ const nextStart = arTurns[index + 1]?.startedSequence ?? Number.POSITIVE_INFINITY;
233
+ const calls = tcList.filter((call) => call.sequence >= turn.startedSequence && call.sequence < nextStart);
234
+ return calls.length > 0 ? Object.freeze({ ...turn, toolCalls: Object.freeze(calls) }) : turn;
235
+ });
236
+ turns.set(arId, byTurn);
237
+ continue;
238
+ }
239
+ }
240
+ // Collect completion attempts
241
+ const completionsByArId = new Map();
242
+ const activeCompletionByArId = new Map();
243
+ const completionStagesByArId = new Map();
244
+ for (const ev of allEvents) {
245
+ const arId = stringValue(ev.agentRunId);
246
+ if (!arId)
247
+ continue;
248
+ if (ev.event.type === "completion.submitted") {
249
+ const data = isObject(ev.event.data) ? ev.event.data : null;
250
+ const attempts = completionsByArId.get(arId) ?? [];
251
+ attempts.push({
252
+ timestamp: ev.timestamp,
253
+ schemaFingerprint: stringValue(data?.schemaFingerprint),
254
+ outcome: "accepted", // default, will be updated
255
+ validationStages: [],
256
+ });
257
+ completionsByArId.set(arId, attempts);
258
+ activeCompletionByArId.set(arId, attempts.length - 1);
259
+ completionStagesByArId.set(arId, []);
260
+ }
261
+ if (ev.event.type === "completion.validation_started") {
262
+ const data = isObject(ev.event.data) ? ev.event.data : null;
263
+ const stage = stringValue(data?.validatorStage);
264
+ if (stage) {
265
+ const stages = completionStagesByArId.get(arId) ?? [];
266
+ stages.push(stage);
267
+ completionStagesByArId.set(arId, stages);
268
+ }
269
+ }
270
+ if (ev.event.type === "completion.accepted" || ev.event.type === "completion.rejected" || ev.event.type === "completion.failed") {
271
+ const data = isObject(ev.event.data) ? ev.event.data : null;
272
+ const outcome = ev.event.type === "completion.accepted" ? "accepted"
273
+ : ev.event.type === "completion.failed" ? "failed" : "rejected";
274
+ const attempts = completionsByArId.get(arId) ?? [];
275
+ const activeIndex = activeCompletionByArId.get(arId);
276
+ const stages = [...(completionStagesByArId.get(arId) ?? [])];
277
+ const index = activeIndex ?? attempts.length;
278
+ const existing = attempts[index];
279
+ attempts[index] = Object.freeze({
280
+ ...(existing ?? { timestamp: ev.timestamp, validationStages: Object.freeze(stages) }),
281
+ outcome,
282
+ reason: stringValue(data?.reason),
283
+ validatorStage: stringValue(data?.validatorStage),
284
+ durationMs: typeof data?.durationMs === "number" ? data.durationMs : undefined,
285
+ validationStages: Object.freeze(stages),
286
+ });
287
+ completionsByArId.set(arId, attempts);
288
+ activeCompletionByArId.delete(arId);
289
+ completionStagesByArId.delete(arId);
290
+ }
291
+ }
292
+ // Attach turns and completions to agent runs
293
+ for (const [arId, ar] of agentRuns) {
294
+ const arTurns = Object.freeze(turns.get(arId)?.map(t => Object.freeze(t)) ?? []);
295
+ const completions = Object.freeze(completionsByArId.get(arId) ?? []);
296
+ agentRuns.set(arId, Object.freeze({ ...ar, turns: arTurns, completions }));
297
+ }
298
+ // Attach agent runs to node visits
299
+ for (const [, ar] of agentRuns) {
300
+ const nv = nodeVisits.get(ar.nodeVisitId);
301
+ if (nv) {
302
+ nodeVisits.set(ar.nodeVisitId, Object.freeze({
303
+ ...nv,
304
+ agentRuns: Object.freeze([...nv.agentRuns, ar]),
305
+ }));
306
+ }
307
+ }
308
+ const nodes = Object.freeze([...nodeVisits.values()].map(nv => Object.freeze({ ...nv, agentRuns: Object.freeze(nv.agentRuns) })));
309
+ return Object.freeze({
310
+ schemaVersion: 1,
311
+ rootRunId: document.rootRunId,
312
+ mode: document.mode,
313
+ createdAt: document.createdAt,
314
+ recording: document.recording,
315
+ result: document.result,
316
+ ...(document.totalCost === undefined ? {} : { totalCost: document.totalCost }),
317
+ summary: Object.freeze(summary),
318
+ invocations: Object.freeze(roots),
319
+ unscopedEvents: Object.freeze(unscoped),
320
+ nodes,
321
+ contextSnapshots: Object.freeze(contextSnapshots),
322
+ });
323
+ }
324
+ function isObject(value) {
325
+ return value !== null && typeof value === "object" && !Array.isArray(value);
326
+ }
327
+ function stringValue(value) {
328
+ return typeof value === "string" ? value : undefined;
329
+ }
330
+ function numberValue(value) {
331
+ return typeof value === "number" ? value : undefined;
332
+ }
@@ -0,0 +1,30 @@
1
+ import type { GraphRunResult, ReplayReference } from "../core/result.js";
2
+ import type { JsonValue } from "../core/json.js";
3
+ import type { RuntimeEventBus } from "../runtime/event-bus.js";
4
+ import { type PricingResolver, type RecordingMode, type ReplayEvent, type ReplayEventScope } from "./events.js";
5
+ import type { RunStore } from "./store.js";
6
+ export interface RecorderOptions {
7
+ readonly mode: Exclude<RecordingMode, "off">;
8
+ readonly store: RunStore;
9
+ readonly artifactThresholdBytes?: number;
10
+ readonly pricingResolver?: PricingResolver;
11
+ readonly now?: () => Date;
12
+ }
13
+ export declare class Recorder {
14
+ private readonly options;
15
+ private sequence;
16
+ private queue;
17
+ private readonly issues;
18
+ private rootRunId;
19
+ private unsubscribe?;
20
+ constructor(options: RecorderOptions);
21
+ attach(eventBus: RuntimeEventBus): void;
22
+ record(event: ReplayEvent, scope: ReplayEventScope): void;
23
+ finalize<T>(result: GraphRunResult<T>): Promise<{
24
+ readonly replay: ReplayReference;
25
+ readonly documentWritten: boolean;
26
+ }>;
27
+ private recordRuntimeEvent;
28
+ private persist;
29
+ }
30
+ export declare function toRecordedJson(value: unknown, mode: RecordingMode): JsonValue;
@@ -0,0 +1,195 @@
1
+ import { finalizeJournal } from "./finalizer.js";
2
+ import { REPLAY_SCHEMA_VERSION } from "./events.js";
3
+ export class Recorder {
4
+ options;
5
+ sequence = 0;
6
+ queue = Promise.resolve();
7
+ issues = [];
8
+ rootRunId;
9
+ unsubscribe;
10
+ constructor(options) {
11
+ this.options = options;
12
+ }
13
+ attach(eventBus) {
14
+ this.unsubscribe?.();
15
+ this.unsubscribe = eventBus.subscribe((event) => this.recordRuntimeEvent(event));
16
+ }
17
+ record(event, scope) {
18
+ this.rootRunId ??= scope.rootRunId;
19
+ const envelope = Object.freeze({
20
+ schemaVersion: REPLAY_SCHEMA_VERSION,
21
+ sequence: ++this.sequence,
22
+ timestamp: (this.options.now?.() ?? new Date()).toISOString(),
23
+ ...scope,
24
+ event: Object.freeze({
25
+ ...event,
26
+ ...(event.data === undefined ? {} : { data: toRecordedJson(event.data, this.options.mode) }),
27
+ }),
28
+ });
29
+ this.queue = this.queue.then(() => this.persist(envelope)).catch((error) => {
30
+ this.issues.push(errorMessage(error));
31
+ });
32
+ }
33
+ async finalize(result) {
34
+ if (this.rootRunId) {
35
+ this.record({ domain: "recording", type: "recording_finalizing" }, { rootRunId: this.rootRunId });
36
+ }
37
+ this.unsubscribe?.();
38
+ this.unsubscribe = undefined;
39
+ await this.queue;
40
+ const runId = this.rootRunId ?? result.rootRunId;
41
+ try {
42
+ const recordedResult = toRecordedJson(result, this.options.mode);
43
+ const serializedResult = JSON.stringify(recordedResult);
44
+ const threshold = this.options.artifactThresholdBytes ?? 64 * 1024;
45
+ const persistedResult = Buffer.byteLength(serializedResult, "utf8") > threshold
46
+ ? await this.options.store.writeArtifact(runId, "final-result.json", serializedResult)
47
+ : recordedResult;
48
+ const document = await finalizeJournal({
49
+ store: this.options.store,
50
+ runId,
51
+ mode: this.options.mode,
52
+ result: persistedResult,
53
+ pricingResolver: this.options.pricingResolver,
54
+ initialIssues: this.issues,
55
+ });
56
+ const status = document.recording.status;
57
+ const replay = Object.freeze({
58
+ mode: this.options.mode,
59
+ status,
60
+ location: this.options.store.location(runId),
61
+ issues: document.recording.issues,
62
+ });
63
+ const replayResult = isArtifactReference(persistedResult)
64
+ ? persistedResult
65
+ : { ...recordedResult, replay };
66
+ await this.options.store.writeReplay(runId, `${JSON.stringify({ ...document, result: replayResult }, null, 2)}\n`);
67
+ return {
68
+ replay,
69
+ documentWritten: true,
70
+ };
71
+ }
72
+ catch (error) {
73
+ const issues = Object.freeze([...this.issues, errorMessage(error)]);
74
+ return {
75
+ replay: Object.freeze({
76
+ mode: this.options.mode,
77
+ status: "failed",
78
+ location: this.options.store.location(runId),
79
+ issues,
80
+ }),
81
+ documentWritten: false,
82
+ };
83
+ }
84
+ }
85
+ recordRuntimeEvent(event) {
86
+ const { rootRunId, graphInvocationId, nodeVisitId, agentRunId, ...data } = event;
87
+ this.record({ domain: runtimeDomain(event.type), type: event.type, data: toRecordedJson(data, this.options.mode) }, {
88
+ rootRunId,
89
+ graphInvocationId,
90
+ nodeVisitId,
91
+ agentRunId,
92
+ });
93
+ if (event.type === "root_started" && this.options.mode === "forensic") {
94
+ this.record({
95
+ domain: "recording",
96
+ type: "forensic_recording_warning",
97
+ data: { message: "Forensic recording may contain unredacted sensitive data and hidden reasoning." },
98
+ }, { rootRunId });
99
+ }
100
+ }
101
+ async persist(envelope) {
102
+ const threshold = this.options.artifactThresholdBytes ?? 64 * 1024;
103
+ const serializedData = envelope.event.data === undefined ? undefined : JSON.stringify(envelope.event.data);
104
+ let persisted = envelope;
105
+ if (serializedData && Buffer.byteLength(serializedData, "utf8") > threshold) {
106
+ const artifactId = `${String(envelope.sequence).padStart(8, "0")}-${safeType(envelope.event.type)}.json`;
107
+ const artifact = await this.options.store.writeArtifact(envelope.rootRunId, artifactId, serializedData);
108
+ persisted = Object.freeze({ ...envelope, event: Object.freeze({ ...envelope.event, data: artifact }) });
109
+ }
110
+ await this.options.store.appendJournal(envelope.rootRunId, JSON.stringify(persisted));
111
+ }
112
+ }
113
+ export function toRecordedJson(value, mode) {
114
+ return sanitize(value, mode, new WeakSet());
115
+ }
116
+ function sanitize(value, mode, ancestors, key = "") {
117
+ if (mode !== "forensic" && sensitiveKey(key))
118
+ return "[REDACTED]";
119
+ if (mode !== "forensic" && key === "thinking")
120
+ return "[REDACTED]";
121
+ if (mode === "events" && verboseKey(key))
122
+ return "[OMITTED]";
123
+ if (value === null || typeof value === "boolean")
124
+ return value;
125
+ if (typeof value === "string")
126
+ return mode === "forensic" ? value : redactString(value);
127
+ if (typeof value === "number")
128
+ return Number.isFinite(value) ? value : String(value);
129
+ if (typeof value === "bigint" || typeof value === "symbol" || typeof value === "function" || value === undefined)
130
+ return String(value);
131
+ if (ancestors.has(value))
132
+ return "[CIRCULAR]";
133
+ if (mode !== "forensic" && isThinkingBlock(value)) {
134
+ return { type: "thinking", thinking: "[REDACTED]" };
135
+ }
136
+ ancestors.add(value);
137
+ try {
138
+ if (Array.isArray(value))
139
+ return value.map((entry) => sanitize(entry, mode, ancestors));
140
+ const output = {};
141
+ for (const [childKey, child] of Object.entries(value)) {
142
+ if (mode !== "forensic" && (childKey === "reasoning" || childKey === "thinking"))
143
+ continue;
144
+ output[childKey] = sanitize(child, mode, ancestors, childKey);
145
+ }
146
+ return output;
147
+ }
148
+ catch {
149
+ return String(value);
150
+ }
151
+ finally {
152
+ ancestors.delete(value);
153
+ }
154
+ }
155
+ function isThinkingBlock(value) {
156
+ return value !== null && typeof value === "object" && !Array.isArray(value)
157
+ && value.type === "thinking";
158
+ }
159
+ function sensitiveKey(key) {
160
+ return /^(authorization|api[-_]?key|token|password|secret|cookie|set-cookie)$/i.test(key);
161
+ }
162
+ function verboseKey(key) {
163
+ return /^(content|input|output|result|payload|reasoning|toolResults)$/i.test(key);
164
+ }
165
+ function redactString(value) {
166
+ return value
167
+ .replace(/(Bearer\s+)[A-Za-z0-9._~+\/-]+=*/gi, "$1[REDACTED]")
168
+ .replace(/\bsk-[A-Za-z0-9_-]{10,}\b/g, "[REDACTED]");
169
+ }
170
+ function runtimeDomain(type) {
171
+ if (type.startsWith("root_") || type === "host_baseline_selected")
172
+ return "root";
173
+ if (type.startsWith("graph_"))
174
+ return "graph";
175
+ if (type.startsWith("node_"))
176
+ return "node";
177
+ if (type.startsWith("agent_"))
178
+ return "agent";
179
+ if (type.startsWith("context_"))
180
+ return "context";
181
+ if (type.startsWith("mechanism_") || type === "runtime_warning")
182
+ return "mechanism";
183
+ if (type.startsWith("transition_"))
184
+ return "transition";
185
+ return "recording";
186
+ }
187
+ function safeType(type) {
188
+ return type.replace(/[^A-Za-z0-9._-]/g, "_");
189
+ }
190
+ function errorMessage(error) {
191
+ return error instanceof Error ? error.message : String(error);
192
+ }
193
+ function isArtifactReference(value) {
194
+ return value !== null && typeof value === "object" && "artifactId" in value && "sha256" in value;
195
+ }
@@ -0,0 +1,41 @@
1
+ import type { ReplayArtifactRef } from "./events.js";
2
+ export interface JournalStore {
3
+ appendJournal(runId: string, line: string): Promise<void>;
4
+ readJournal(runId: string): Promise<string>;
5
+ }
6
+ export interface ArtifactStore {
7
+ writeArtifact(runId: string, artifactId: string, content: string, mediaType?: string): Promise<ReplayArtifactRef>;
8
+ readArtifact(runId: string, artifactId: string): Promise<string>;
9
+ }
10
+ export interface CheckpointStore {
11
+ writeCheckpoint(runId: string, checkpointId: string, content: string): Promise<void>;
12
+ readCheckpoint(runId: string, checkpointId: string): Promise<string>;
13
+ listCheckpoints?(runId: string): Promise<readonly string[]>;
14
+ pruneCheckpoints?(runId: string, keep: readonly string[]): Promise<void>;
15
+ deleteCheckpoint?(runId: string, checkpointId: string): Promise<void>;
16
+ }
17
+ export interface RunStore extends JournalStore, ArtifactStore, CheckpointStore {
18
+ writeReplay(runId: string, content: string): Promise<void>;
19
+ readReplay(runId: string): Promise<string>;
20
+ location(runId: string): string | undefined;
21
+ }
22
+ export interface FileRunStoreOptions {
23
+ readonly rootDir?: string;
24
+ }
25
+ export declare class FileRunStore implements RunStore {
26
+ readonly rootDir: string;
27
+ constructor(options?: FileRunStoreOptions | string);
28
+ location(runId: string): string;
29
+ appendJournal(runId: string, line: string): Promise<void>;
30
+ readJournal(runId: string): Promise<string>;
31
+ writeArtifact(runId: string, artifactId: string, content: string, mediaType?: string): Promise<ReplayArtifactRef>;
32
+ readArtifact(runId: string, artifactId: string): Promise<string>;
33
+ writeCheckpoint(runId: string, checkpointId: string, content: string): Promise<void>;
34
+ readCheckpoint(runId: string, checkpointId: string): Promise<string>;
35
+ listCheckpoints(runId: string): Promise<readonly string[]>;
36
+ pruneCheckpoints(runId: string, keep: readonly string[]): Promise<void>;
37
+ deleteCheckpoint(runId: string, checkpointId: string): Promise<void>;
38
+ writeReplay(runId: string, content: string): Promise<void>;
39
+ readReplay(runId: string): Promise<string>;
40
+ private runDir;
41
+ }