/** * Observability seam (1.37). core emits structured, **metadata-only** trace events; a deployment's * hook bridges them to OTel / Prometheus. core never imports an APM SDK. * * Privacy: events carry NO prompt/completion/tool-argument content (OTel SHOULD-NOT-by-default). If * content capture is ever added it will be a separate opt-in event the deployment redacts at collection. * * Delivery: best-effort, fire-and-forget. The hook returns `void` (callers can't synchronously await), * MUST NOT throw (core wraps it) and MUST NOT do sync I/O on the hot path. Every event carries * `version` so a hook can gracefully degrade on unknown fields. */ import type { TaskStatus, ToolEffect } from "./types.js"; import type { ThinkingLevel } from "../internal/harness.js"; export type TraceEvent = { kind: "task.start"; version: 1; taskId: string; model: string; ts: number; } | { /** * 提示词主权批 — the labelled composition of the assembled system prompt, emitted once right after * `task.start`. Kills the "which blocks were actually in the prompt" black box: a deployment can * assert the constitution/safety blocks are present (or that their absence was a deliberate * `replaceAll`) instead of diagnosing missing-prompt incidents by archaeology. Metadata-only * (block ids + sizes + short content hashes — never prompt text, per the file-header privacy rule). */ kind: "prompt.assembled"; version: 1; taskId: string; /** Who owned the constitution layer: "core" (structural default) / "replaced" (provider * `replaceAll` opt-out) / "provider-assembled" (migration guard pass-through) / "legacy" * (free-form `PromptProvider.system`). */ constitution: "core" | "replaced" | "provider-assembled" | "legacy"; /** Ordered blocks: stable id (e.g. `role.base`, `harness.context`, `memory.safety`, `skills.block`, * `env.context`, `memory.tail`), size in chars, sha256-prefix content hash. */ blocks: Array<{ id: string; chars: number; hash: string; }>; totalChars: number; ts: number; } | { /** * How a task's requested reasoning intensity RESOLVED against the model's real capability (design/96 S6). * Emitted once at task start when thinking is on, so a deployment can SEE — not silently swallow (§E * honesty red-line) — that a binary provider ignored the tier (`graded:false`) or that an effort endpoint * clamped it down (`clamped:true`). Metadata-only (tiers + format + endpoint, never prompt content). */ kind: "reasoning.resolved"; version: 1; taskId: string; /** The model whose endpoint resolved the intensity. */ model: string; /** The intensity the task asked for. */ requested: ThinkingLevel; /** The intensity actually in effect on the wire. */ effective: ThinkingLevel; /** False = binary enable-only / no-effort endpoint: the tier was NOT honored as a gradient (intent only). */ graded: boolean; /** True = `effective` differs from `requested` (the endpoint couldn't honor the request exactly). */ clamped: boolean; /** The wire format the request uses (`openai`/`deepseek`/`qwen`/`budget`/…) — the reason for graded/clamped. */ format: string; /** Coarse endpoint label (`model.api`, e.g. `openai-completions` / `anthropic-messages`). */ endpoint: string; ts: number; } | { kind: "task.end"; version: 1; taskId: string; status: TaskStatus; errorCode?: string; turns: number; tokens: number; costMicroUsd?: number; durationMs: number; /** True when the task finished with a structured output (`submit_output`); 1.41 observability. */ hasStructuredOutput?: boolean; ts: number; } | { kind: "turn.end"; version: 1; taskId: string; turn: number; ts: number; } | { kind: "brain.call"; version: 1; taskId: string; model: string; provider?: string; /** Normalized total input tokens (incl. cache). */ promptTokens: number; completionTokens: number; cacheRead: number; cacheWrite: number; /** Wall time from the brain request to its final message. */ latencyMs: number; /** Time to the first content delta (excludes thinking) — maps to OTel time_to_first_chunk (×1000). */ firstTokenMs?: number; costMicroUsd?: number; /** design/130 P1: the wall-clock-derived per-call output cap applied to THIS call (absent = no shrink). */ callCap?: number; /** design/130 P1 (codex ①): cap landed in [1024,2047] with budget-thinking requested → thinking skipped. */ capThinkingSkipped?: boolean; /** TB telemetry B1 (service [397]): the NORMALIZED finish reason of this call (`StopReason`: * `"stop"`/`"length"`/`"toolUse"`/`"error"`/`"aborted"`). Post-normalization (Anthropic * `end_turn`/`stop_sequence`→`"stop"`, `max_tokens`→`"length"`). Judge `callCap` binding by * `stopReason==="length" && callCap !== undefined`. */ stopReason?: string; ts: number; } | { kind: "tool.call"; version: 1; taskId: string; name: string; durationMs: number; ok: boolean; effect?: ToolEffect; ts: number; } | { /** Degenerate-repetition detector telemetry (clay 2026-07-10, 2e1c161 observability): a model * stream was CUT for a degenerate loop, or a repetition landed in a detection window but was * SPARED by a structural allowance (code-line shape / divider run). One event per detector hit. */ kind: "repetition.detected"; version: 1; taskId: string; turn: number; action: "cut" | "spared"; rule: "char-run" | "unit-loop"; /** Repeating-unit length in chars (1 for a char run). */ period: number; /** How many times the unit repeated. */ reps: number; /** ≤120-char sample of the repeated unit/tail. */ segment: string; ts: number; } | { /** A task was **degraded** to a cheaper model (rate_limit / breaker-open / near-budget, plus * design/126 server_error / last_resort on the chain form). An alert-worthy operational event — * quality dropped. Emitted once, when first degraded. */ kind: "task.degraded"; version: 1; taskId: string; from: string; to: string; reason: "breaker_open" | "rate_limit" | "budget" | "server_error" | "last_resort"; atTurn: number; ts: number; } | { /** C1 — the failover brain served this call from a FALLBACK entry (`createFailoverBrain`): the * primary (and possibly earlier hops) failed cleanly upfront. Without this, same-model gateway * swaps are indistinguishable in `brain.call` (the model id doesn't change). */ kind: "brain.failover"; version: 1; taskId: string; /** Index of the brain that SERVED the call (0 = primary; ≥1 = a fallback hop). */ servedIndex: number; /** Chain length. */ total: number; /** Canonical `BrainErrorCode` of the LAST failed hop (the reason the switch happened), if coded. */ errorCode?: string; ts: number; } | { /** C5 — a circuit breaker changed phase (trip / half-open probe window / close). The breaker * previously mutated state with zero emit; only the fast-fail's generic error text was visible. */ kind: "breaker.transition"; version: 1; taskId: string; /** Breaker key (default `:`). */ key: string; phase: "open" | "half-open" | "closed"; /** Consecutive transient failures at the transition. */ failures: number; ts: number; } | { /** C6 — the connect/mid-stream retry loop retried a provider call. The DISCARDED attempt's cost is * UNKNOWABLE by construction (the usage frame never arrived — that's why the attempt failed), so * this reports the attempt count honestly instead of a fabricated cost figure. */ kind: "brain.retry"; version: 1; taskId: string; /** 1-based attempt number that was ABANDONED (the retry that follows is attempt+1). */ attempt: number; phase: "connect" | "midstream"; ts: number; } | { /** C2 — the agent loop drove one of its self-heal recoveries (malformed-tool retry / thinking-only * retry / truncated continue / degenerate cut / mid-stream partial continue / reactive compact / * walltime cutoff write-out). Previously test-only (LoopTraceSink was never wired in production); * the only artifact was a display:false nudge message. */ kind: "loop.recovery"; version: 1; taskId: string; reason: string; ts: number; } | { /** C3 — within-task compaction WANTED to fire (over threshold) but the anti-thrash floor * (design/64 §25.2 `minTokens`) suppressed it. Repeated suppression = the run is drifting into * the request-trim / prefix-cache-collapse band with zero signal (the success-only onCompaction * never fires on this path). */ kind: "compaction.suppressed"; version: 1; taskId: string; /** Estimated context tokens at the suppressed boundary. */ estTokens: number; /** The anti-thrash floor that suppressed the pass. */ floor: number; ts: number; } | { /** design/134 复审 — a preCompact callback BLOCKED a compaction at a turn boundary (auto/manual * triggers only; "forced" ignores the block). Deliberate hook decision, NOT a failure: the * breaker is neither fed nor reset by it. This frame is what tells a blocked boundary apart * from a structural no-op (under threshold / no cut point). */ kind: "compaction.blocked"; version: 1; taskId: string; /** The trigger the blocked pass ran under (never "forced" — forced ignores blocks). */ trigger: "auto" | "manual"; ts: number; } | { /** MF-18 修① [496]③ — a compaction pass FAILED: either a burned summary attempt threw * (summarizer error / empty summary / oversized-summary guard; counted toward the §17.4 * breaker) or a manual /compact was drained against an ALREADY-OPEN breaker (no attempt). * Before this frame the only evidence was `Runner.onError(phase:"compaction")` — a service * that acked the manual verb (202) but saw no `compacted` event had ZERO stream/trace signal * about why (the [496]③ fingerprint). */ kind: "compaction.failed"; version: 1; taskId: string; /** The trigger of the failed pass. */ trigger: "auto" | "manual" | "forced"; /** Engine-authored failure detail (CompactionError / guard message), clipped. */ reason: string; ts: number; } | { /** MF-18 修① [496]③ — a manual /compact request was PROCESSED but MOOTED: the loop is dying * (abort fired / durable suspend / plan review in flight) so no compaction can run. The * compact() promise resolves "mooted"; this frame is the trace-side counterpart. */ kind: "compaction.mooted"; version: 1; taskId: string; reason: "task_ending"; ts: number; } | { /** MF-18 修① [496]③ — a manual /compact attempt ran but found NOTHING to compact (no valid * cut point / empty history): `maybeCompact` returned a structural `{compacted:false}` on a * forced pass. Manual-only by design — an auto pass no-ops at nearly every boundary (spam). */ kind: "compaction.noop"; version: 1; taskId: string; ts: number; } | { /** MF-18 修① [496]③ — a manual /compact was processed while compaction is DISABLED by the * task spec (`compaction.enabled:false`): the request can never be honored this run. */ kind: "compaction.disabled"; version: 1; taskId: string; ts: number; } | { /** MF-18 修② [496]③ — the summarization INPUT was truncated to fit the compaction model's * context window (the 300K-session shape that previously guaranteed a prompt-too-long throw * at every boundary). Fidelity disclosure: oldest `droppedChars` characters of the serialized * conversation were omitted from the summary prompt. */ kind: "compaction.input_truncated"; version: 1; taskId: string; /** Which summary call was truncated: the history summary or the split-turn prefix summary. */ label: "history" | "turn_prefix"; droppedChars: number; keptChars: number; ts: number; } | { /** MF-18 修③ [496]③ — a compaction landed at the run's FINAL boundary and the one-shot * post-compact announce latch (background-task snapshot, G1 [482]) was never consumed: the * next collected boundary it waits for never came. The announce is lost for THIS run (a * follow-up run on the same session re-arms it from the session tail — see the resume * re-arm seed in runtask). */ kind: "compaction.announce_dropped"; version: 1; taskId: string; ts: number; } | { /** C9 — the request-layer context guard TRIMMED messages out of a request view (design/123 * request-only trim; the session keeps them, but the model didn't see them this turn). */ kind: "context.trim"; version: 1; taskId: string; /** Messages dropped from THIS request view. */ dropped: number; ts: number; } | { /** C8 — the per-turn aggregate tool-result budget capped a result (offloaded to the store, or * degraded to a self-contained truncation preview when the store failed/absent). */ kind: "tool_result.capped"; version: 1; taskId: string; tool?: string; /** Chars of the full result before capping. */ sizeChars: number; /** True = store.put failed (or no store): the model lost read-back for this result. */ storeFallback: boolean; ts: number; } | { /** C4 — image blocks were replaced with text placeholders because the served model declares no * vision input (`ModelSpec.input` without "image"). Silent quality loss without this. */ kind: "vision.placeholder"; version: 1; taskId: string; /** Image blocks replaced in THIS request. */ count: number; ts: number; } | { /** C10 — a `preemptSignal` fired on a task that is NOT resource-suspend-eligible: the preempt is a * documented silent no-op, but the SCHEDULER that raised it needs to know it was ignored. */ kind: "preempt.ignored"; version: 1; taskId: string; ts: number; } | { /** C11 — a tool requested a human plan review (`ctx.requestReview()`) but the deployment cannot * honor it (no checkpoint store / loop already terminating): the request was consumed + DROPPED * (headless degrade). The HITL ask vanished with zero signal before this. NO free-text reason * field (codex MED: `requestReview({reason})` carries tool/model-authored text — the file-header * metadata-only rule forbids it here; the fact of the drop is the signal). */ kind: "review.dropped"; version: 1; taskId: string; ts: number; }; /** * Best-effort, fire-and-forget trace sink. Returns `void` by contract so a caller cannot synchronously * await it; the default (absent) hook is a no-op the JIT can elide. Must not throw or do sync I/O. */ export type TracerHook = (e: TraceEvent) => void; /** Invoke a tracer without ever letting it break the task (absent → no-op; throws swallowed). */ export declare function emitTrace(tracer: TracerHook | undefined, build: () => TraceEvent): void; //# sourceMappingURL=trace.d.ts.map