/**
 * 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.
 *
 * OTel GenAI semantic conventions: several fields below note the `gen_ai.*` attribute they feed. Those are
 * MAPPING NOTES for whoever writes the bridge, not a compatibility promise — the whole `gen_ai.*` namespace
 * is still Development-status upstream (no span/attribute marked Stable, and attribute names have already
 * been renamed once), so a bridge must pin the convention version it targets and expect upgrade breakage.
 * core's own contract is only the field names here; it deliberately stops at supplying the FACTS a bridge
 * needs (ids, turn indices, token counts) rather than adopting the convention's vocabulary internally.
 */
import type { BrainRetryErrClass, TaskStatus, ToolEffect } from "./types.js";
import type { ThinkingLevel } from "../internal/harness-types.js";
import type { ToolManifestRow } from "../prompt-assembly/tool-catalog.js";
import type { PromptEpochActivation, PromptIdentityLayers } from "../prompt-assembly/epoch.js";
import type { ChainLink, SupersededOverlay } from "../prompt-assembly/overlay.js";
import type { HostForm, HostRole, RendererId } from "../prompt-assembly/catalog/schema.js";
/** The run's prompt identity as the manifest reports it: the identity digest, its inputs, and the session record that
 *  names it (the record's counter and how it came to be). */
export interface PromptManifestIdentity {
    artifact: string;
    layers: PromptIdentityLayers;
    epoch: number;
    activatedBy: PromptEpochActivation;
}
/** The run's selector values — the plain form of the identity's `binding` element. */
export interface PromptManifestSelectors {
    binding?: string;
    traits: string[];
    host: {
        role: HostRole;
        form: HostForm;
    };
    facts: Record<string, boolean>;
}
/** One rendered section: where it sits, which variant and renderer made it, whose text it carries (`locale`: the
 *  catalog's canonical text, or text an overlay author wrote), its content digest (`sha256:` unsalted for published
 *  text, `salted:` for text carrying the task's or the run's own inputs) and its history (`chain`). */
export interface PromptManifestSection {
    id: string;
    band: string;
    rank: number;
    variant?: string;
    renderer?: {
        id: RendererId;
        version: number;
    };
    chars: number;
    locale: "canonical" | "overlay";
    contentHash: string;
    chain: ChainLink[];
}
/**
 * R4 (1.285): the scalar face of `TaskResult.stats.mechanisms`, echoed on `task.end` so "did the
 * engine machinery fire" is answerable from the trace alone. Counters/flags only —
 * `repetitionEvents` carries model-authored text samples and stays result-side (the file-header
 * metadata-only rule). Field meanings: see the `stats.mechanisms` JSDoc in core/types.ts.
 */
export interface TraceMechanismsSummary {
    finalVerifyInjected?: true;
    finalVerifyInjections?: number;
    attachmentsInjected?: number;
    /** design/164: how many LIMIT-APPROACH frames were injected this run (0..2). */
    approachNoticesSent?: number;
    repetitionCuts?: number;
    repetitionSpared?: number;
}
/**
 * design/168 (additive) — the RESOLVED deferred-tool disclosure strategy for one prepared leg, plus the
 * seat that chose it. `strategy` is the EFFECTIVE value (after the direct-call-lane interlock), and
 * `source` says where it came from: the task spec, the `SEMA_TOOL_MATERIALIZE_STRATEGY` env channel, the
 * engine default, or `"degraded_no_direct_lane"` — the interlock narrowed a `"static"` request to
 * `"swap"` because `deferSelfResolve: false` left the placeholder permanently uncallable. That last
 * value implies an ENV-sourced request: the spec pairing is refused at prepare and the default is not
 * `"static"`.
 *
 * Why it is reported at all: "which strategy did this leg actually run" was previously only answerable
 * by re-deriving spec > env > release default by hand — the archaeology the config manifest exists to
 * kill — and the two strategies differ in how the schema reaches the model, which is exactly what an
 * operator diagnosing a caller that cannot form tool arguments needs to know first.
 */
export interface ToolDisclosureManifest {
    /** How many tool names were deferred on this leg (0 ⇒ this section is absent entirely). */
    deferredTools: number;
    strategy: "swap" | "static";
    source: "spec" | "env" | "default" | "degraded_no_direct_lane";
}
export type TraceEvent = {
    kind: "task.start";
    version: 1;
    taskId: string;
    /** The ENGINE-minted identity of THIS run (`TaskResult.runId`). `taskId` is
     *  `spec.taskId ?? sessionId`, so two runs of one session that declare no host task id emit two
     *  task.start frames under ONE id and a consumer pairing start↔end has no way to say which end
     *  closes which start. Present on every `task.start` (the frame is emitted after prepare, which
     *  is where the id is minted).
     *
     *  PAIRING CONTRACT, stated in full because it is not symmetric: a `task.end` may carry a
     *  `runId` this frame never announced — a run whose PREPARE threw emits the end and no start
     *  (there was nothing to start), and that end still names its run because prepare-time
     *  disclosures already did. So: every start has an end; not every end has a start. A consumer
     *  building spans must treat an unmatched end as a run that never began, not as a lost start.
     *
     *  SCOPE, stated so it is not mistaken for more: only this frame and `task.end` carry the run
     *  axis today. Every other frame in this union still keys on `taskId` alone, so frames from two
     *  CONCURRENT runs that share a host `taskId` remain mutually indistinguishable inside their
     *  windows. Extending the axis across the union is a separate pass. */
    runId?: string;
    model: string;
    /** R4 (1.285): the engine build (package.json version, runtime-resolved) that produced this
     *  run — binds trace forensics to an exact build. "unknown" if resolution failed. */
    engineVersion?: string;
    ts: number;
} | {
    /**
     * The labelled composition of the assembled system prompt (manifest v3), emitted once right after `task.start`:
     * which prompt this is (the identity and its selector values), which blocks and sections composed it, and where
     * each section's text came from (its chain of overlays, the superseded ones marked). Metadata-only — ids, sizes
     * and digests, never prompt text (the file-header privacy rule).
     */
    kind: "prompt.assembled";
    version: 3;
    taskId: string;
    identity: PromptManifestIdentity;
    selectors: PromptManifestSelectors;
    /** Ordered blocks: stable id (e.g. `role.base`, `harness.context`, `memory.safety`, `env.context`,
     *  `memory.tail`), size in chars, salted short content hash — the block-view id wordlist is a downstream contract. */
    blocks: Array<{
        id: string;
        chars: number;
        hash: string;
    }>;
    sections: PromptManifestSection[];
    /** Every overlay the composition superseded (a later one on the same target won, or it changed nothing this
     *  run); absent when none was. */
    superseded?: SupersededOverlay[];
    /**
     * S2 (additive): the mounted tools plane — per-tool contract identity, durable-replay
     * aliases, presentation-invariant shape digest and full wire-schema digest (initial mount face;
     * deferred activations announce via tools_delta). Content-addressed digests (schemas are code).
     */
    tools?: ToolManifestRow[];
    totalChars: number;
    /** design/148 S3 (additive, §10.2): the nine-element cache identity — digests only. */
    snapshot?: {
        cacheIdentity: string;
        elements: import("../prompt-assembly/turn-snapshot.js").CacheIdentityElements;
    };
    /** design/148 S3 (additive, §10.1): the run's lowering record. */
    lowering?: {
        version: string;
        form: string;
        intentionalDivergences: readonly string[];
    };
    /** design/168 (additive): the resolved deferred-tool disclosure strategy + the seat that chose
     *  it. Absent when the leg deferred nothing. */
    toolDisclosure?: ToolDisclosureManifest;
    ts: number;
} | {
    /** design/148 S3 — a mid-run identity refresh (deferred materialization / RB-31
     *  adoption / finalize thinking flip): the trace stream must never keep claiming a superseded
     *  prefix identity. Digests only, never prompt text. */
    kind: "prompt.snapshot_changed";
    version: 1;
    taskId: string;
    snapshot: {
        cacheIdentity: string;
        elements: import("../prompt-assembly/turn-snapshot.js").CacheIdentityElements;
    };
    ts: number;
} | {
    /**
     * How a task's requested reasoning intensity RESOLVED against the model's real capability (design/96 S6).
     * Emitted 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`), that an effort endpoint
     * clamped it down (`clamped:true`), or that a NON-reasoning model dropped the request entirely
     * (`dropped:true` — the frame fires for that model too; it used to be the one arm with no report).
     * Metadata-only (tiers + format + endpoint, never prompt content). The resolution is against the
     * task's PRIMARY serving model at leg entry (same law as `TaskResult.model`): a mid-run
     * degradation does not re-emit this frame — the switch is observed on its own seats
     * (`TaskResult.degraded`).
     *
     * A SECOND frame follows, for the same taskId, in exactly one case: the leg's FIRST
     * committed request reported wire facts that CHANGE the resolution the task-start frame claimed
     * (today: the anthropic budget path's cap-wins skip, where a hard per-request output cap too small
     * to host a legal thinking budget deletes the thinking block — so the honest report is
     * `dropped:true`, not the gradient the entry frame guessed). The correction is deduped (a first
     * request that changes nothing emits nothing) and capped at one (only the FIRST committed request
     * is consumed, so the leg's nested internal calls — compaction summary, side query — cannot
     * rewrite the leg's posture). A consumer that keeps only the LATEST frame per taskId is therefore
     * always reading the truth, and one that kept only the first now under-reports a drop it could not
     * have seen before. The result-face twin `TaskResult.effectiveReasoning` moves with it — the two
     * faces are one mint at every instant.
     */
    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;
    /**
     * Present (true) only when the model declares NO reasoning capability (`Model.reasoning` falsy): the
     * brains send no thinking parameter at all, so the requested tier was DROPPED — not clamped.
     * `effective:"off"` states the ENGINE side (nothing was requested), not a measured gateway state:
     * on the binary enable-only formats (qwen/zai/qwen-chat-template) parameter absence is
     * provider-default and a default-on gateway may still reason at its own tier — read `format` to
     * know which family applies. `format`/`endpoint` report the family that WOULD have carried the
     * tier. Absent on every reasoning-capable resolution (old consumers see identical frames).
     */
    dropped?: true;
    /** design/148 S3 (additive, §10.2): the nine-element cache identity — digests only. */
    snapshot?: {
        cacheIdentity: string;
        elements: Record<string, string>;
    };
    /** design/148 S3 (additive, §10.1): the run's lowering record. */
    lowering?: {
        version: string;
        form: string;
        intentionalDivergences: readonly string[];
    };
    ts: number;
} | {
    /**
     * Config-catalog manifest (ruled 2026-07-19) — the run's EFFECTIVE task-launch config
     * with per-field provenance (default / spec / derived / host-declared), emitted once right
     * after `prompt.assembled`. Kills the "which layer set this knob" archaeology: every run
     * self-reports its true scale, and host-layer overrides arrive as DECLARED entries
     * (`TaskSpec.configOverrides`, reason required) instead of invisible env/adapter state.
     */
    kind: "config.assembled";
    version: 1;
    taskId: string;
    catalogVersion: string;
    fields: Array<{
        key: string;
        value: number | boolean | string | null;
        provenance: "default" | "spec" | "derived" | "host-declared";
    }>;
    /** Reasons for host-declared entries, keyed by catalog key (the announcement face). */
    overrideReasons?: Record<string, string>;
    ts: number;
} | {
    /**
     * RB-371 ③ — an `additionalDirectories` entry could not be canonicalized at prepare (missing
     * directory / dangling symlink / env error) and was SKIPPED fail-closed: the run proceeds with
     * a NARROWER allow-root set than the spec asked for. Previously only the side-band
     * `onError(phase:"config")` callback saw this; a deployment without an onError sink got a
     * silently narrowed fence (the # Environment block advertises only the EFFECTIVE roots, so the
     * model never sees the asked-for entry either). One frame per skipped entry.
     */
    kind: "config.additional_directory_skipped";
    version: 1;
    taskId: string;
    /** Which spec field the skipped entry came from. Absent = `additionalDirectories` (the
     *  original face, kept implicit for existing readers); `"additionalReadDirectories"` = the
     *  read-only whitelist arm (ruled 2026-08-05); `"persistedReadRule"` = a person's persisted `Read(//dir/**)` allow row. */
    field?: "additionalReadDirectories" | "persistedReadRule";
    /** The spec entry as written (raw, not canonical — canonicalization is what failed). */
    entry: string;
    /** Why the entry was skipped, `code: message` form: the canonicalization failure, or (a persisted row only)
     *  `admits_artifact_store` — the in-flight grant's store rule. */
    reason: string;
    ts: number;
} | {
    kind: "task.end";
    version: 1;
    taskId: string;
    /** The run this terminal closes; pairs with the `task.start` frame's `runId`
     *  (see that field for why `taskId` cannot carry the pairing, and for the ASYMMETRY: this frame
     *  is also emitted on the prepare-throw backstop, where no `task.start` exists — it still names
     *  its run, because the id is minted at prepare's first statement and prepare-time disclosures
     *  already carry it). Absent only where prepare was never entered at all, which is the same
     *  condition its `taskId: "unknown"` states. */
    runId?: string;
    status: TaskStatus;
    errorCode?: string;
    turns: number;
    tokens: number;
    /** ABSENT when any spend was unpriced (RB-368: no pricing entry + no `Model.cost` — no
     *  fabricated 0; matches `stats.costMicroUsd`) and on the prepare/finish-throw backstop frame. */
    costMicroUsd?: number;
    durationMs: number;
    /** True when the task finished with a structured output (`submit_output`); 1.41 observability. */
    hasStructuredOutput?: boolean;
    /** R4 (1.285): scalar echo of `stats.mechanisms` (absent when no deadline mechanism engaged —
     *  byte-compatible with pre-1.285 frames). */
    mechanisms?: TraceMechanismsSummary;
    /** 1.296 件2a (TB2.0 run89): how LATE the hard walltime-abort timer fired vs its scheduled
     *  time, when materially late (> 5s; absent otherwise, and absent when it never fired). A
     *  large value is EVIDENCE THE ENGINE WAS NOT AT FAULT — setTimeout only runs when the event
     *  loop breathes, so lateness this size means the environment starved the process (run89: an
     *  abandoned compile group froze the loop 646s; the 1200s hard abort landed 683s late). */
    timerLatenessMs?: number;
    /** The run was stopped by the SYNCHRONOUS turn-boundary walltime
     *  backstop, not the hard-abort timer — i.e. the deadline had passed but the setTimeout leg
     *  had not fired yet (starved event loop). Pairs with `timerLatenessMs` for attribution. */
    walltimeSyncBackstop?: 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;
    /**
     * 1-based index of the turn this provider call served, same numbering as `turn.end` and as
     * `tool.call.turn`. Lets a bridge put a `chat` span and the `execute_tool` spans of the tools that
     * call requested under one parent without re-deriving turn boundaries from event order.
     *
     * ABSENT on the compaction summarizer's own call: that call fires from the turn-boundary hook and
     * is not a loop turn, so any number here would be a different measurement wearing the same name.
     */
    turn?: number;
    /** Normalized prompt tokens that MISSED the cache (Anthropic-protocol `input_tokens`
     *  shape) — disjoint from `cacheRead`/`cacheWrite`. Carried the cache-inclusive total up to 2.13.x. */
    promptTokens: number;
    /** The cache-INCLUSIVE prompt total for this call (`promptTokens + cacheRead +
     *  cacheWrite`) — the figure `costMicroUsd` is computed from. */
    totalInputTokens: 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;
    /** 1.296 件2b (TB2.0 run89 归因盲区): epoch ms at which the ENGINE ISSUED this provider call
     *  (the per-call closure evaluation immediately before the stream call — unlike `latencyMs`,
     *  which anchors on the FIRST DELTA and reads 0 for a zero-delta call). Distinguishes an
     *  in-flight call cut before its first delta (present, latencyMs 0, stopReason "aborted")
     *  from a call that was never issued (absent). Compaction-summarizer rows never carry it (they
     *  circumvent the loop's per-call closures). */
    callStartedAt?: number;
    /** ABSENT when THIS call's serving model was unpriced (RB-368 — no fabricated 0). */
    costMicroUsd?: number;
    /** TB telemetry B1: the NORMALIZED finish reason of this call (`StopReason`:
     *  `"stop"`/`"length"`/`"toolUse"`/`"error"`/`"aborted"`). Post-normalization (Anthropic
     *  `end_turn`/`stop_sequence`→`"stop"`, `max_tokens`→`"length"`). */
    stopReason?: string;
    ts: number;
} | {
    kind: "tool.call";
    version: 1;
    taskId: string;
    name: string;
    /**
     * The resolved call's id: the SAME value the wire's `tool_start`/`tool_end` `TaskEvent`
     * carries for this call. OTel GenAI names it `gen_ai.tool.call.id` on the `execute_tool` span.
     *
     * Why it matters: without it the trace face and the event face could only be paired by tool NAME
     * plus timestamp, which is ambiguous exactly where it hurts — two same-named calls inside one
     * parallel batch. Absent only on frames from a build that predates this field.
     */
    toolCallId?: string;
    /**
     * 1-based index of the turn this call ran in, on the same numbering `turn.end` reports
     * (a `tool.call` is emitted BEFORE the `turn.end` that closes its turn, so the two agree without
     * look-ahead). No GenAI attribute corresponds; it is the PARENT-span correlation key a bridge
     * needs to nest an `execute_tool` span under the `chat` call that requested the tool.
     */
    turn?: number;
    durationMs: number;
    ok: boolean;
    effect?: ToolEffect;
    ts: number;
} | {
    /** Degenerate-repetition detector telemetry (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;
} | {
    /** RB-473 batch 2b — a HOST observer callback threw inside a safe-notify isolation scope
     *  (bounded: first failure per site per scope). The engine is unaffected (the swallow is the
     *  contract); this event is the disclosure so a deployment learns its observer plumbing broke.
     *  `site` is the owner-chosen literal (e.g. "toolGate.onHookError"). */
    kind: "observer.notify_failed";
    version: 1;
    taskId: string;
    site: string;
    message: string;
    ts: number;
} | {
    /**
     * design/179 — a persisted allow-rule COVERAGE resolved an ask, so no person was asked and no
     * checkpoint was minted. The gate has an attribution channel for the deny side and had none for
     * the allow side; this is that channel, because "why did this run without asking me?" must be
     * answerable. Metadata only (each rule text is the canonical rule spelling, never the command's
     * arguments). version 2 (design/375, BREAKING): `rule: string` became `rules: string[]` — a
     * compound may now be admitted by the CONJUNCTION of per-segment rules, and the attribution names
     * every rule the decision stood on (a single-rule resolution is a one-element array).
     */
    kind: "permission.persisted_rule_allowed";
    version: 2;
    taskId: string;
    toolName: string;
    toolCallId: string;
    rules: readonly string[];
    ts: number;
} | {
    /**
     * The READ-ONLY reader cleared a shell call, so no person and no classifier was asked: the
     * allow layer's second attribution channel beside `permission.persisted_rule_allowed`. `command` is
     * the FINAL command the gate judged (a policy rewrite included) — model-authored text, carried
     * verbatim as the audit fact this frame exists for (a consumer rendering it applies its own
     * display sanitizer, as it does for `tool_start` args).
     */
    kind: "permission.read_only_allowed";
    version: 1;
    taskId: string;
    toolName: string;
    toolCallId: string;
    command: string;
    ts: number;
} | {
    /**
     * An active skill's `allowed-tools` row cleared the call, so no person and no classifier was asked: the allow
     * layer's third attribution channel beside `permission.persisted_rule_allowed` and `permission.read_only_allowed`.
     * `skill` names the skill whose list spoke and `entries` the rows the call stands on, as the skill spells them
     * (deployment text; a consumer rendering it applies its own display sanitizer).
     */
    kind: "permission.skill_allowed";
    version: 1;
    taskId: string;
    toolName: string;
    toolCallId: string;
    skill: string;
    entries: string[];
    ts: number;
} | {
    /**
     * An entry of the deployment's `RunnerDeps.allowedTools` (`source: "deployment"`) or of this run's
     * `TaskSpec.allowedTools` (`source: "request"`) cleared the call, so no person and no classifier was asked: the
     * allow layer's attribution channel for its two entry-list members, beside `permission.skill_allowed`. `entries` are
     * the entries the call stands on, as the list spells them (deployment or caller text; a consumer rendering it applies
     * its own display sanitizer).
     */
    kind: "permission.allowed_tools_allowed";
    version: 1;
    taskId: string;
    toolName: string;
    toolCallId: string;
    source: "deployment" | "request";
    entries: string[];
    ts: number;
} | {
    /**
     * design/179 — the person's persisted rule store could not be read (at prepare, or for one call), so
     * that read's rows took no part: a call it served was judged without them — and, at a call, asks a person (the
     * rows could hold a deny). The operator's per-read record; the person hears it once per run
     * (`permission.rule_store_unavailable`).
     */
    kind: "permission.rule_store_unreadable";
    version: 1;
    taskId: string;
    message: string;
    ts: number;
} | {
    /**
     * design/182 §4.6 — a cloud-sync round moved a (rule, scope) from REMOVED back to LIVE: another
     * replica's add outlived the local tombstone (add-wins is the locked merge semantic, so the
     * transition itself is not news — its VISIBILITY is). The person who deleted the rule has to be
     * able to learn that it came back, and to delete it again. Metadata only: the canonical rule
     * text and the scope KIND — a project scope's root path stays out of the trace.
     */
    kind: "permission.rule_sync_resurrected";
    version: 1;
    /** The bucket this round synced. A local-owner bucket cannot sync, so this is always a principal. */
    principal: string;
    rule: string;
    /** `"session"` is unreachable here in a healthy fleet (design/382 §4.3: a session row never
     *  lands, so it can never resurrect) — declared because the sibling drop event names it and a
     *  lockstep union is what keeps the two from drifting. */
    scopeKind: "global" | "project" | "session";
    ts: number;
} | {
    /**
     * design/182 §4.3/§5.2 — a cloud-sync round REFUSED an inbound row, or quarantined a local one.
     * The reason is a closed code from the governance-codes family (never free text): the machine
     * reads the table. One event per dropped row — a silently refused row is indistinguishable from
     * a row that never existed, which is the failure this event exists to prevent.
     */
    kind: "permission.rule_sync_dropped";
    version: 1;
    principal: string;
    rule: string;
    /** `"session"` names the design/382 §4.3 durable-face drop: a peer sent a session-scope row,
     *  refused per-row with `session_scope_not_durable`. */
    scopeKind: "global" | "project" | "session";
    /** Closed reason code ({@link import("./governance-codes.js").RuleSyncDropReason}). */
    reason: string;
    ts: number;
} | {
    /**
     * design/182 §7.4 — an org-governed deployment could NOT adjudicate against an org snapshot
     * (never installed, provider failed past the staleness bound, or the persisted state was
     * refused). The gate answered by failing the WHOLE decision boundary closed: every terminal
     * allow became a real-approval ask. This is the operator-facing half of that tighten — the
     * deployment is paying the "ask about everything" price and has to be able to see why.
     */
    kind: "permission.org_snapshot_unavailable";
    version: 1;
    taskId: string;
    toolName: string;
    toolCallId: string;
    /** The overlay's own disclosure lines (deployment-authored/engine-authored, never model text),
     *  joined — provider failure, refused snapshot, rollback refusal, missing durable persistence. */
    message: string;
    ts: number;
} | {
    /**
     * F-012 L2 — a pending ask was AUTO-ADMITTED by the sandbox-admission leg: the deployment's
     * execution env declares an isolated sandbox, every surviving ask on the call was
     * engine-classified `sandbox_local`, and the call crosses no declared boundary. This record is
     * the durable admission ledger's operator half (the transcript half rides the tool result's
     * system-reminder context); a consumer anchors on THIS kind, never on wording.
     */
    kind: "permission.sandbox_admitted";
    version: 1;
    taskId: string;
    toolName: string;
    toolCallId: string;
    /** The engine ask classes of the admitted call's surviving asks (all `sandbox_local` by the
     *  admission predicate; carried explicitly so the ledger states what was judged, not just that
     *  judgment passed). */
    askClasses: readonly string[];
    /** Which layers raised the admitted asks (`caller_policy` / `ancestor_constraint` /
     *  `gate_safety_tighten`). */
    sourceLayers: readonly string[];
    /** The boundary classification the admission asserted. */
    boundary: "sandbox_internal";
    ts: number;
} | {
    /**
     * The auto-mode classifier decided (or declined to decide) one pending ask — one frame per
     * `decide` call, so a consumer can read the gate's wall-clock cost per tool call and the failure
     * cause behind a call the classifier could not evaluate. A TRACE frame rather than a task event: it
     * fires once per gated call and it is an operator's read (why did that ask take four seconds / why
     * was that call denied). `ms` is the wall time from the
     * decide call to its verdict (timeout included: a timed-out round reads the deadline).
     */
    kind: "auto_mode.classified";
    version: 1;
    taskId: string;
    /** The gated call the classifier judged. */
    toolCallId: string;
    /** The classifier model (the resolved `classifier` role, or the main model it fell back to). */
    model: string;
    /** Wall-clock milliseconds the gate waited on this decision (integer, ≥ 0). */
    ms: number;
    /** The verdict kind the gate acted on (`allow` / `block` / `unavailable` / `parse_error`). */
    verdict: "allow" | "block" | "unavailable" | "parse_error";
    /** Present iff `verdict === "unavailable"`: `error` / `timeout`. */
    cause?: "error" | "timeout";
    /** Present exactly when NO MODEL WAS ASKED because the question did not assemble: `timeout` = the
     *  assembly outran its cap, `failed` = it threw. The verdict reads `unavailable` / `error` either way
     *  (nothing was dispatched), and `ms` is then time spent BUILDING, not waiting on a provider — which is
     *  what separates "the transcript store is slow" from "the provider is down". */
    notAsked?: "failed" | "timeout";
    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 `<provider>:<modelId>`). */
    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";
    /** WHY the attempt is being retried, as the closed neutral bucket the user-facing status frame
     *  also carries ({@link BrainRetryErrClass}) — an operator reading this and a user reading a
     *  progress line then agree on the reason. Optional: absent on frames minted before it existed. */
    errClass?: BrainRetryErrClass;
    /** The backoff about to be slept, ms — 0 for an immediate re-send. Together with `attempt` this
     *  makes a whole retry chain reconstructible from the trace alone, which is what a caller that saw
     *  only silence needs to explain where the time went. */
    nextDelayMs?: number;
    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;
} | {
    /** The auto-compaction trigger could not be EVALUATED: the serving model declares no usable
     *  context window, so the predicate refuses on the unknown-window sentinel and there is no
     *  threshold to report. Distinct from a healthy under-threshold pass, which is deliberately
     *  silent — this one means auto-compaction is inert for the whole run. Emitted once per task
     *  (the state is configuration, not an event); the host also gets one `phase: "config"` error. */
    kind: "compaction.unevaluable";
    version: 1;
    taskId: string;
    /** What the pass could still estimate (structural + prompt overhead) despite the unknown window. */
    estTokens: 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 修① — 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 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 修① — 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.
     *  `reason:"cancelled"` (additive) — the caller withdrew an IN-FLIGHT request via
     *  `compact({signal})` and the pass's summary call was aborted mid-air. A caller cancel is an
     *  external cause, not summarizer evidence: never counted toward the consecutive-failure
     *  breaker, never fired through `onError` (a deliberate withdrawal is not an engine error).
     *  A cancel that lands while the request is still PARKED resolves the caller silently (no
     *  frame — no boundary ever saw the request, so there is no pass to account for). */
    kind: "compaction.mooted";
    version: 1;
    taskId: string;
    reason: "task_ending" | "cancelled";
    ts: number;
} | {
    /** MF-18 修① — 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 修① — 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;
} | {
    /** design/145 §2 — static config early-warning, emitted once at task start when the configured
     *  compaction model's window is SMALLER than the main model's: from `fallbackAt` estimated
     *  context tokens on, every boundary's summary call will either clamp-truncate its input or
     *  fall back to the main model. A deployment shell surfaces this as a startup hint. */
    kind: "compaction.window_config_warning";
    version: 1;
    taskId: string;
    compactionModelWindow: number;
    mainModelWindow: number;
    /** ≈ headroom/(1−clampTolerance), in the compaction model's structural-token coordinate
     *  (estimate). Absent when `clampTolerance ≥ 1` (fallback disabled — clamp-only behavior). */
    fallbackAt?: number;
    ts: number;
} | {
    /** design/145 §3 — a compaction pass's summary call FELL BACK from the independent compaction
     *  model to the MAIN model: the dry-run clamp showed the conversation would lose more than
     *  `clampTolerance` of its chars in the small window (and the budget/walltime gates allowed
     *  the more expensive main-model call). The `compacted` wire event carries the additive
     *  `modelFallback`/`fallbackReason` mirror. */
    kind: "compaction.model_fallback";
    version: 1;
    taskId: string;
    /** Serialized conversation size, MAIN-model structural tokens. */
    contentTokens: number;
    /** ≈ what fits in the compaction model's window (its structural tokens; estimate). */
    headroomTokens: number;
    truncationRatio: number;
    /** 门A estimate (contentTokens × main input price); absent when pricing was unavailable. */
    estCostMicroUsd?: number;
    ts: number;
} | {
    /** design/145 §3 — a summary input WAS clamp-truncated on the small compaction model and the
     *  loss is hereby DISCLOSED (the pre-145 silent mid-section drop): reason "budget"/"walltime"
     *  = the fallback was denied by that gate; "tolerance" = within `clampTolerance` (bounded).
     *  Fires pre-call (survives a later summary failure). */
    kind: "compaction.clamp_disclosure";
    version: 1;
    taskId: string;
    truncationRatio: number;
    reason: "budget" | "walltime" | "tolerance";
    estCostMicroUsd?: number;
    ts: number;
} | {
    /** design/145 门B-3 — a manual /compact was honored INSIDE the graceful-finalize window
     *  (user sovereignty over engine throttling), but the summary call will consume write-out
     *  time; the soft summary deadline (wall − cushion) still bounds it. Warn-grade. */
    kind: "compaction.manual_in_finalize";
    version: 1;
    taskId: string;
    ts: number;
} | {
    /** MF-18 修② — 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;
} | {
    /** 压缩子阶段遥测 (1.283 批 G 观察项 / design/145 §6 正交后续批) — sub-phase wall-time split of
     *  ONE landed compaction pass, mirroring `maybeCompact().phaseDurations` 1:1 so a TB "整段 ~55s
     *  停顿" reading can be attributed to prepare (context build/cut-point/gates) vs the summary LLM
     *  call vs persist (attachments/session rewrite/post measurement). Invariant: prepareMs +
     *  (summaryMs ?? 0) + persistMs === durationMs (exact tiling, shared end mark).
     *
     *  EMITTED by the runtask bridge (F 批, follow-up to the frame-definition batch): each of the
     *  three `compacted`-event bridge points (boundary/manual, PTL-recovery, end-of-task finish)
     *  reads `comp.phaseDurations` off the maybeCompact result (next to the
     *  `...(comp.durationMs !== undefined …)` passthrough — which now also forwards `phaseDurations`
     *  onto the wire event, additive) and emits this frame right beside the event.
     *  Metadata-only per the file-header rule (durations + a retry counter, no content). */
    kind: "compaction.phase_timings";
    version: 1;
    taskId: string;
    /** Context build → trigger math → cut-point selection → hooks/provider consult → dry-run/gates. */
    prepareMs: number;
    /** The summary LLM leg (serialization → clamp → completion, incl. PTL retries). ABSENT on a
     *  provider-reuse pass — no summary call happened. */
    summaryMs?: number;
    /** Working-file attachments → session rewrite (appendCompaction) → post measurement → observers. */
    persistMs: number;
    /** Reactive prompt-too-long retries burned inside this pass's summary call(s). */
    ptlRetries: number;
    /** Whole-pass wall time — equals the `compacted` wire event's `durationMs`. */
    durationMs: number;
    ts: number;
} | {
    /** MF-18 修③ — a compaction landed at the run's FINAL boundary and the one-shot
     *  post-compact announce latch (background-task snapshot, G1) 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;
} | {
    /** design/374 (G11) — the microCompact clearing machine FIRED: `clearedCount` tool-result
     *  occurrences were replaced with markers in one pass. `trigger` names the arm: `"frontier"`
     *  = the proactive request-build pass (anchored estimate crossed the edit budget),
     *  `"refusal"` = the MC-R rejection-recovery arm (provider said input-too-long), `"blocking"`
     *  = the slice-3 guard-chain arm A (the machine's one pre-guard shot when the frontier pass
     *  is off, `microCompact.machine: "off"`). The `"frontier"` arm requires
     *  `microCompact.machine: "cc"` (the default), the `"refusal"` arm requires
     *  `microCompact.clearOnRejection` (also default-on). Cardinality: exactly ONE frame per firing pass — a retry
     *  chain re-sending an already-cleared view emits none. A `"refusal"` frame also marks one
     *  attempt of the shared prompt-too-long recovery budget spent (the MC-R arm's retry rides
     *  the same per-chain account the forced-compaction arm draws on — the knob doc on
     *  `RunnerDeps.microCompact` carries the full accounting). */
    kind: "context.mc_clear";
    version: 1;
    taskId: string;
    clearedCount: number;
    /** The pass's structural savings estimate (the ≥20k gate's own number). */
    tokensSavedEstimate: number;
    trigger: "frontier" | "refusal" | "blocking";
    ts: number;
} | {
    /** design/374 (G11) — the MC-R rejection arm ran and DECLINED to clear: `reason` says why
     *  (`"no_candidates"` = nothing clearable beyond the keep window on the rejected projection;
     *  `"below_min_savings"` = clearable but under the 20k gate), `nextArm` names where the
     *  recovery chain goes instead (`"forced_compaction"` when the compaction arm is available,
     *  `"none"` when it is disabled/tripped — the turn will surface the provider error). Emitted
     *  only on the refusal arm; the frontier pass declines silently every request by design. */
    kind: "context.mc_null";
    version: 1;
    taskId: string;
    reason: "no_candidates" | "below_min_savings";
    nextArm: "forced_compaction" | "none";
    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;
} | {
    /** A provider echo field was refused by {@link import("../brain/errors.js").PROVIDER_ECHO_SHAPE} (visible ASCII,
     *  1–128 characters) and dropped: the response's `request-id` / `x-request-id` header values (`field:
     *  "requestId"`, read at connect — the id a failure would carry as `apiFailure.requestId`), the model name
     *  its frames report (`field: "responseModel"`), or the word a refusal's error envelope states (`field:
     *  "apiErrorCode"`). One frame per provider response per field; the value itself is never carried
     *  (metadata-only). A count of these frames is this engine's drop count. */
    kind: "brain.echo_dropped";
    version: 1;
    taskId: string;
    field: "requestId" | "responseModel" | "apiErrorCode";
    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 (`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;
} | {
    /** The deployment `RunnerDeps.onError` sink ITSELF failed — threw synchronously, or (an async
     *  sink) returned a promise that rejected. The failure is swallowed at the Runner's chokepoint
     *  wrap by contract (a broken observer never breaks the run), so this frame is the only evidence
     *  a persistently broken sink leaves. No `taskId`: the sink context is session-scoped. */
    kind: "task.onerror_sink_failed";
    version: 1;
    /** Session of the original `onError` invocation whose sink failed. */
    sessionId: string;
    /** Phase of the original `onError` invocation (e.g. "compaction", "config", "memory"). */
    phase: string;
    /** Sink-authored failure detail (`Error.message` / `String(value)`), clipped to 256 chars —
     *  deployment-code-authored, same posture as `compaction.failed.reason` (never model text). */
    reason: 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;
