@vincemakes/kiso-runtime 0.1.38 → 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.
@@ -0,0 +1,208 @@
1
+ /**
2
+ * E1 (1.2.0) — slice 3, the request tracer (the guard).
3
+ *
4
+ * One TraceRecord per adapter call, settled when the stream ends. The
5
+ * guard lives AT the adapter boundary (run.ts wires
6
+ * truncationGuard(traceGuard(tracer, adapter))): the kernel, the log,
7
+ * and the model-visible byte stream are untouched (I6 — pinned by
8
+ * trace-bytes.test.ts). retryAttempt counts prior calls in this run
9
+ * with an identical contextHash — the loop re-streams the SAME messages
10
+ * array on retry (loop.ts), so an identical hash is exactly "same
11
+ * request, retried" (§1.4).
12
+ *
13
+ * Soft-fail: any failure in trace assembly (never expected — hashes and
14
+ * the manifest cannot throw today) marks the request ABSENT rather than
15
+ * breaking the stream; the writer's own degradation covers I/O.
16
+ *
17
+ * Usage is provider-raw, never normalized (E2's job): openai-compat
18
+ * reports input TOTAL (fresh = input − cacheRead); anthropic's
19
+ * input_tokens is ALREADY fresh-only (the trace records it as-is).
20
+ * A request that settles with NO usage data records the quartet as
21
+ * zeros with cacheWrite null — "0 = unknown", documented at record.ts;
22
+ * the honest nullable quartet is a schema bump, deferred.
23
+ */
24
+ import { randomUUID } from "node:crypto";
25
+ import { buildContextManifest, segmentHashes } from "./manifest.js";
26
+ import { cacheableHashes } from "./analyze.js";
27
+ import { hashContext, hashSystemPrompt, hashToolSpecs, stablePrefixFingerprint } from "./hash.js";
28
+ import { PRICING_TABLE_V1, canonicalizeUsage } from "../usage/canonical.js";
29
+ import { TRACE_SCHEMA_VERSION } from "./record.js";
30
+ import { TraceWriter } from "./writer.js";
31
+ export class RequestTracer {
32
+ #writer;
33
+ #log;
34
+ #provider;
35
+ #runId;
36
+ #adapterVersion;
37
+ #requestIndex = 0;
38
+ #contextHashCounts = new Map();
39
+ constructor(deps) {
40
+ this.#writer = new TraceWriter({ root: deps.root, sessionId: deps.sessionId });
41
+ this.#log = deps.log;
42
+ this.#provider = deps.provider;
43
+ this.#runId = deps.runId;
44
+ this.#adapterVersion = deps.adapterVersion ?? null;
45
+ }
46
+ init() {
47
+ this.#writer.init();
48
+ }
49
+ async *wrap(options, upstream) {
50
+ const t0 = performance.now();
51
+ let record = null;
52
+ try {
53
+ record = this.#startRecord(options);
54
+ }
55
+ catch {
56
+ record = null; // trace absent; the stream is never affected
57
+ }
58
+ // null until the first event; settled to 0 (unknown) only when no
59
+ // event ever came — a stream whose first event lands in the same
60
+ // tick records 0, the resolution limit of the "0 = unknown" marker
61
+ let ttftMs = null;
62
+ let inputTokens = null;
63
+ let cacheRead = null;
64
+ let cacheWrite = null;
65
+ let outputTokens = null;
66
+ let usageKnown = false;
67
+ const toolCalls = [];
68
+ let outcome = "ok";
69
+ try {
70
+ for await (const ev of upstream) {
71
+ // the null check must be the ONLY guard — a 0-initialized
72
+ // number never moves (the slice-3 dead-field finding)
73
+ if (ttftMs === null)
74
+ ttftMs = performance.now() - t0;
75
+ if (ev.type === "tool_call_start")
76
+ toolCalls.push(ev.name);
77
+ if (ev.type === "usage") {
78
+ usageKnown = usageKnown || ev.known;
79
+ if (ev.inputTokens !== null)
80
+ inputTokens = ev.inputTokens;
81
+ if (ev.cacheRead !== null)
82
+ cacheRead = ev.cacheRead;
83
+ if (ev.cacheWrite !== null)
84
+ cacheWrite = ev.cacheWrite;
85
+ if (ev.outputTokens !== null)
86
+ outputTokens = ev.outputTokens;
87
+ }
88
+ yield ev;
89
+ }
90
+ }
91
+ catch (err) {
92
+ outcome = this.#classifyOutcome(err, options);
93
+ throw err;
94
+ }
95
+ finally {
96
+ if (record !== null) {
97
+ this.#settle(record, {
98
+ outcome,
99
+ t0,
100
+ ttftMs,
101
+ toolCalls,
102
+ inputTokens,
103
+ cacheRead,
104
+ cacheWrite,
105
+ outputTokens,
106
+ usageKnown,
107
+ });
108
+ }
109
+ }
110
+ }
111
+ /** Clean-settle marking for the whole run. */
112
+ finishRun() {
113
+ this.#writer.finishRun(this.#runId, this.#requestIndex - 1);
114
+ }
115
+ #startRecord(options) {
116
+ const { systemPrompt, tools, messages } = options;
117
+ const contextHash = hashContext(systemPrompt, tools, messages);
118
+ const retryAttempt = this.#contextHashCounts.get(contextHash) ?? 0;
119
+ this.#contextHashCounts.set(contextHash, retryAttempt + 1);
120
+ const manifest = buildContextManifest({ log: this.#log, systemPrompt, tools, messages });
121
+ const hashes = segmentHashes(systemPrompt, tools, messages);
122
+ return {
123
+ schemaVersion: TRACE_SCHEMA_VERSION,
124
+ kind: "request",
125
+ requestId: randomUUID(),
126
+ runId: this.#runId,
127
+ requestIndex: this.#requestIndex++,
128
+ retryAttempt,
129
+ provider: this.#provider,
130
+ model: options.model,
131
+ adapterVersion: this.#adapterVersion,
132
+ systemPromptHash: hashSystemPrompt(systemPrompt),
133
+ toolSchemaHash: hashToolSpecs(tools),
134
+ contextHash,
135
+ contextManifest: manifest,
136
+ // R4b: the per-segment hash LIST rides the record (the break
137
+ // derivation is analysis-side but needs the list, not the
138
+ // aggregated fingerprint — slice 5's data source); the
139
+ // fingerprint covers the CACHEABLE prefix only — the current
140
+ // turn (freshness fresh) is never part of it (slice 4)
141
+ segmentHashes: hashes,
142
+ stablePrefixFingerprint: stablePrefixFingerprint(cacheableHashes(manifest, hashes)),
143
+ freshInput: 0, // unknown until the usage event — "0 = unknown"
144
+ cacheRead: 0,
145
+ cacheWrite: null,
146
+ output: 0,
147
+ // the canonical block starts at the same "0 = unknown" convention
148
+ // and is settled with the quartet (same raw, one derivation)
149
+ canonical: {
150
+ input: 0,
151
+ output: 0,
152
+ cacheRead: 0,
153
+ cacheWrite: null,
154
+ reasoning: null,
155
+ costUsd: 0,
156
+ pricingTableId: PRICING_TABLE_V1.id,
157
+ pricingTableVersion: PRICING_TABLE_V1.version,
158
+ },
159
+ latencyMs: 0,
160
+ ttftMs: 0,
161
+ toolCalls: [],
162
+ outcome: "ok",
163
+ ts: Date.now(),
164
+ };
165
+ }
166
+ #settle(record, p) {
167
+ record.outcome = p.outcome;
168
+ record.latencyMs = performance.now() - p.t0;
169
+ record.ttftMs = p.ttftMs ?? 0; // null = no event ever — the "0 = unknown" marker
170
+ record.toolCalls = p.toolCalls;
171
+ if (p.usageKnown) {
172
+ record.freshInput =
173
+ this.#provider === "anthropic"
174
+ ? p.inputTokens ?? 0
175
+ : p.inputTokens !== null
176
+ ? Math.max(0, p.inputTokens - (p.cacheRead ?? 0))
177
+ : 0;
178
+ record.cacheRead = p.cacheRead ?? 0;
179
+ record.cacheWrite = p.cacheWrite ?? null;
180
+ record.output = p.outputTokens ?? 0;
181
+ }
182
+ // E2 — the canonical block formalizes the same raw (the route-keyed
183
+ // mapping; the validator pins block == quartet, so the two can never
184
+ // drift apart). Unknown usage canonicalizes to the "0 = unknown"
185
+ // convention, consistent with the quartet above.
186
+ record.canonical = canonicalizeUsage(this.#provider, {
187
+ inputTokens: p.inputTokens,
188
+ outputTokens: p.outputTokens,
189
+ cacheRead: p.cacheRead,
190
+ cacheWrite: p.cacheWrite,
191
+ });
192
+ this.#writer.enqueue(record);
193
+ }
194
+ #classifyOutcome(err, options) {
195
+ if (options.signal?.aborted === true)
196
+ return "aborted";
197
+ const name = err instanceof Error ? err.name : "";
198
+ if (name === "AbortError" || name === "APIUserAbortError")
199
+ return "aborted";
200
+ return "provider_error";
201
+ }
202
+ }
203
+ /** Wrap the adapter so every stream() call settles a trace record. */
204
+ export function traceGuard(tracer, adapter) {
205
+ return {
206
+ stream: (options) => tracer.wrap(options, adapter.stream(options)),
207
+ };
208
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * E1 (1.2.0) — the request hashes (proposal §4, ruling R4b + R1a).
3
+ *
4
+ * Every hash is pinned to the schemaVersion's HashSpec (sha-256,
5
+ * full hex) — `hashSpecFor(TRACE_SCHEMA_VERSION)` is checked at every
6
+ * call, so a version bump without a re-pin fails loudly instead of
7
+ * silently changing the algorithm. The canonical serialization is
8
+ * JSON.stringify of the request-shaped values; the byte discipline is
9
+ * that the same construction path (the kernel's projection) produces
10
+ * the same serialization, which the trace-bytes gate pins end-to-end.
11
+ */
12
+ import type { Message, ToolSpec } from "@vincemakes/kiso-core";
13
+ export declare function sha256Hex(input: string): string;
14
+ export declare const canonicalJson: (v: unknown) => string;
15
+ export declare function hashSystemPrompt(systemPrompt: string | undefined): string;
16
+ export declare function hashToolSpecs(tools: readonly ToolSpec[] | undefined): string;
17
+ /** The full request projection — the same messages array the loop hands
18
+ * the adapter, so a retry re-serializes to an IDENTICAL hash (§1.4). */
19
+ export declare function hashContext(systemPrompt: string | undefined, tools: readonly ToolSpec[] | undefined, messages: readonly Message[]): string;
20
+ /** sha-256 over the per-segment hashes of the cacheable prefix — the
21
+ * segments up to but NOT including the current turn (R4b). */
22
+ export declare function stablePrefixFingerprint(segmentHashes: readonly string[]): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * E1 (1.2.0) — the request hashes (proposal §4, ruling R4b + R1a).
3
+ *
4
+ * Every hash is pinned to the schemaVersion's HashSpec (sha-256,
5
+ * full hex) — `hashSpecFor(TRACE_SCHEMA_VERSION)` is checked at every
6
+ * call, so a version bump without a re-pin fails loudly instead of
7
+ * silently changing the algorithm. The canonical serialization is
8
+ * JSON.stringify of the request-shaped values; the byte discipline is
9
+ * that the same construction path (the kernel's projection) produces
10
+ * the same serialization, which the trace-bytes gate pins end-to-end.
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ import { TRACE_SCHEMA_VERSION, hashSpecFor } from "./record.js";
14
+ export function sha256Hex(input) {
15
+ hashSpecFor(TRACE_SCHEMA_VERSION); // the pinned algorithm, or fail loudly
16
+ return createHash("sha256").update(input, "utf8").digest("hex");
17
+ }
18
+ export const canonicalJson = (v) => JSON.stringify(v);
19
+ export function hashSystemPrompt(systemPrompt) {
20
+ return sha256Hex(systemPrompt ?? "");
21
+ }
22
+ export function hashToolSpecs(tools) {
23
+ return sha256Hex(canonicalJson(tools ?? []));
24
+ }
25
+ /** The full request projection — the same messages array the loop hands
26
+ * the adapter, so a retry re-serializes to an IDENTICAL hash (§1.4). */
27
+ export function hashContext(systemPrompt, tools, messages) {
28
+ return sha256Hex(canonicalJson({ systemPrompt, tools, messages }));
29
+ }
30
+ /** sha-256 over the per-segment hashes of the cacheable prefix — the
31
+ * segments up to but NOT including the current turn (R4b). */
32
+ export function stablePrefixFingerprint(segmentHashes) {
33
+ return sha256Hex(segmentHashes.join(""));
34
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * E1 (1.2.0) — slice 3, the context manifest (proposal §1.3).
3
+ *
4
+ * buildContextManifest derives the per-request segment list from the
5
+ * session log and the request projection. Segments are the system
6
+ * prompt, the tool schema, then one per user turn. Each turn carries a
7
+ * THIN seqRange pointer into the event log ([firstSeq, lastSeq] of the
8
+ * events that produced it) plus an estTokens estimate — the record
9
+ * never copies payloads (proposal §1.1).
10
+ *
11
+ * Turn boundaries are the log's visible user_input events: a VETOED
12
+ * input (user_input_replaced with null content) is not a boundary — the
13
+ * model never saw it; a REWRITE keeps the original boundary position
14
+ * (the projection renders the replacement in place, events.ts:339).
15
+ * The last turn is current_turn/fresh, earlier turns cache_read; the
16
+ * system/tools head is cache_read (a stable prefix, R4b).
17
+ *
18
+ * When the projection's user-message count and the log's visible
19
+ * boundary count diverge (a summary replaced whole turns, an alignment
20
+ * surprise), every turn range degrades to null — honest thin pointers
21
+ * rather than wrong ones.
22
+ */
23
+ import type { Event, Message, ToolSpec } from "@vincemakes/kiso-core";
24
+ import type { TraceSegment } from "./record.js";
25
+ export interface ManifestInput {
26
+ log: readonly Event[];
27
+ systemPrompt: string | undefined;
28
+ tools: readonly ToolSpec[] | undefined;
29
+ messages: readonly Message[];
30
+ }
31
+ export declare function buildContextManifest(input: ManifestInput): TraceSegment[];
32
+ /** Per-segment content hashes for the fingerprint (R4b): the system
33
+ * prompt, the tool schema, and each turn's messages, in manifest
34
+ * order. The CURRENT turn's hash is the last element — the cacheable
35
+ * prefix is everything before it. */
36
+ export declare function segmentHashes(systemPrompt: string | undefined, tools: readonly ToolSpec[] | undefined, messages: readonly Message[]): string[];
@@ -0,0 +1,100 @@
1
+ /**
2
+ * E1 (1.2.0) — slice 3, the context manifest (proposal §1.3).
3
+ *
4
+ * buildContextManifest derives the per-request segment list from the
5
+ * session log and the request projection. Segments are the system
6
+ * prompt, the tool schema, then one per user turn. Each turn carries a
7
+ * THIN seqRange pointer into the event log ([firstSeq, lastSeq] of the
8
+ * events that produced it) plus an estTokens estimate — the record
9
+ * never copies payloads (proposal §1.1).
10
+ *
11
+ * Turn boundaries are the log's visible user_input events: a VETOED
12
+ * input (user_input_replaced with null content) is not a boundary — the
13
+ * model never saw it; a REWRITE keeps the original boundary position
14
+ * (the projection renders the replacement in place, events.ts:339).
15
+ * The last turn is current_turn/fresh, earlier turns cache_read; the
16
+ * system/tools head is cache_read (a stable prefix, R4b).
17
+ *
18
+ * When the projection's user-message count and the log's visible
19
+ * boundary count diverge (a summary replaced whole turns, an alignment
20
+ * surprise), every turn range degrades to null — honest thin pointers
21
+ * rather than wrong ones.
22
+ */
23
+ import { estimateTokens } from "@vincemakes/kiso-core";
24
+ import { canonicalJson, hashSystemPrompt, hashToolSpecs, sha256Hex } from "./hash.js";
25
+ /** The log's visible user boundaries, in log order: every user_input
26
+ * whose replacement (if any) is not a veto. */
27
+ function visibleBoundaries(log) {
28
+ const vetoed = new Set();
29
+ for (const ev of log) {
30
+ if (ev.type === "user_input_replaced" && ev.content === null)
31
+ vetoed.add(ev.replaces);
32
+ }
33
+ const result = [];
34
+ for (const ev of log) {
35
+ if (ev.type === "user_input" && !vetoed.has(ev.seq))
36
+ result.push(ev.seq);
37
+ }
38
+ return result;
39
+ }
40
+ const systemSegment = (systemPrompt) => ({
41
+ role: "system",
42
+ seqRange: null,
43
+ estTokens: estimateTokens([{ role: "user", content: systemPrompt ?? "" }]),
44
+ freshness: "cache_read",
45
+ });
46
+ const toolsSegment = (tools) => ({
47
+ role: "tools",
48
+ seqRange: null,
49
+ estTokens: estimateTokens([{ role: "tool", callId: "", content: JSON.stringify(tools ?? []), isError: false }]),
50
+ freshness: "cache_read",
51
+ });
52
+ export function buildContextManifest(input) {
53
+ const { log, systemPrompt, tools, messages } = input;
54
+ const boundaries = visibleBoundaries(log);
55
+ const userCount = messages.filter((m) => m.role === "user").length;
56
+ const aligned = userCount === boundaries.length;
57
+ const lastSeq = log.length > 0 ? log[log.length - 1].seq : -1;
58
+ const segments = [systemSegment(systemPrompt), toolsSegment(tools)];
59
+ // Partition the projection at user boundaries: partition k runs from
60
+ // user_k (inclusive) to user_{k+1} (exclusive); messages before the
61
+ // first user (not produced by the projection today) join partition 0.
62
+ const userPositions = [];
63
+ messages.forEach((m, i) => {
64
+ if (m.role === "user")
65
+ userPositions.push(i);
66
+ });
67
+ for (let k = 0; k < userPositions.length; k++) {
68
+ const start = k === 0 ? 0 : userPositions[k];
69
+ const end = k + 1 < userPositions.length ? userPositions[k + 1] : messages.length;
70
+ const isCurrent = k === userPositions.length - 1;
71
+ const range = aligned && boundaries[k] !== undefined
72
+ ? [boundaries[k], k + 1 < boundaries.length ? boundaries[k + 1] - 1 : lastSeq]
73
+ : null;
74
+ segments.push({
75
+ role: isCurrent ? "current_turn" : "turn",
76
+ seqRange: range,
77
+ estTokens: estimateTokens(messages.slice(start, end)),
78
+ freshness: isCurrent ? "fresh" : "cache_read",
79
+ });
80
+ }
81
+ return segments;
82
+ }
83
+ /** Per-segment content hashes for the fingerprint (R4b): the system
84
+ * prompt, the tool schema, and each turn's messages, in manifest
85
+ * order. The CURRENT turn's hash is the last element — the cacheable
86
+ * prefix is everything before it. */
87
+ export function segmentHashes(systemPrompt, tools, messages) {
88
+ const hashes = [hashSystemPrompt(systemPrompt), hashToolSpecs(tools)];
89
+ const userPositions = [];
90
+ messages.forEach((m, i) => {
91
+ if (m.role === "user")
92
+ userPositions.push(i);
93
+ });
94
+ for (let k = 0; k < userPositions.length; k++) {
95
+ const start = k === 0 ? 0 : userPositions[k];
96
+ const end = k + 1 < userPositions.length ? userPositions[k + 1] : messages.length;
97
+ hashes.push(sha256Hex(canonicalJson(messages.slice(start, end))));
98
+ }
99
+ return hashes;
100
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * E1 (1.2.0) — the Request Trace record schema (proposal §1.1 as adopted
3
+ * by the R1 ruling: "adopt as-is, keep ts, per-round additive"). This is
4
+ * the 1.2.0 field-set lock: every field below is spec'd, and the
5
+ * closed-field-set gate (ruling R1a) pins the shape bidirectionally —
6
+ * `TRACE_RECORD_FIELDS`/`TRACE_SEGMENT_FIELDS` must stay in exact
7
+ * agreement with the interfaces, or trace-schema.test.ts goes red.
8
+ *
9
+ * R1a: `schemaVersion` pins BOTH the record shape AND the hash /
10
+ * fingerprint algorithms (`HASH_SPEC_BY_VERSION`). A version bump must
11
+ * pin a spec for the new version before any writer can use it
12
+ * (`hashSpecFor` throws otherwise).
13
+ *
14
+ * The ledger is OUT-side (ADR-0051 §6): versionable, never part of the
15
+ * correctness ABI. These types are runtime-internal.
16
+ *
17
+ * E2 (1.3.0) — schemaVersion 2: the record gains the `canonical` block
18
+ * (E2 proposal §1.5 Case A — the nested block, ruled 2026-08-13; raw
19
+ * quartet stays as provider observation above it). The validators accept
20
+ * BOTH generations (R1d-1): a v1 sidecar has no canonical block and reads
21
+ * as defaults at every consumer — never a crash.
22
+ */
23
+ /** schemaVersion: 2 for 1.3.0 (the canonical block). Version 1 = the 1.2.0
24
+ * shape, kept for generation-compat reads (R1d-1). Algorithm and shape
25
+ * changes bump it (ADR-0051 §6 OUT-side versioning). */
26
+ export declare const TRACE_SCHEMA_VERSION = 2;
27
+ /** The versions a reader may meet in a ledger. v1 records are accepted
28
+ * (generation-compat, R1d-1) and read as defaults — no canonical block. */
29
+ export declare const TRACE_SCHEMA_VERSIONS: Readonly<Set<number>>;
30
+ import type { CanonicalUsage } from "../usage/canonical.js";
31
+ export type Freshness = "fresh" | "cache_read" | "cache_write";
32
+ /** That is the complete set for 1.2.0. */
33
+ export type Outcome = "ok" | "provider_error" | "aborted";
34
+ /** That is the complete set for 1.2.0. (Truncation surfaces as "aborted"
35
+ * when the stream ends before settle; refined in E5 if needed.) */
36
+ export interface TraceSegment {
37
+ role: "system" | "tools" | "turn" | "current_turn";
38
+ /** Thin pointer into the event log: [firstSeq, lastSeq] inclusive of the
39
+ * events that produced this segment. null for system/tools (not events). */
40
+ seqRange: [number, number] | null;
41
+ estTokens: number;
42
+ freshness: Freshness;
43
+ }
44
+ /** That is the complete set for 1.2.0. */
45
+ export interface TraceRecord {
46
+ schemaVersion: 2;
47
+ kind: "request";
48
+ requestId: string;
49
+ runId: string;
50
+ requestIndex: number;
51
+ retryAttempt: number;
52
+ provider: string;
53
+ model: string;
54
+ adapterVersion: string | null;
55
+ systemPromptHash: string;
56
+ toolSchemaHash: string;
57
+ contextHash: string;
58
+ contextManifest: TraceSegment[];
59
+ /** Per-segment content hashes, indexed 1:1 with contextManifest
60
+ * (segment i's canonical serialization — R4b's analysis-side data
61
+ * source: the cache-break derivation needs the LIST, not the
62
+ * aggregated fingerprint). Sizing note: 64 hex chars × segments
63
+ * per request. */
64
+ segmentHashes: string[];
65
+ stablePrefixFingerprint: string;
66
+ /** The usage quartet is PROVIDER-RAW — never a billing surface.
67
+ * Canonical/billing usage lives in the `canonical` block (E2); these
68
+ * fields are observation only (a provider may count a token a dozen
69
+ * ways; billing must not). */
70
+ freshInput: number;
71
+ cacheRead: number;
72
+ cacheWrite: number | null;
73
+ output: number;
74
+ /** E2 — the canonical record of the same raw quartet (the pinned
75
+ * sentence: input is FRESH-ONLY; total = input + cacheRead + cacheWrite
76
+ * is the derived quantity). Formalizes the quartet by construction —
77
+ * the validator pins the equality — and carries the cost from the
78
+ * versioned pricing table (every cost records its table version). */
79
+ canonical: CanonicalUsage;
80
+ latencyMs: number;
81
+ ttftMs: number;
82
+ toolCalls: string[];
83
+ outcome: Outcome;
84
+ lineageLink?: {
85
+ parentSessionId: string;
86
+ parentRunId: string;
87
+ parentInvocationSeq: number;
88
+ role: string;
89
+ };
90
+ ts: number;
91
+ }
92
+ /** That is the complete set for 1.2.0. */
93
+ export interface HeaderLine {
94
+ schemaVersion: 2;
95
+ kind: "header";
96
+ sessionId: string;
97
+ kisoVersion: string;
98
+ createdAt: number;
99
+ }
100
+ export interface RunEndLine {
101
+ schemaVersion: 2;
102
+ kind: "run_end";
103
+ runId: string;
104
+ ts: number;
105
+ lastRequestIndex: number;
106
+ }
107
+ export interface CrashLine {
108
+ schemaVersion: 2;
109
+ kind: "crash";
110
+ ts: number;
111
+ note: string;
112
+ }
113
+ export type TraceLine = HeaderLine | TraceRecord | RunEndLine | CrashLine;
114
+ export interface HashSpec {
115
+ readonly algorithm: "sha-256";
116
+ readonly output: "full-hex";
117
+ }
118
+ export declare const HASH_SPEC_BY_VERSION: Readonly<Record<number, HashSpec>>;
119
+ export declare function hashSpecFor(version: number): HashSpec;
120
+ /** The 1.2.0 field set (schemaVersion 1) — kept verbatim for
121
+ * generation-compat reads of old sidecars (R1d-1): a v1 record has no
122
+ * canonical block and reads as defaults at every consumer. */
123
+ export declare const TRACE_RECORD_FIELDS_V1: readonly ["schemaVersion", "kind", "requestId", "runId", "requestIndex", "retryAttempt", "provider", "model", "adapterVersion", "systemPromptHash", "toolSchemaHash", "contextHash", "contextManifest", "segmentHashes", "stablePrefixFingerprint", "freshInput", "cacheRead", "cacheWrite", "output", "latencyMs", "ttftMs", "toolCalls", "outcome", "lineageLink", "ts"];
124
+ /** The 1.3.0 field set (schemaVersion 2) = the v1 set + `canonical`. */
125
+ export declare const TRACE_RECORD_FIELDS: readonly ["schemaVersion", "kind", "requestId", "runId", "requestIndex", "retryAttempt", "provider", "model", "adapterVersion", "systemPromptHash", "toolSchemaHash", "contextHash", "contextManifest", "segmentHashes", "stablePrefixFingerprint", "freshInput", "cacheRead", "cacheWrite", "output", "latencyMs", "ttftMs", "toolCalls", "outcome", "lineageLink", "ts", "canonical"];
126
+ export declare const TRACE_SEGMENT_FIELDS: readonly ["role", "seqRange", "estTokens", "freshness"];
127
+ export declare function validateTraceSegment(v: unknown): v is TraceSegment;
128
+ export declare function validateTraceRecord(v: unknown): v is TraceRecord;
129
+ export declare function validateTraceLine(v: unknown): v is TraceLine;