/** * Logging contract — structured, correlated, span-capable Logger/Span ABI. * * PURE TYPES ONLY — no implementations. The createLogger factory lives in * ../logging/logger.js. The kernel owns types + pure record-builders. * NO I/O — the LogSink is injected by the host-bin. * * Key properties: * - P3-safe: correlation flows via explicit child()/span() values, no ambient. * - P2: record-builder is pure ({ now, newId } injected). * - D3: spans emitted incrementally (open at span(), close at end()). * - D6: extensionId auto-stamped by host, not caller-supplied. * - Flat scalar attributes (serializable D3, queryable D9). * - Optional `body` field on spans for large verbatim payloads (store-fat-serve-thin). */ // --- Levels --- export type Level = "debug" | "info" | "warn" | "error"; // --- Attributes --- /** Flat, serializable key/value pairs. Caller stringifies nested objects. */ export type Attributes = Readonly>; // --- LogContext (correlation) --- /** Correlation context carried on every log record and span. */ export interface LogContext { /** Auto-stamped by host from manifest.id (D6) — never caller-supplied. */ readonly extensionId: string; readonly conversationId?: string; readonly turnId?: string; readonly spanId?: string; readonly parentSpanId?: string; } // --- Span --- /** * A timed unit of work within a trace. Opened via `logger.span(name)`, * closed via `span.end()`. Emits incremental open/close records so a * crashed turn is reconstructable from the journal (D3). */ export interface Span { readonly id: string; /** Pre-bound Logger scoped to this span's correlation. */ readonly log: Logger; /** Add or overwrite attributes on this span. */ readonly setAttributes: (attrs: Attributes) => void; /** Record a causal link to another span (D4 cross-feature causality). */ readonly addLink: ( target: { readonly spanId: string; readonly turnId?: string }, reason?: string, ) => void; /** Open a child span nested under this one. */ readonly child: (name: string, attrs?: Attributes, body?: string) => Span; /** * Close this span. Records duration + status. Optionally records an * error, additional attributes, and/or a body payload. */ readonly end: (outcome?: { readonly err?: unknown; readonly attrs?: Attributes; readonly body?: string; }) => void; } // --- Logger --- /** * Structured, correlated logger. The host auto-scopes each extension's * logger with its manifest.id as extensionId (D6). * * `info("msg")` must still compile — attrs is optional (backward compat). */ export interface Logger { readonly debug: (msg: string, attrs?: Attributes) => void; readonly info: (msg: string, attrs?: Attributes) => void; readonly warn: (msg: string, attrs?: Attributes) => void; readonly error: (msg: string, attrs?: ErrorAttributes) => void; /** * Create a child logger with additional correlation context. * Explicit values passed down (P3 — no ambient state). */ readonly child: (ctx: Partial & { readonly attrs?: Attributes }) => Logger; /** Open a new span. Emits a `span-open` record immediately (D3). */ readonly span: (name: string, attrs?: Attributes, body?: string) => Span; } /** * Attributes for error log calls. The `err` field is `unknown` (not * constrained to Attributes' scalar index signature) so callers can * pass `error("msg", { err })` directly. */ export interface ErrorAttributes { readonly err?: unknown; readonly [key: string]: unknown; } // --- LogRecord (discriminated union) --- /** * Status of a span. "ok" is the default when no error is recorded. */ export type SpanStatus = "ok" | "error"; /** * A link to another span, recorded at a handoff moment (D4). */ export interface SpanLink { readonly spanId: string; readonly turnId?: string; readonly reason?: string; } /** * Flat, JSON-serializable discriminated union. Spans are emitted * incrementally (open at span(), close at end()) so a crashed turn is * reconstructable from the journal (D3). * * Every variant carries correlation keys + timestamp. An optional `body` * field (string) holds large verbatim payloads (store-fat-serve-thin). */ export type LogRecord = LogLineRecord | SpanOpenRecord | SpanCloseRecord; /** A structured log line (debug/info/warn/error). */ export interface LogLineRecord { readonly kind: "log"; readonly level: Level; readonly msg: string; readonly timestamp: number; readonly extensionId: string; readonly conversationId?: string; readonly turnId?: string; readonly spanId?: string; readonly parentSpanId?: string; readonly attributes?: Attributes; /** Optional large verbatim payload (store-fat, serve-thin). */ readonly body?: string; } /** Emitted when a span is opened (at `logger.span(name)`). */ export interface SpanOpenRecord { readonly kind: "span-open"; readonly spanId: string; readonly name: string; readonly timestamp: number; readonly extensionId: string; readonly conversationId?: string; readonly turnId?: string; readonly parentSpanId?: string; readonly attributes?: Attributes; readonly links?: readonly SpanLink[]; readonly body?: string; } /** Emitted when a span is closed (at `span.end()`). Carries duration + status. */ export interface SpanCloseRecord { readonly kind: "span-close"; readonly spanId: string; readonly name: string; readonly timestamp: number; readonly durationMs: number; readonly status: SpanStatus; readonly extensionId: string; readonly conversationId?: string; readonly turnId?: string; readonly parentSpanId?: string; readonly attributes?: Attributes; readonly links?: readonly SpanLink[]; readonly body?: string; } // --- LogSink --- /** * Fire-and-forget sink. The kernel calls emit(); the host-bin injects * a concrete implementation. Kernel never lets sink errors escape (D7). */ export interface LogSink { readonly emit: (record: LogRecord) => void; } // --- Deterministic helpers (injected for testability) --- /** Clock + id generator injected into the logger factory. */ export interface LogDeps { readonly now: () => number; readonly newId: () => string; }