import type { CodeIdentity } from "./runDirectory/codeIdentity.js"; import type { AliasCorrection } from "./utils/parameterAliases.js"; import { ModelName } from "smoltalk"; import { JSONEdge } from "./types.js"; import type { DecisionLog } from "./statelog/wireTypes.js"; import type { ProviderUsageKind } from "./runtime/invocationUsage.js"; import type { GlobalStore } from "./runtime/state/globalStore.js"; export declare const STATELOG_FORMAT_VERSION = 1; /** Max chars of a prompt/input kept in a statelog preview field, so a * transcript-with-PII never ends up in a remote sink. */ export declare const PROMPT_PREVIEW_MAX = 200; export type SpanType = "agentRun" | "nodeExecution" | "llmCall" | "toolExecution" | "threadEndHooks" | "forkAll" | "race" | "handlerChain" | "abortUnwind" | "embedding" | "decisionBatch" | "memoryRemember" | "memoryRecall" | "memoryForget" | "memoryCompaction" | "subprocessRun"; export type SpanContext = { spanId: string; parentSpanId: string | null; spanType: SpanType; startTime: number; }; export type RunMetadata = { tags?: string[]; environment?: string; userId?: string; agentVersion?: string; custom?: Record; }; export type StatelogConfig = { host: string; traceId?: string; apiKey: string; projectId: string; debugMode: boolean; metadata?: RunMetadata; /** Which code this process is running; recorded on agentStart so a trace can * be attributed to a version after the fact. */ code?: CodeIdentity; observability?: boolean; logFile?: string; /** * Per-request timeout (milliseconds) applied to the remote http POST. * Prevents a slow/unreachable statelog host from wedging the agent's * end-of-run cleanup. Default: 1500ms. */ requestTimeoutMs?: number; }; /** What the interrupt events say about an interrupt, so a log reader sees * what was raised without finding another event for it. */ export type InterruptSummary = { effect: string; message: string; data: any; }; /** One interrupt a checkpoint was taken for, as `checkpointCreated` logs it. */ export type CheckpointInterrupt = InterruptSummary & { interruptId: string; origin: string; /** Set when the debugger raised it to stop the run. */ debugger?: boolean; }; export type TokenUsage = { inputTokens?: number; outputTokens?: number; cachedInputTokens?: number; cacheCreationInputTokens?: number; totalTokens?: number; }; export type TokenCost = { inputCost?: number; outputCost?: number; cachedInputCost?: number; cacheCreationInputCost?: number; totalCost?: number; }; export declare class StatelogClient { private host; private debugMode; private traceId; private apiKey; private projectId; /** Appends one line to the configured log file, synchronously, so a test * can read the file right after an awaited event. Null without a file. */ private logFileSink; private enabled; private remoteEnabled; private rootStack; private metadata?; private code?; private requestTimeoutMs; private fallbackGlobals; private boundStack; private boundGlobals; constructor(config: StatelogConfig); private currentStack; /** * The logger for a run: this client, bound to one branch's tag store and * span stack. Every run in a branch shares one, as `run.log`. Posts made * through it redact with that branch's tags and nest under that branch's * spans, wherever the call is made from. * * `logFor` binds the span stack of the logger it is called on. Use it * where a run is made at the root of a branch that already exists. */ logFor(globals: GlobalStore): StatelogClient; /** The logger for a new branch, with the branch's own span stack. */ forBranch(globals: GlobalStore, spans: SpanContext[]): StatelogClient; snapshotStack(): SpanContext[]; runInBranchContext(parentStack: SpanContext[], fn: (spans: SpanContext[]) => Promise): Promise; /** Open a span. Only `reopenSpan` passes an `id`; everyone else gets a * random one. */ startSpan(type: SpanType, options?: { id?: string; }): string | undefined; /** Open the span saved in the slot `name` of `locals`, or a new one * whose id is then saved there. For a span that can still be open when * a checkpoint is taken: the resumed run reopens it under the same id, * so its events join the paused run's instead of starting a span of * their own. Closing is the usual `endSpan`. See * lib/statelog/spanSlot.ts for what a slot is and how long an id lives. */ reopenSpan(type: SpanType, locals: Record, name: string): string | undefined; adoptExternalParentSpan(spanId: string): void; endSpan(spanId?: string): SpanContext | undefined; get currentSpan(): SpanContext | undefined; toJSON(): { traceId: string; projectId: string; host: string; debugMode: boolean; }; debug(message: string, data: any): Promise; graph({ nodes, edges, startNode, }: { nodes: string[]; edges: Record; startNode?: string; }): Promise; enterNode({ nodeId, data }: { nodeId: string; data: any; }): Promise; exitNode({ nodeId, data, timeTaken, }: { nodeId: string; data: any; timeTaken?: number; }): Promise; beforeHook({ nodeId, startData, endData, timeTaken, }: { nodeId: string; startData: any; endData: any; timeTaken?: number; }): Promise; afterHook({ nodeId, startData, endData, timeTaken, }: { nodeId: string; startData: any; endData: any; timeTaken?: number; }): Promise; followEdge({ fromNodeId, toNodeId, isConditionalEdge, data, }: { fromNodeId: string; toNodeId: string; isConditionalEdge: boolean; data: any; }): Promise; /** Fired immediately before an LLM request is dispatched. Paired with a * terminator — `promptCompletion` (success), an `error` event with * errorType "llmError" (failure), or `promptCancelled` (race loser / * Esc / timeout abort) — by span + order: the nth promptStart in an * llmCall span pairs with the nth terminator. An unpaired start means * the call vanished — a hung or killed-mid-call run. Mirrors the * toolCallStart/toolCall pair (same OTEL start+end merge story). * Small payload by design: the request SHAPE, not its content — the * redacted message array stays on promptCompletion. hasResponseFormat * + maxTokens are the runaway-generation fingerprint (grammar- * constrained plus uncapped). `model` arrives JSON.stringify-quoted, * like every model field on the wire. */ promptStart({ model, threadId, messageCount, toolCount, hasResponseFormat, maxTokens, label, usageKind, }: { model?: string; threadId?: string | null; messageCount: number; toolCount: number; hasResponseFormat: boolean; maxTokens?: number | null; /** "completion" for a text model, "decision" for a decision model. */ usageKind?: ProviderUsageKind; /** The `llm(label:)` debug tag for this call, or null when unlabeled. * Observability only — never sent to the provider. Lets a log reader * tell, say, a verifier's call from the main one. */ label?: string | null; }): Promise; /** Terminator for a promptStart whose call was cancelled — a race * loser's abort, Esc-cancel, or a timeout. Deliberately NOT an error * event: a user cancel is not a request failure (the cancellation * path skips llmError for the same reason). Without this, every * healthy race() would leave its losers' starts unpaired and the * viewer would cry wolf. */ promptCancelled({ threadId }: { threadId?: string | null; }): Promise; /** Fired when Runner.thread starts invoking onThreadEnd hooks. Paired * with threadEndHooksEnd; both live inside the threadEndHooks span so * hook-initiated LLM calls nest underneath. */ threadEndHooksStart({ threadId, eagerSummarize, messageCount, }: { threadId: string; eagerSummarize: boolean; messageCount: number; }): Promise; /** Fired when Runner.thread finishes invoking onThreadEnd hooks — * including when a hook threw (the wrapper posts from a finally). */ threadEndHooksEnd({ threadId, timeTaken, }: { threadId: string; timeTaken: number; }): Promise; promptCompletion({ messages, completion, model, timeTaken, tools, responseFormat, usage, cost, finishReason, stream, threadId, threadIdentity, threadLabel, usageKind, decision, }: { messages: any[]; completion: any; model?: ModelName | string; timeTaken?: number; tools?: { name: string; description?: string; schema: any; }[]; responseFormat?: any; usage?: TokenUsage; cost?: TokenCost; finishReason?: string; stream?: boolean; /** Registry id of the thread that issued this LLM call. Stamped * here so downstream eval / log tools can attribute the call to * a thread without walking the span tree (which doesn't link * promptCompletion back to threadCreated). Null when the caller * has no active thread (rare — only at the very start of a run). */ threadId?: string | null; /** Stable identity of the MessageThread that issued this call. */ threadIdentity?: string | null; /** Optional user-facing label on that MessageThread. */ threadLabel?: string | null; /** "completion" for a text model, "decision" for a decision model. */ usageKind?: ProviderUsageKind; /** What a decision model was sent and what it answered. Only on a * decision call. */ decision?: DecisionLog; }): Promise; /** * Emit an `embedCompletion` event. Mirrors `promptCompletion` so the * viewer can render the two with a shared formatter, but keeps the * embedding-specific bits (dimensions, phase tag) separate. * * `inputPreview` is intentionally a short slice of the source text — * the full text is kept off the wire so a transcript-with-PII never * ends up in a remote sink by accident. Vectors are NEVER logged * (only their length, via `dimensions`). */ embedCompletion({ inputPreview, inputCount, model, dimensions, timeTaken, usage, cost, phase, }: { /** First ~200 chars of the embedded text, for human-readable tracing. */ inputPreview: string; /** Number of input strings in the batch. Currently always 1 from * memory; left as a count so future batching changes don't * require a schema bump. */ inputCount: number; /** Embedding model name (e.g. "text-embedding-3-small"). */ model?: string; /** Vector length. Useful for catching index-vs-query model * mismatches at trace time without comparing the full vectors. */ dimensions?: number; /** Wall-clock latency in ms (caller measures via `performance.now`). */ timeTaken?: number; /** Optional provider-reported usage. Not all embed providers * return these; absent values stay undefined. */ usage?: TokenUsage; cost?: TokenCost; /** Free-form tag identifying the caller — e.g. "recall-query", * "new-observation". Lets a viewer filter "embed for recall" * vs "embed for storage" without parsing the parent span tree. */ phase?: string; }): Promise; /** * Emit an `imageGeneration` event. Mirrors `embedCompletion`. `promptPreview` * is a short slice of the prompt so a transcript-with-PII never ends up in a * remote sink; the generated image bytes are NEVER logged. */ imageGeneration({ promptPreview, model, timeTaken, usage, cost, }: { /** First ~200 chars of the prompt, for human-readable tracing. */ promptPreview: string; /** Image model name (e.g. "gpt-image-1"). */ model?: string; /** Wall-clock latency in ms (caller measures via `performance.now`). */ timeTaken?: number; /** Optional provider-reported usage. */ usage?: TokenUsage; cost?: TokenCost; }): Promise; /** * Emit a `transcription` (speech-to-text) leaf event. Mirrors * `imageGeneration`. `textPreview` is a short slice of the TRANSCRIPT so a * transcript-with-PII never lands in a remote sink; the input audio bytes are * NEVER logged. Callers must pass a projected `usage`/`cost` (5-field token * shape, no audio fields, no `raw`/`pcm`) — see the speech runtime helper. */ transcription({ textPreview, model, durationSeconds, timeTaken, usage, cost, }: { /** First ~200 chars of the transcript, for human-readable tracing. */ textPreview: string; /** Transcription model name (e.g. "whisper-1"). */ model?: string; /** Audio length in seconds, when the provider reports it. */ durationSeconds?: number; /** Wall-clock latency in ms (caller measures via `performance.now`). */ timeTaken?: number; usage?: TokenUsage; cost?: TokenCost; }): Promise; /** * Emit a `speechSynthesis` (text-to-speech) leaf event. `textPreview` is a * short slice of the INPUT text; the synthesized audio bytes are NEVER logged. * Callers pass a projected `cost` (no `raw`/`pcm`/audio bytes). */ speechSynthesis({ textPreview, model, voice, format, timeTaken, cost, }: { /** First ~200 chars of the input text, for human-readable tracing. */ textPreview: string; /** Speech model name (e.g. "tts-1"). */ model?: string; /** Voice name (e.g. "alloy"). */ voice?: string; /** Output audio format (e.g. "mp3"). */ format?: string; /** Wall-clock latency in ms (caller measures via `performance.now`). */ timeTaken?: number; cost?: TokenCost; }): Promise; /** * Emit a `localModelLoaded` leaf event: which local model the run is * pinned to and where the llama-cpp provider package was resolved from. * `entrySource` is the chooseEntryPath decision — "override" * (AGENCY_LLAMA_PROVIDER_MODULE), "local" (bare import from the local * require paths), or "global" (a global npm/pnpm install root). Emitted * once per process at bootstrap when the baked config names the llama-cpp * provider, and when agency code registers a local model explicitly. */ localModelLoaded({ model, entryPath, entrySource, }: { /** The .gguf path (or model name) calls are pinned to, when known. */ model?: string; /** Resolved entry file of the provider package, when not a bare import. */ entryPath?: string; /** How the provider package was found (see chooseEntryPath). */ entrySource: string; }): Promise; /** * Memory umbrella-span marker events. * * Why these exist: the logs viewer infers span types from EVENT * types (see `lib/logsViewer/tree.ts#inferSpanLabel`). A span that * never has an event posted with its own `span_id` is invisible to * the viewer — its children get re-parented to the trace root. * * Calling `startSpan("memoryRemember")` only sets up the AsyncLocal- * Storage stack; on its own it produces no event. These methods * post a tiny marker event right after `startSpan` so the span * materializes in the tree with the right label, and the inner * `llmCall` / `embedding` spans nest under it. The payload is also * useful in its own right (content/query preview, memoryId). */ memoryRemember({ contentPreview, memoryId, }: { /** First ~N chars of the content being remembered. */ contentPreview: string; /** Active memory scope (from `setMemoryId`, defaults to "default"). */ memoryId: string; }): Promise; memoryRecall({ queryPreview, memoryId, phase, }: { /** First ~N chars of the recall query. */ queryPreview: string; memoryId: string; /** "recall" for the user-facing tier-3 call, "recallForInjection" * for the auto-injection variant. Lets the viewer distinguish * the two without inspecting the parent span tree. */ phase: "recall" | "recallForInjection"; }): Promise; memoryForget({ queryPreview, memoryId, }: { queryPreview: string; memoryId: string; }): Promise; memoryCompaction({ memoryId, messageCount, threshold, }: { memoryId: string; /** Number of messages in the thread that triggered compaction. */ messageCount: number; /** Threshold the count was checked against (so a viewer can show * how close to the trigger this run was). */ threshold: number; }): Promise; /** Fired immediately before a tool is invoked. Paired with `toolCall` * (which fires on tool completion). Consumers can use the pair to * detect tool calls that started but never finished — e.g. when the * user cancels the current run mid-tool. The two events share the * same `span_id` (the toolExecution span). Designed to be * OTEL-compatible: an OTLP aggregator can merge the start + end * events into a single span using start_time + end_time. */ toolCallStart({ toolName, args, argCorrections, model, threadId, }: { toolName: string; args: any; /** Alias keys the runtime renamed before the call, `url` to * `baseUrl`. `args` already carries the corrected keys. Left out * when nothing was renamed. This is the record of which aliases are * earning their keep. */ argCorrections?: AliasCorrection[]; model?: ModelName; /** Registry id of the thread that issued the LLM call which is * invoking this tool. Stamped here so downstream tools can * attribute the tool call to a thread without walking the span * tree. Null when no active thread is known. */ threadId?: string | null; }): Promise; toolCall({ toolName, args, argCorrections, output, model, timeTaken, threadId, }: { toolName: string; args: any; /** See `toolCallStart`. */ argCorrections?: AliasCorrection[]; output: any; model?: ModelName; timeTaken?: number; /** Registry id of the thread that issued the LLM call which is * invoking this tool. See `toolCallStart` for rationale. */ threadId?: string | null; }): Promise; agentName({ name }: { name: string; }): Promise; evalValueRecorded({ value, threadId, }: { value: unknown; threadId: string | null; }): Promise; /** A person sent the program a message: the REPL logs one per submitted * line, and `std::statelog`'s `turnStart` lets other code log one. The * logs viewer splits a session into turns at these. */ turnStart({ message }: { message: string; }): Promise; evalOutputRecorded({ value, threadId, }: { value: unknown; threadId: string | null; }): Promise; diff({ itemA, itemB, message, }: { itemA: any; itemB: any; message?: string; }): Promise; runMetadata(metadata: RunMetadata & { moduleName?: string; entryNode?: string; }): Promise; agentStart({ entryNode, args, input, }: { entryNode: string; args?: any; /** What the entry node was given, when the caller says so explicitly (an * eval input, or any single-parameter invocation that names it). */ input?: unknown; }): Promise; agentEnd({ entryNode, result, timeTaken, tokenStats, }: { entryNode: string; result?: any; timeTaken: number; tokenStats?: { usage: TokenUsage; cost: TokenCost; }; }): Promise; interruptThrown({ interruptId, interruptData, }: { interruptId: string; interruptData: any; }): Promise; handlerDecision({ interruptId, handlerIndex, decision, value, interrupt, }: { interruptId: string; handlerIndex: number; decision: "approve" | "reject" | "propagate" | "pass" | "none"; value?: any; /** Optional summary of the interrupt being decided on. Carries * `effect`, `message`, and `data` so log consumers can see *what* * was being approved/rejected without having to correlate with * a separate `interruptThrown` event (which doesn't fire for * synchronously-resolved interrupts like `with approve`). */ interrupt?: InterruptSummary; }): Promise; interruptResolved({ interruptId, outcome, resolvedBy, timeTaken, interrupt, }: { interruptId: string; outcome: "approved" | "rejected" | "propagated" | "passed"; /** Null when nothing resolved anything — the chain-outcome event a * surfacing interrupt records ("propagated"/"passed"). */ resolvedBy: "handler" | "user" | "policy" | "ipc" | null; timeTaken?: number; /** Optional summary of the interrupt being resolved. See * `handlerDecision.interrupt` for rationale. */ interrupt?: InterruptSummary; }): Promise; checkpointCreated({ checkpointId, reason, sourceLocation, interrupts, }: { checkpointId: number; reason: "interrupt" | "explicit" | "failure" | "fork" | "race" | "pause"; sourceLocation?: { moduleId: string; scopeName: string; stepPath: string; }; /** The interrupts the run stopped for. Set by the prompt runner, a * batch (tool calls, fork, race), and the guard gate. The handler * chain logs every interrupt raised through it; this also covers the * ones made outside it, such as a debugger stop. */ interrupts?: CheckpointInterrupt[]; }): Promise; checkpointRestored({ checkpointId, restoreCount, maxRestores, overrides, }: { checkpointId: number; restoreCount: number; maxRestores?: number; overrides?: { args?: boolean; globals?: boolean; locals?: boolean; }; }): Promise; subprocessStarted({ moduleId, node, subprocessSessionId, mode, depth, }: { moduleId: string; node: string; subprocessSessionId: string; mode: "run" | "resume"; depth: number; }): Promise; subprocessEnd({ moduleId, node, subprocessSessionId, outcome, timeTaken, }: { moduleId: string; node: string; subprocessSessionId: string; outcome: "success" | "interrupted" | "failure"; timeTaken: number; }): Promise; forkStart({ forkId, mode, branchCount, }: { forkId: string; mode: "all" | "race"; branchCount: number; }): Promise; forkBranchEnd({ forkId, branchIndex, outcome, timeTaken, value, }: { forkId: string; branchIndex: number; outcome: "success" | "failure" | "interrupted" | "aborted"; timeTaken: number; /** The branch's return value, present only on a `success` outcome. * The caller serializes it defensively (see Runner's fork hooks) so * a large or non-JSON value can't bloat or break the telemetry post. */ value?: unknown; }): Promise; forkEnd({ forkId, mode, timeTaken, winnerIndex, }: { forkId: string; mode: "all" | "race"; timeTaken: number; winnerIndex?: number; }): Promise; /** One round of batched decision calls. `groups` has one entry per * request sent; a round with two conversations sends two. */ decisionBatch({ forkId, reason, groups, timeTaken, }: { forkId: string; reason: "idle" | "cap"; groups: Array<{ model: string; armKeys: string[]; callCount: number; questionCount: number; }>; timeTaken: number; }): Promise; threadCreated({ threadId, threadType, parentThreadId, label, session, hidden, }: { threadId: string; threadType: "thread" | "subthread"; parentThreadId?: string; /** User-supplied label from `thread(label: "...") { ... }`. */ label?: string | null; /** User-supplied session name from `thread(session: "...") { ... }`. * Only populated on first-create of a session (later resumes fire * `threadResumed`, not `threadCreated`). */ session?: string | null; /** True when the thread was created with `thread(hidden: true)`. */ hidden?: boolean; }): Promise; /** Fired when a previously-closed thread is re-activated via * `ThreadStore.resumeExisting()` (i.e. by `thread(continue: id)` * or `thread(session: name)` on second+ entry). Mirrors * `threadCreated` so trace consumers can distinguish first-entry * from resumption. */ threadResumed({ threadId }: { threadId: string; }): Promise; /** Fired when a reopened thread (session second+ entry or * `thread(continue: id)`) was found structurally invalid — its trailing * assistant message had tool calls with no results — and * `repairAbandonedTurn` synthesized the missing results before new work * was appended. A repair means a previous turn parked on an interrupt * and was abandoned; that is worth being able to find in a trace. */ threadRepaired({ threadId, toolCallIds, }: { threadId: string; toolCallIds: string[]; }): Promise; /** Fired when the `onThreadEnd` callback dispatcher itself throws * inside the `Runner.thread` finally block. Individual callback * errors are caught and logged by `fireWithGuard`; this event * covers the rarer case where the dispatcher loop itself blew up * (e.g. a malformed registration). Emitted in lieu of the * previous `console.error` so the failure is observable in * traces. */ threadEndHookError({ threadId, error, }: { threadId: string; error: string; }): Promise; error({ errorType, message, functionName, neverStarted, destructiveRan, sourceLocation, tools, }: { errorType: "toolError" | "llmError" | "runtimeError" | "validationError" | "limitExceeded" | "structuredOutput" | "finalizeError"; message: string; functionName?: string; /** Tool-failure classification for the tool loop's retry policy. */ neverStarted?: boolean; destructiveRan?: boolean; sourceLocation?: { moduleId: string; line?: number; }; /** Tool definitions advertised on the failed request, if any. Lets an * `llmError` carry the request's tool list — otherwise lost, because * the `promptCompletion` event (which logs `tools`) only fires on * success. `toolsOf()` reads this the same way it reads * `promptCompletion.data.tools`. */ tools?: unknown[]; }): Promise; /** Structured warning event. First consumer: the failure-propagation * check (warnType "failurePropagation"), which logs every skipped call * and every would-be throw in "warn" mode, and every skip in "on" mode. * * The variable payload — most importantly `error`, an arbitrary user * value — is nested under `data` because post()'s redaction replacer is * scoped to the `data` payload ONLY; a top-level `error` would carry * redact()-tagged secrets into statelog unscrubbed. `message` embeds a * truncated error PREVIEW as a string; that matches the exposure of the * existing error() event's message and is a deliberate parity call. */ warn({ warnType, message, functionName, param, error, schemaChars, threshold, }: { warnType: "failurePropagation" | "toolSchemaSize" | "failureData" | "decisionBatchCap" | "endTurn"; message: string; functionName?: string; param?: string; error?: unknown; /** toolSchemaSize only: the offending tool's serialized schema length * and the threshold it crossed. */ schemaChars?: number; threshold?: number; }): Promise; /** One event per hop where an abort's travel touches a saveDraft * partial: a frame attached its draft (carried), a frame dropped a * callee's partial by having none of its own (erased), a partial was * discarded at a boundary (clearedAtFork, droppedAtArgPosition, * droppedAtNodeBoundary), or a * guard salvaged one (delivered). An abort through undrafted code * emits nothing. `partial` and `functionArgs` are pre-truncated string * previews, nested under `data` so post()'s redaction replacer covers * them. `spanId` is the abort's unwind span, carried explicitly * because an abort can cross span contexts (e.g. out of a fork * branch), where currentSpan attribution alone would split the trail. */ abortSalvage({ action, scopeName, spanId, functionArgs, partial, }: { action: "carried" | "erased" | "delivered" | "clearedAtFork" | "droppedAtArgPosition" | "droppedAtNodeBoundary"; scopeName?: string; spanId?: string; functionArgs?: string; partial?: string; }): Promise; /** Wire the out-of-frame redaction fallback (see the field's docstring). * Called by the execution context right after this client is created. */ setFallbackGlobals(fn: () => GlobalStore | undefined): void; post(body: Record, options?: { /** * When true, fire the remote http POST without awaiting its * result. The synchronous file/stdout sinks still run inline so * the event ordering they care about is preserved; only the * network round-trip is detached. Used for end-of-run events * (e.g. `agentEnd`) where nothing useful can be done with the * response and waiting would just delay process exit. */ noWait?: boolean; }): Promise; } export declare function getStatelogClient(config: { host: string; traceId?: string; projectId: string; debugMode?: boolean; observability?: boolean; logFile?: string; }): StatelogClient; /** * The logging client without `startSpan`, `reopenSpan` and `endSpan`. This is the type of * `ctx.statelogClient` and `ctx.rootLog`. * * A span opened on the client itself goes on the root span stack, which * every fork branch shares. Two branches doing that at once would pop each * other's spans. A run's own logger, `run.log`, is bound to its branch's * span stack and keeps the span methods. */ export type RootLog = Omit;