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,40 @@
1
+ import type { JsonValue } from "../core/json.js";
2
+ export declare const CHECKPOINT_SCHEMA_VERSION: 1;
3
+ export interface CheckpointNodeBoundary {
4
+ readonly kind: "node-boundary";
5
+ readonly schemaVersion: typeof CHECKPOINT_SCHEMA_VERSION;
6
+ readonly checkpointId: string;
7
+ /** Monotonic wall-clock ordering metadata; checkpoint ids are opaque. */
8
+ readonly createdAt?: string;
9
+ readonly rootRunId: string;
10
+ readonly graph: {
11
+ readonly id: string;
12
+ readonly version: string;
13
+ };
14
+ readonly invocationStack: readonly {
15
+ readonly graphInvocationId: string;
16
+ readonly parentGraphInvocationId?: string;
17
+ readonly boundary: "root" | "call" | "compose" | "delegate";
18
+ readonly depth: number;
19
+ readonly graph?: {
20
+ readonly id: string;
21
+ readonly version: string;
22
+ };
23
+ }[];
24
+ readonly next: {
25
+ readonly stageId: string;
26
+ readonly nodeInput: JsonValue;
27
+ /** Stable identity for repeated attempts to resume this pending Node Visit. */
28
+ readonly nodeVisitId?: string;
29
+ };
30
+ readonly frames: readonly JsonValue[];
31
+ readonly budget: JsonValue;
32
+ readonly resumeAttempt: number;
33
+ readonly mechanisms: readonly {
34
+ readonly name: string;
35
+ readonly snapshot: JsonValue;
36
+ }[];
37
+ }
38
+ export type CheckpointDocument = CheckpointNodeBoundary;
39
+ export declare function encodeCheckpoint(checkpoint: CheckpointDocument): string;
40
+ export declare function decodeCheckpoint(content: string): CheckpointDocument;
@@ -0,0 +1,57 @@
1
+ export const CHECKPOINT_SCHEMA_VERSION = 1;
2
+ export function encodeCheckpoint(checkpoint) {
3
+ assertCheckpoint(checkpoint);
4
+ return JSON.stringify(checkpoint);
5
+ }
6
+ export function decodeCheckpoint(content) {
7
+ let value;
8
+ try {
9
+ value = JSON.parse(content);
10
+ }
11
+ catch (error) {
12
+ throw new Error(`Invalid checkpoint JSON: ${error instanceof Error ? error.message : String(error)}`);
13
+ }
14
+ assertCheckpoint(value);
15
+ return value;
16
+ }
17
+ function assertCheckpoint(value) {
18
+ if (!isRecord(value) || value.schemaVersion !== CHECKPOINT_SCHEMA_VERSION || value.kind !== "node-boundary") {
19
+ throw new Error(`Unsupported checkpoint schema: expected node-boundary v${CHECKPOINT_SCHEMA_VERSION}`);
20
+ }
21
+ if (!isNonEmpty(value.checkpointId) || !isNonEmpty(value.rootRunId) || !isNonEmpty(value.graph?.id) || !isNonEmpty(value.graph?.version)) {
22
+ throw new Error("Checkpoint identity is incomplete");
23
+ }
24
+ if (!Array.isArray(value.invocationStack) || value.invocationStack.length === 0 || !Array.isArray(value.frames) || !Array.isArray(value.mechanisms)) {
25
+ throw new Error("Checkpoint collections are invalid");
26
+ }
27
+ if (!isRecord(value.next) || !isNonEmpty(value.next.stageId) || !Number.isInteger(value.resumeAttempt) || value.resumeAttempt < 0) {
28
+ throw new Error("Checkpoint next boundary is invalid");
29
+ }
30
+ if (value.createdAt !== undefined && (typeof value.createdAt !== "string" || !Number.isFinite(Date.parse(value.createdAt)))) {
31
+ throw new Error("Checkpoint createdAt is invalid");
32
+ }
33
+ for (const entry of value.invocationStack) {
34
+ if (!isRecord(entry) || !isNonEmpty(entry.graphInvocationId) || !Number.isInteger(entry.depth) || entry.depth < 1
35
+ || !["root", "call", "compose", "delegate"].includes(entry.boundary))
36
+ throw new Error("Checkpoint invocation stack is invalid");
37
+ }
38
+ if (!isJson(value.next.nodeInput) || !value.frames.every(isJson) || !isJson(value.budget)
39
+ || !value.mechanisms.every((item) => isRecord(item) && isNonEmpty(item.name) && isJson(item.snapshot))) {
40
+ throw new Error("Checkpoint payload is not JSON-compatible");
41
+ }
42
+ }
43
+ function isRecord(value) {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ }
46
+ function isNonEmpty(value) {
47
+ return typeof value === "string" && value.length > 0;
48
+ }
49
+ function isJson(value) {
50
+ if (value === null || typeof value === "string" || typeof value === "boolean")
51
+ return true;
52
+ if (typeof value === "number")
53
+ return Number.isFinite(value);
54
+ if (Array.isArray(value))
55
+ return value.every(isJson);
56
+ return isRecord(value) && Object.values(value).every(isJson);
57
+ }
@@ -0,0 +1,40 @@
1
+ import type { JsonValue } from "../core/json.js";
2
+ export type { RecordingMode } from "../core/result.js";
3
+ export declare const REPLAY_SCHEMA_VERSION: 1;
4
+ export interface ReplayArtifactRef {
5
+ readonly artifactId: string;
6
+ readonly mediaType: string;
7
+ readonly byteSize: number;
8
+ readonly sha256: string;
9
+ }
10
+ export type ReplayEventDomain = "root" | "graph" | "node" | "agent" | "model" | "tool" | "completion" | "mechanism" | "context" | "compaction" | "transition" | "checkpoint" | "recording";
11
+ export interface ReplayEvent {
12
+ readonly domain: ReplayEventDomain;
13
+ readonly type: string;
14
+ readonly data?: JsonValue | ReplayArtifactRef;
15
+ }
16
+ export interface ReplayEventScope {
17
+ readonly rootRunId: string;
18
+ readonly graphInvocationId?: string;
19
+ readonly nodeVisitId?: string;
20
+ readonly agentRunId?: string;
21
+ readonly toolCallId?: string;
22
+ }
23
+ export interface ReplayEventEnvelope extends ReplayEventScope {
24
+ readonly schemaVersion: typeof REPLAY_SCHEMA_VERSION;
25
+ readonly sequence: number;
26
+ readonly timestamp: string;
27
+ readonly event: ReplayEvent;
28
+ }
29
+ export interface ModelUsage {
30
+ readonly inputTokens?: number;
31
+ readonly outputTokens?: number;
32
+ readonly cacheReadTokens?: number;
33
+ readonly cacheWriteTokens?: number;
34
+ }
35
+ export interface PricingInput {
36
+ readonly provider: string;
37
+ readonly model: string;
38
+ readonly usage: ModelUsage;
39
+ }
40
+ export type PricingResolver = (input: PricingInput) => number | null | Promise<number | null>;
@@ -0,0 +1 @@
1
+ export const REPLAY_SCHEMA_VERSION = 1;
@@ -0,0 +1,26 @@
1
+ import type { GraphRunResult } from "../core/result.js";
2
+ import type { ReplayArtifactRef, ReplayEventEnvelope, PricingResolver, RecordingMode } from "./events.js";
3
+ import type { RunStore } from "./store.js";
4
+ export interface ReplayRecordingSummary {
5
+ readonly status: "complete" | "incomplete" | "failed";
6
+ readonly issues: readonly string[];
7
+ }
8
+ export interface ReplayDocument {
9
+ readonly schemaVersion: 1;
10
+ readonly rootRunId: string;
11
+ readonly mode: Exclude<RecordingMode, "off">;
12
+ readonly createdAt: string;
13
+ readonly result: GraphRunResult | ReplayArtifactRef;
14
+ readonly events: readonly ReplayEventEnvelope[];
15
+ readonly recording: ReplayRecordingSummary;
16
+ readonly totalCost?: number;
17
+ }
18
+ export interface FinalizeJournalOptions {
19
+ readonly store: RunStore;
20
+ readonly runId: string;
21
+ readonly mode: Exclude<RecordingMode, "off">;
22
+ readonly result: GraphRunResult | ReplayArtifactRef;
23
+ readonly pricingResolver?: PricingResolver;
24
+ readonly initialIssues?: readonly string[];
25
+ }
26
+ export declare function finalizeJournal(options: FinalizeJournalOptions): Promise<ReplayDocument>;
@@ -0,0 +1,117 @@
1
+ import { createHash } from "node:crypto";
2
+ export async function finalizeJournal(options) {
3
+ const issues = [...(options.initialIssues ?? [])];
4
+ const events = [];
5
+ let journal = "";
6
+ try {
7
+ journal = await options.store.readJournal(options.runId);
8
+ }
9
+ catch (error) {
10
+ issues.push(`journal unavailable: ${errorMessage(error)}`);
11
+ }
12
+ const lines = journal.split(/\r?\n/);
13
+ for (let index = 0; index < lines.length; index += 1) {
14
+ const line = lines[index];
15
+ if (!line.trim())
16
+ continue;
17
+ try {
18
+ const parsed = JSON.parse(line);
19
+ if (!isEnvelope(parsed, options.runId))
20
+ throw new Error("invalid envelope");
21
+ events.push(parsed);
22
+ }
23
+ catch {
24
+ issues.push(`invalid journal line ${index + 1}`);
25
+ }
26
+ }
27
+ for (let index = 0; index < events.length; index += 1) {
28
+ if (events[index].sequence !== index + 1) {
29
+ issues.push(`journal sequence gap at event ${index + 1}`);
30
+ break;
31
+ }
32
+ }
33
+ for (const envelope of events) {
34
+ const data = envelope.event.data;
35
+ if (!isArtifactRef(data))
36
+ continue;
37
+ await verifyArtifact(options.store, options.runId, data, issues);
38
+ }
39
+ if (isArtifactRef(options.result))
40
+ await verifyArtifact(options.store, options.runId, options.result, issues);
41
+ const totalCost = await calculateCost(events, options.store, options.runId, options.pricingResolver, issues);
42
+ return Object.freeze({
43
+ schemaVersion: 1,
44
+ rootRunId: options.runId,
45
+ mode: options.mode,
46
+ createdAt: new Date().toISOString(),
47
+ result: options.result,
48
+ events: Object.freeze(events),
49
+ recording: Object.freeze({
50
+ status: issues.length === 0 ? "complete" : (events.length > 0 ? "incomplete" : "failed"),
51
+ issues: Object.freeze(issues),
52
+ }),
53
+ ...(totalCost == null ? {} : { totalCost }),
54
+ });
55
+ }
56
+ async function verifyArtifact(store, runId, artifact, issues) {
57
+ try {
58
+ const content = await store.readArtifact(runId, artifact.artifactId);
59
+ const digest = createHash("sha256").update(content).digest("hex");
60
+ if (digest !== artifact.sha256)
61
+ issues.push(`artifact ${artifact.artifactId} checksum mismatch`);
62
+ }
63
+ catch (error) {
64
+ issues.push(`artifact ${artifact.artifactId} unavailable: ${errorMessage(error)}`);
65
+ }
66
+ }
67
+ async function calculateCost(events, store, runId, resolver, issues) {
68
+ if (!resolver)
69
+ return undefined;
70
+ let total = 0;
71
+ let found = false;
72
+ for (const envelope of events) {
73
+ if (envelope.event.type !== "model_turn_finished")
74
+ continue;
75
+ let data = envelope.event.data;
76
+ if (isArtifactRef(data)) {
77
+ try {
78
+ data = JSON.parse(await store.readArtifact(runId, data.artifactId));
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ }
84
+ if (!isObject(data))
85
+ continue;
86
+ const { provider, model, usage } = data;
87
+ if (typeof provider !== "string" || typeof model !== "string" || !isObject(usage))
88
+ continue;
89
+ try {
90
+ const value = await resolver({ provider, model, usage });
91
+ if (value != null) {
92
+ total += value;
93
+ found = true;
94
+ }
95
+ }
96
+ catch (error) {
97
+ issues.push(`pricing resolver failed: ${errorMessage(error)}`);
98
+ }
99
+ }
100
+ return found ? total : undefined;
101
+ }
102
+ function isArtifactRef(value) {
103
+ return isObject(value) && typeof value.artifactId === "string" && typeof value.sha256 === "string";
104
+ }
105
+ function isEnvelope(value, runId) {
106
+ if (!isObject(value) || value.schemaVersion !== 1 || value.rootRunId !== runId)
107
+ return false;
108
+ if (!Number.isInteger(value.sequence) || value.sequence <= 0 || typeof value.timestamp !== "string")
109
+ return false;
110
+ return isObject(value.event) && typeof value.event.domain === "string" && typeof value.event.type === "string";
111
+ }
112
+ function isObject(value) {
113
+ return typeof value === "object" && value !== null && !Array.isArray(value);
114
+ }
115
+ function errorMessage(error) {
116
+ return error instanceof Error ? error.message : String(error);
117
+ }
@@ -0,0 +1,3 @@
1
+ import type { ReplayModel } from "./model.js";
2
+ export declare function exportReplayHtml(model: ReplayModel): string;
3
+ export declare function escapeHtml(value: string): string;
@@ -0,0 +1,270 @@
1
+ export function exportReplayHtml(model) {
2
+ const header = renderHeader(model);
3
+ const nodes = model.nodes.length > 0
4
+ ? `<section><h2>🧠 What the Model Saw &amp; Did</h2>${model.nodes.map(n => renderNodeVisit(n)).join("")}</section>`
5
+ : "";
6
+ const timeline = `<section><h2>📊 Timeline</h2>${renderTimeline(model)}</section>`;
7
+ const errors = renderErrors(model);
8
+ const raw = `<section><details><summary><h2 style="display:inline">📋 Raw Events (${model.summary ? Object.values(model.summary).reduce((a, b) => a + b, 0) : 0} total)</h2></summary>
9
+ <details><summary>Result</summary><pre>${escapeHtml(JSON.stringify(model.result, null, 2))}</pre></details>
10
+ ${renderInvocations(model.invocations)}${renderEvents("Root events", model.unscopedEvents)}</details></section>`;
11
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Loop Graph Replay · ${escapeHtml(model.rootRunId.substring(0, 8))}</title><style>${CSS}</style></head><body>${header}${nodes}${errors}${timeline}${raw}</body></html>`;
12
+ }
13
+ function renderHeader(model) {
14
+ const result = model.result;
15
+ const isObj = result && typeof result === "object" && !Array.isArray(result);
16
+ const raw = result;
17
+ const status = isObj && "status" in (raw ?? {}) ? String(raw.status) : "";
18
+ const statusClass = status === "completed" ? "ok" : status === "failed" ? "fail" : status === "cancelled" ? "cancel" : "";
19
+ const durationMs = isObj && "durationMs" in (raw ?? {}) ? Number(raw.durationMs) : 0;
20
+ const steps = isObj && "steps" in (raw ?? {}) ? Number(raw.steps) : 0;
21
+ const recordingStatus = model.recording.status;
22
+ const cost = model.totalCost != null ? `$${model.totalCost.toFixed(4)}` : "";
23
+ return `<header>
24
+ <div class="hb"><h1>Loop Graph Replay</h1><span class="badge ${statusClass}">${escapeHtml(status)}</span></div>
25
+ <div class="meta">
26
+ <span>Run <code>${escapeHtml(model.rootRunId.substring(0, 8))}</code></span>
27
+ <span>${formatMs(durationMs)}</span>
28
+ <span>${steps} steps</span>
29
+ ${cost ? `<span>${cost}</span>` : ""}
30
+ <span class="rec-${recordingStatus}">recording: ${escapeHtml(recordingStatus)}</span>
31
+ </div>
32
+ <div class="meta"><span>Mode: ${escapeHtml(model.mode)}</span><span>Created: ${escapeHtml(model.createdAt)}</span></div>
33
+ </header>`;
34
+ }
35
+ function renderNodeVisit(nv) {
36
+ const duration = nv.exitedAt ? ` · ${formatMs(Date.parse(nv.exitedAt) - Date.parse(nv.enteredAt))}` : "";
37
+ return `<div class="node-card">
38
+ <div class="node-head">
39
+ <span class="node-stage">${escapeHtml(nv.stageId || nv.nodeVisitId.substring(0, 8))}</span>
40
+ <span class="node-meta">entered ${formatTime(nv.enteredAt)}${duration}</span>
41
+ </div>
42
+ ${nv.agentRuns.map(ar => renderAgentRun(ar)).join("")}
43
+ </div>`;
44
+ }
45
+ function renderAgentRun(ar) {
46
+ const ctx = ar.contextSnapshot;
47
+ const lastCompletion = ar.completions.at(-1);
48
+ const completionBadge = lastCompletion
49
+ ? `<span class="badge ${lastCompletion.outcome === "accepted" ? "ok" : lastCompletion.outcome === "rejected" ? "warn" : "fail"}">${lastCompletion.outcome}</span>`
50
+ : "";
51
+ return `<div class="ar-card">
52
+ <div class="ar-head">
53
+ <span>🤖 Agent Run <code>${escapeHtml(ar.agentRunId.substring(0, 8))}</code></span>
54
+ ${completionBadge}
55
+ </div>
56
+ ${ctx ? renderContext(ctx) : ""}
57
+ ${ar.turns.map(t => renderTurn(t, ar.completions)).join("")}
58
+ ${ar.completions.map(renderCompletionVerdict).join("")}
59
+ </div>`;
60
+ }
61
+ function renderContext(ctx) {
62
+ return `<details class="ctx" open>
63
+ <summary>📥 Context — what the model saw</summary>
64
+ <div class="ctx-blocks">${ctx.blocks.map(b => `<pre class="ctx-block">${escapeHtml(b.text)}</pre>`).join("")}</div>
65
+ </details>`;
66
+ }
67
+ function renderTurn(turn, completions) {
68
+ const toolsHtml = turn.toolCalls.length > 0
69
+ ? `<div class="tools">${turn.toolCalls.map(tc => renderToolCall(tc, completions)).join("")}</div>`
70
+ : "";
71
+ const textsHtml = turn.assistantTexts.length > 0
72
+ ? turn.assistantTexts.map(t => `<div class="asstext">${escapeHtml(t)}</div>`).join("")
73
+ : "";
74
+ const meta = [
75
+ turn.provider ? `${turn.provider}/${turn.model}` : "",
76
+ turn.durationMs != null ? formatMs(turn.durationMs) : "",
77
+ turn.usage?.inputTokens != null ? `↑${turn.usage.inputTokens}` : "",
78
+ turn.usage?.outputTokens != null ? `↓${turn.usage.outputTokens}` : "",
79
+ ].filter(Boolean).join(" · ");
80
+ return `<div class="turn">
81
+ <div class="turn-head">Turn ${turn.turnIndex} ${meta ? `<span class="turn-meta">${meta}</span>` : ""}</div>
82
+ ${textsHtml}
83
+ ${toolsHtml}
84
+ </div>`;
85
+ }
86
+ function renderToolCall(tc, completions) {
87
+ const isCompletion = tc.toolName === "__graph_complete__";
88
+ const icon = tc.isError ? "❌" : isCompletion ? "✅" : "🔧";
89
+ const cls = tc.isError ? "tool-err" : isCompletion ? "tool-comp" : "";
90
+ const completion = isCompletion ? completions.find((item) => item.timestamp >= tc.timestamp) ?? completions.at(-1) : undefined;
91
+ const outcomeNote = isCompletion && completion
92
+ ? ` → <span class="badge ${completion.outcome === "accepted" ? "ok" : completion.outcome === "rejected" ? "warn" : "fail"}">${completion.outcome}</span>${completion.reason ? ` <span class="reason">${escapeHtml(completion.reason)}</span>` : ""}`
93
+ : "";
94
+ const argsSummary = tc.args ? summarizeValue(tc.args) : "";
95
+ const resultSummary = tc.result ? summarizeResult(tc.result) : "";
96
+ return `<details class="tool ${cls}">
97
+ <summary>${icon} <code>${escapeHtml(tc.toolName)}</code>${argsSummary ? `(${argsSummary})` : ""}${outcomeNote}</summary>
98
+ ${tc.args ? `<div class="tool-detail"><b>Args</b><pre>${escapeHtml(JSON.stringify(tc.args, null, 2))}</pre></div>` : ""}
99
+ ${tc.result ? `<div class="tool-detail"><b>Result</b><pre class="${tc.isError ? "err" : ""}">${escapeHtml(JSON.stringify(tc.result, null, 2))}</pre></div>` : ""}
100
+ </details>`;
101
+ }
102
+ function renderCompletionVerdict(completion) {
103
+ if (!completion)
104
+ return "";
105
+ const verdictIcon = completion.outcome === "accepted" ? "✅" : completion.outcome === "rejected" ? "🔄" : "❌";
106
+ const stages = completion.validationStages.length > 0
107
+ ? `<div class="val-chain">${completion.validationStages.map(s => `<span class="val-stage">→ ${escapeHtml(s)}</span>`).join("")}</div>`
108
+ : "";
109
+ return `<div class="completion-verdict ${completion.outcome}">
110
+ ${verdictIcon} Completion <b>${escapeHtml(completion.outcome)}</b>
111
+ ${completion.durationMs != null ? ` · ${formatMs(completion.durationMs)}` : ""}
112
+ ${stages}
113
+ </div>`;
114
+ }
115
+ function renderTimeline(model) {
116
+ const events = allEvents(model);
117
+ if (events.length === 0)
118
+ return "<p>No events recorded.</p>";
119
+ const items = events.map(ev => {
120
+ const icon = domainIcon(ev.event.domain);
121
+ const label = ev.event.type;
122
+ const ts = formatTime(ev.timestamp);
123
+ const scope = [ev.graphInvocationId, ev.nodeVisitId, ev.agentRunId]
124
+ .filter(Boolean)
125
+ .map(id => String(id).substring(0, 8))
126
+ .join("/");
127
+ const dataSummary = ev.event.data ? summarizeValue(ev.event.data, 100) : "";
128
+ return `<div class="tl-item">
129
+ <span class="tl-ts">${ts}</span>
130
+ <span class="tl-icon">${icon}</span>
131
+ <span class="tl-type">${escapeHtml(label)}</span>
132
+ ${scope ? `<span class="tl-scope">${escapeHtml(scope)}</span>` : ""}
133
+ ${dataSummary ? `<span class="tl-data">${escapeHtml(dataSummary)}</span>` : ""}
134
+ </div>`;
135
+ });
136
+ return `<div class="timeline">${items.join("")}</div>`;
137
+ }
138
+ function renderErrors(model) {
139
+ const issues = model.recording.issues ?? [];
140
+ if (issues.length === 0)
141
+ return "";
142
+ return `<section class="errors"><h2>⚠️ Recording Issues</h2><ul>${issues.map(i => `<li>${escapeHtml(i)}</li>`).join("")}</ul></section>`;
143
+ }
144
+ function renderInvocations(invocations) {
145
+ return invocations.map(inv => {
146
+ const label = `${inv.graphId ?? "Graph"} · ${inv.boundary ?? "unknown"}`;
147
+ return `<details><summary>📦 ${escapeHtml(label)} <code>${escapeHtml(inv.id.substring(0, 8))}</code></summary>${renderEvents("Events", inv.events)}${inv.children.length > 0 ? renderInvocations(inv.children) : ""}</details>`;
148
+ }).join("");
149
+ }
150
+ function renderEvents(title, events) {
151
+ if (events.length === 0)
152
+ return "";
153
+ return `<details><summary>${escapeHtml(title)} (${events.length})</summary><ol class="raw-events">${events.map(ev => `<li><b>${escapeHtml(ev.event.type)}</b> <small>${escapeHtml(ev.timestamp)}</small><pre>${escapeHtml(JSON.stringify(ev.event.data ?? {}, null, 2))}</pre></li>`).join("")}</ol></details>`;
154
+ }
155
+ // ── helpers ──
156
+ function allEvents(model) {
157
+ const collect = (invs) => invs.flatMap(inv => [...inv.events, ...collect(inv.children)]);
158
+ return [...model.unscopedEvents, ...collect(model.invocations)].sort((a, b) => a.sequence - b.sequence);
159
+ }
160
+ function domainIcon(d) {
161
+ const icons = { root: "🏠", graph: "📊", node: "📍", agent: "🤖", model: "💬", tool: "🔧", completion: "✅", mechanism: "⚙️", context: "📥", compaction: "🗜️", transition: "➡️", checkpoint: "💾", recording: "📝" };
162
+ return icons[d] ?? "•";
163
+ }
164
+ function summarizeValue(value, maxLen = 60) {
165
+ if (typeof value === "string")
166
+ return value.length > maxLen ? value.substring(0, maxLen) + "…" : value;
167
+ if (typeof value === "number" || typeof value === "boolean")
168
+ return String(value);
169
+ if (Array.isArray(value))
170
+ return `[${value.length} items]`;
171
+ if (value && typeof value === "object") {
172
+ const keys = Object.keys(value);
173
+ if (keys.length === 0)
174
+ return "{}";
175
+ const kv = keys.slice(0, 3).map(k => `${k}=${summarizeValue(value[k], 20)}`).join(", ");
176
+ return `{${kv}${keys.length > 3 ? ", …" : ""}}`;
177
+ }
178
+ return "";
179
+ }
180
+ function summarizeResult(value) {
181
+ if (value && typeof value === "object" && !Array.isArray(value)) {
182
+ const obj = value;
183
+ // Extract decision/reason fields for completion results
184
+ const parts = [];
185
+ if ("decision" in obj)
186
+ parts.push(`decision=${obj.decision}`);
187
+ if ("reason" in obj)
188
+ parts.push(`reason="${String(obj.reason).substring(0, 80)}"`);
189
+ if (parts.length > 0)
190
+ return parts.join(", ");
191
+ return `{${Object.keys(obj).length} keys}`;
192
+ }
193
+ return summarizeValue(value);
194
+ }
195
+ function formatTime(iso) {
196
+ try {
197
+ return new Date(iso).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", fractionalSecondDigits: 3 });
198
+ }
199
+ catch {
200
+ return iso;
201
+ }
202
+ }
203
+ function formatMs(ms) {
204
+ if (ms < 1000)
205
+ return `${ms}ms`;
206
+ if (ms < 60000)
207
+ return `${(ms / 1000).toFixed(1)}s`;
208
+ return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
209
+ }
210
+ export function escapeHtml(value) {
211
+ return value.replace(/[&<>"']/g, char => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
212
+ }
213
+ const CSS = `
214
+ body{font:14px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;max-width:1100px;margin:auto;padding:24px;color:#1a1a2e;background:#fafbfc}
215
+ header{border-bottom:2px solid #e2e8f0;padding-bottom:16px;margin-bottom:24px}
216
+ h1{font-size:1.5rem;margin:0}.hb{display:flex;align-items:center;gap:12px;margin-bottom:8px}
217
+ h2{font-size:1.15rem;margin:24px 0 12px;color:#334155}
218
+ .meta{display:flex;gap:20px;flex-wrap:wrap;font-size:.85rem;color:#64748b;margin-top:4px}
219
+ .meta code{background:#f1f5f9;padding:2px 6px;border-radius:4px;font-size:.8rem}
220
+ .badge{padding:2px 8px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase}
221
+ .badge.ok{background:#dcfce7;color:#166534}.badge.fail{background:#fee2e2;color:#991b1b}
222
+ .badge.warn{background:#fef3c7;color:#92400e}.badge.cancel{background:#f1f5f9;color:#475569}
223
+ .rec-complete{color:#166534}.rec-incomplete,.rec-failed{color:#991b1b}
224
+ .node-card{border:1px solid #e2e8f0;border-radius:10px;margin:12px 0;overflow:hidden}
225
+ .node-head{background:#f8fafc;padding:10px 16px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #e2e8f0}
226
+ .node-stage{font-weight:700;font-size:1.05rem}
227
+ .node-meta{font-size:.8rem;color:#64748b}
228
+ .ar-card{margin:0;padding:12px 16px;border-top:1px solid #f1f5f9}
229
+ .ar-head{display:flex;align-items:center;gap:10px;margin-bottom:8px;font-weight:600}
230
+ details.ctx{margin:8px 0;padding:8px 12px;background:#f0f9ff;border-radius:8px;border:1px solid #bae6fd}
231
+ details.ctx summary{color:#0369a1;cursor:pointer;font-weight:600}
232
+ .ctx-blocks{margin-top:8px}
233
+ .ctx-block{background:#fff;border:1px solid #e0f2fe;padding:10px;border-radius:6px;margin:6px 0;white-space:pre-wrap;font-size:.82rem;max-height:18rem;overflow:auto}
234
+ .turn{margin:8px 0 8px 20px;padding:8px 12px;background:#fff;border:1px solid #f1f5f9;border-radius:8px}
235
+ .turn-head{font-size:.85rem;color:#475569;margin-bottom:4px;font-weight:600}
236
+ .turn-meta{font-weight:400;font-size:.78rem;color:#94a3b8}
237
+ .asstext{margin:0 0 6px 0;font-size:.85rem;line-height:1.6;white-space:pre-wrap;color:#1e293b}
238
+ .tools{margin-top:6px}
239
+ details.tool{margin:4px 0;padding:4px 8px;border-radius:6px;font-size:.83rem}
240
+ details.tool summary{cursor:pointer}
241
+ details.tool-comp{background:#f0fdf4;border:1px solid #bbf7d0}
242
+ details.tool-err{background:#fef2f2;border:1px solid #fecaca}
243
+ details.tool summary code{background:#e2e8f0;padding:1px 5px;border-radius:3px;font-size:.8rem}
244
+ .tool-detail{margin-top:4px}.tool-detail pre{white-space:pre-wrap;max-height:16rem;overflow:auto;background:#f8fafc;padding:8px;border-radius:4px;font-size:.78rem;margin:2px 0}
245
+ .tool-detail pre.err{background:#fef2f2}
246
+ .reason{font-size:.8rem;color:#991b1b;margin-left:8px}
247
+ .completion-verdict{padding:8px 12px;margin:8px 0 0 12px;border-radius:6px;font-size:.85rem}
248
+ .completion-verdict.accepted{background:#f0fdf4;border:1px solid #bbf7d0}
249
+ .completion-verdict.rejected{background:#fef3c7;border:1px solid #fde68a}
250
+ .completion-verdict.failed{background:#fef2f2;border:1px solid #fecaca}
251
+ .val-chain{display:flex;gap:6px;flex-wrap:wrap;margin-top:4px;font-size:.78rem}
252
+ .val-stage{background:#f1f5f9;padding:2px 6px;border-radius:4px;color:#64748b}
253
+ .timeline{font-size:.82rem;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
254
+ .tl-item{display:flex;align-items:baseline;gap:8px;padding:4px 12px;border-bottom:1px solid #f8fafc;flex-wrap:wrap}
255
+ .tl-item:hover{background:#f8fafc}
256
+ .tl-ts{font-family:monospace;font-size:.75rem;color:#94a3b8;min-width:6rem}
257
+ .tl-icon{min-width:1.2rem;text-align:center}
258
+ .tl-type{font-weight:600;min-width:10rem;font-size:.8rem}
259
+ .tl-scope{font-family:monospace;font-size:.7rem;color:#94a3b8}
260
+ .tl-data{font-size:.75rem;color:#64748b;overflow:hidden;text-overflow:ellipsis;max-width:30rem}
261
+ .errors{background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:12px 16px;margin:16px 0;font-size:.85rem;color:#991b1b}
262
+ .errors ul{margin:4px 0 0;padding-left:20px}
263
+ .raw-events{font-size:.78rem;padding:0;list-style:none}
264
+ .raw-events li{padding:4px 8px;border-bottom:1px solid #f1f5f9}
265
+ .raw-events pre{white-space:pre-wrap;max-height:12rem;overflow:auto;background:#f8fafc;padding:6px;border-radius:4px;font-size:.72rem;margin:2px 0}
266
+ summary{cursor:pointer}
267
+ details>summary h2{display:inline;margin:0}
268
+ pre{margin:0;word-break:break-word}
269
+ code{word-break:break-all}
270
+ `;
@@ -0,0 +1,13 @@
1
+ export { REPLAY_SCHEMA_VERSION } from "./events.js";
2
+ export type { ModelUsage, PricingInput, PricingResolver, RecordingMode, ReplayArtifactRef, ReplayEvent, ReplayEventDomain, ReplayEventEnvelope, ReplayEventScope, } from "./events.js";
3
+ export { FileRunStore } from "./store.js";
4
+ export { CHECKPOINT_SCHEMA_VERSION, encodeCheckpoint, decodeCheckpoint } from "./checkpoint.js";
5
+ export type { CheckpointDocument, CheckpointNodeBoundary } from "./checkpoint.js";
6
+ export type { ArtifactStore, CheckpointStore, FileRunStoreOptions, JournalStore, RunStore, } from "./store.js";
7
+ export { Recorder, toRecordedJson } from "./recorder.js";
8
+ export type { RecorderOptions } from "./recorder.js";
9
+ export { finalizeJournal } from "./finalizer.js";
10
+ export type { FinalizeJournalOptions, ReplayDocument, ReplayRecordingSummary } from "./finalizer.js";
11
+ export { parseReplay } from "./parser.js";
12
+ export { exportReplayHtml, escapeHtml } from "./html.js";
13
+ export type { ReplayModel, ReplayInvocationModel } from "./model.js";
@@ -0,0 +1,7 @@
1
+ export { REPLAY_SCHEMA_VERSION } from "./events.js";
2
+ export { FileRunStore } from "./store.js";
3
+ export { CHECKPOINT_SCHEMA_VERSION, encodeCheckpoint, decodeCheckpoint } from "./checkpoint.js";
4
+ export { Recorder, toRecordedJson } from "./recorder.js";
5
+ export { finalizeJournal } from "./finalizer.js";
6
+ export { parseReplay } from "./parser.js";
7
+ export { exportReplayHtml, escapeHtml } from "./html.js";