Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/coding-agent/docs/invocation-traces.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,43 @@ Moving or renaming a session does not rewrite existing trace references.
This preserves invocation evidence while allowing readers to report that referenced session content is missing or moved.

Trace files contain execution telemetry and immutable session entry references, not duplicated transcript content.

## Evidence Ownership

Each fact has one authoritative source so a trace remains compact and materialization does not create competing copies.

| Source | Authoritative facts | Materialized form |
| --- | --- | --- |
| Coding-agent v3 session JSONL | Messages, assistant tool calls, tool results, message usage, model and thinking changes, compaction and branch summaries, custom extension entries, labels, and session metadata | `session_item` evidence selected by entry ordinal, entry ID, and digest |
| AgentHarness session JSONL | Durable entries, operation admission and settlement, abort requests, retry attempts, tool admission, queue changes, deferred writes, usage, lane pointers, and session facts | `session_item` evidence selected by durable session sequence |
| Agent and AI telemetry spans | Agent runs, turns, logical provider requests, provider outcomes, tool execution outcomes, usage, and causal invocation, turn, request, and tool-call identifiers | Consolidated `span` evidence |
| Coding-agent and extension telemetry spans | User-visible operations, retries and sleeps, extension hook outcomes, subagent coordination, terminal commands, external artifact references, and runtime diagnostics | Consolidated `span` evidence |
| Trace lifecycle records | Producer identity, session bindings, harness activation, session segments, trace writer degradation, and completed, failed, or aborted process termination | `trace_record` evidence and document diagnostics |
| Missing trace footer | A process that did not close its trace orderly | A `crashed` materialized termination status with a missing-footer diagnostic |

Agent and extension events remain live notification APIs.
Their durable facts are represented by the owning session entry, session record, or telemetry span listed above rather than by copying event payloads into the trace.

Startup and configuration failures are recorded as runtime diagnostic spans after trace creation.
Failures that prevent trace creation remain an explicit limitation because no durable trace destination exists.

## Materialization

`materializeJsonlInvocation()` reads and validates a trace, resolves every referenced session binding, verifies each segment boundary, and returns a versioned `MaterializedInvocation` document.
The optional session path resolver can locate a session that moved after the trace was written while preserving both its recorded and resolved paths.

Coding-agent v3 content digests are SHA-256 hashes of `JSON.stringify(entry)`.
Prefix digests are SHA-256 hashes of the LF-framed canonical entry JSON from the first non-header entry through the referenced entry.
AgentHarness segments use inclusive durable session sequence bounds.

The materializer embeds only session items referenced by trace segments.
It preserves message and tool payloads exactly as stored in the session, combines span starts, events, and settlements, and retains every contributing trace record ID, span ID, session binding ID, session entry ID, and session sequence.

Evidence is ordered deterministically by trace sequence, session cursor, timestamp, span parentage, and causal identifiers.
Session cursor order is durable inside a segment, but the document reports ordering ambiguity when clocks and segment boundaries cannot establish a total order with trace records.

Missing, changed, truncated, or incompatible session data produces diagnostics without discarding valid trace records or earlier session evidence.
Writer-degraded and crashed traces likewise produce partial documents instead of failing materialization.

`serializeMaterializedInvocation()` emits stable, indented JSON with a trailing newline for storage or bounded LLM analysis.
The materialized document contains evidence only and does not include extracted claims, evolution proposals, approval state, or behavior mutations.
26 changes: 26 additions & 0 deletions packages/harness-trace/src/evidence-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { MaterializedCausalIdentifier } from "./materialized-types.ts";

function isIdentifierName(name: string): boolean {
return name === "id" || name.endsWith("Id") || name.endsWith(".id") || name.endsWith("_id");
}

function collectCausalIdentifiers(value: unknown, prefix = "", result = new Map<string, string>()): void {
if (Array.isArray(value)) {
for (const item of value) collectCausalIdentifiers(item, prefix, result);
return;
}
if (typeof value !== "object" || value === null) return;
for (const [name, item] of Object.entries(value)) {
const qualifiedName = prefix ? `${prefix}.${name}` : name;
if (typeof item === "string" && isIdentifierName(name)) result.set(`${qualifiedName}\0${item}`, item);
else collectCausalIdentifiers(item, qualifiedName, result);
}
}

export function causalIdentifiers(...values: readonly unknown[]): readonly MaterializedCausalIdentifier[] {
const identifiers = new Map<string, string>();
for (const value of values) collectCausalIdentifiers(value, "", identifiers);
return [...identifiers.entries()]
.map(([key, value]) => ({ name: key.slice(0, key.indexOf("\0")), value }))
.sort((left, right) => left.name.localeCompare(right.name) || left.value.localeCompare(right.value));
}
22 changes: 22 additions & 0 deletions packages/harness-trace/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
export { encodeTraceRecord, parseTraceRecord } from "./codec.ts";
export {
MATERIALIZED_INVOCATION_VERSION,
type MaterializationDiagnostic,
type MaterializationDiagnosticCode,
type MaterializationIntegrity,
type MaterializedCausalIdentifier,
type MaterializedEvidence,
type MaterializedEvidenceOrder,
type MaterializedInvocation,
type MaterializedInvocationOrdering,
type MaterializedSessionEvidence,
type MaterializedSessionPathResolver,
type MaterializedSessionSource,
type MaterializedSpan,
type MaterializedSpanEvent,
type MaterializedSpanEvidence,
type MaterializedTerminationStatus,
type MaterializedTraceEvidence,
type MaterializedTraceRecordSource,
type MaterializeJsonlInvocationOptions,
} from "./materialized-types.ts";
export { materializeJsonlInvocation, serializeMaterializedInvocation } from "./materializer.ts";
export { readJsonlTraceFile } from "./reader.ts";
export { createJsonlTraceContext } from "./recorder.ts";
export {
Expand Down
136 changes: 136 additions & 0 deletions packages/harness-trace/src/materialized-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { SpanAttributes, SpanStatus } from "@earendil-works/diffex-telemetry";
import type {
TraceHealth,
TraceProducer,
TraceRecord,
TraceSessionAttachRecord,
TraceTerminationStatus,
} from "./types.ts";

export const MATERIALIZED_INVOCATION_VERSION = 1 as const;

export type MaterializationIntegrity = "complete" | "partial" | "incompatible";
export type MaterializedTerminationStatus = TraceTerminationStatus | "crashed" | "unknown";

export type MaterializationDiagnosticCode =
| "trace_integrity"
| "trace_writer_degraded"
| "unsettled_span"
| "session_missing"
| "session_changed"
| "session_truncated"
| "session_incompatible"
| "session_invalid";

export interface MaterializationDiagnostic {
readonly code: MaterializationDiagnosticCode;
readonly message: string;
readonly recoverable: boolean;
readonly traceSequence?: number;
readonly bindingId?: string;
readonly sourceCode?: string;
}

export interface MaterializedCausalIdentifier {
readonly name: string;
readonly value: string;
}

export interface MaterializedEvidenceOrder {
readonly traceSequence: number;
readonly timestamp: string | null;
readonly sessionPosition?: number;
readonly parentSpanId?: number | null;
readonly causalIdentifiers: readonly MaterializedCausalIdentifier[];
readonly ambiguity: readonly string[];
}

export interface MaterializedTraceRecordSource {
readonly recordId: string;
readonly sequence: number;
readonly type: TraceRecord["type"];
}

export interface MaterializedSessionSource {
readonly bindingId: string;
readonly sessionId: string;
readonly path: string;
readonly resolvedPath?: string;
readonly sessionFormat: string;
readonly formatVersion: number;
readonly segmentTraceSequences: readonly number[];
readonly entryId?: string;
readonly entryOrdinal?: number;
readonly sessionSequence?: number;
}

export type MaterializedSessionPathResolver = (
attachment: TraceSessionAttachRecord,
) => string | undefined | Promise<string | undefined>;

export interface MaterializeJsonlInvocationOptions {
readonly resolveSessionPath?: MaterializedSessionPathResolver;
}

export interface MaterializedSpanEvent {
readonly name: string;
readonly timestamp: string;
readonly attributes: Readonly<SpanAttributes>;
readonly sourceTraceRecordId: string;
}

export interface MaterializedSpan {
readonly spanId: number;
readonly parentSpanId: number | null;
readonly name: string;
readonly startedAt: string;
readonly endedAt: string | null;
readonly startAttributes: Readonly<SpanAttributes>;
readonly endAttributes: Readonly<SpanAttributes> | null;
readonly events: readonly MaterializedSpanEvent[];
readonly status: SpanStatus | null;
readonly settled: boolean;
}

interface MaterializedEvidenceBase {
readonly evidenceId: string;
readonly order: MaterializedEvidenceOrder;
readonly traceSources: readonly MaterializedTraceRecordSource[];
readonly sessionSource?: MaterializedSessionSource;
}

export interface MaterializedTraceEvidence extends MaterializedEvidenceBase {
readonly kind: "trace_record";
readonly payload: TraceRecord;
}

export interface MaterializedSpanEvidence extends MaterializedEvidenceBase {
readonly kind: "span";
readonly payload: MaterializedSpan;
}

export interface MaterializedSessionEvidence extends MaterializedEvidenceBase {
readonly kind: "session_item";
readonly itemType: string;
readonly payload: Readonly<Record<string, unknown>>;
}

export type MaterializedEvidence = MaterializedTraceEvidence | MaterializedSpanEvidence | MaterializedSessionEvidence;

export interface MaterializedInvocationOrdering {
readonly criteria: readonly ["trace_sequence", "session_cursor", "timestamp", "span_parentage", "causal_id"];
readonly ambiguity: readonly string[];
}

export interface MaterializedInvocation {
readonly version: typeof MATERIALIZED_INVOCATION_VERSION;
readonly tracePath: string;
readonly traceId: string | null;
readonly producer: TraceProducer | null;
readonly integrity: MaterializationIntegrity;
readonly terminationStatus: MaterializedTerminationStatus;
readonly health: TraceHealth | null;
readonly ordering: MaterializedInvocationOrdering;
readonly evidence: readonly MaterializedEvidence[];
readonly diagnostics: readonly MaterializationDiagnostic[];
}
Loading