import { ModelName } from "smoltalk"; import { JSONEdge } from "./types.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" | "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; 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; }; 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; private logFile?; private enabled; private remoteEnabled; private inFlight; private rootStack; private spanStorage; private metadata?; private requestTimeoutMs; private fallbackGlobals; constructor(config: StatelogConfig); private currentStack; snapshotStack(): SpanContext[]; runInBranchContext(parentStack: SpanContext[], fn: () => Promise): Promise; startSpan(type: SpanType): 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, }: { model?: string; threadId?: string | null; messageCount: number; toolCount: number; hasResponseFormat: boolean; maxTokens?: number | null; /** 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, }: { 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; }): 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; /** * 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, model, threadId, }: { toolName: string; args: any; 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, output, model, timeTaken, threadId, }: { toolName: string; args: any; 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; evalValueRecorded({ value, threadId, }: { value: unknown; threadId: string | null; }): 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, }: { entryNode: string; args?: any; }): 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?: { effect: string; message: string; data: any; }; }): Promise; interruptResolved({ interruptId, outcome, resolvedBy, timeTaken, interrupt, }: { interruptId: string; outcome: "approved" | "rejected" | "propagated"; resolvedBy: "handler" | "user" | "policy" | "ipc"; timeTaken?: number; /** Optional summary of the interrupt being resolved. See * `handlerDecision.interrupt` for rationale. */ interrupt?: { effect: string; message: string; data: any; }; }): Promise; checkpointCreated({ checkpointId, reason, sourceLocation, }: { checkpointId: number; reason: "interrupt" | "explicit" | "failure" | "fork" | "race"; sourceLocation?: { moduleId: string; scopeName: string; stepPath: string; }; }): 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; 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, }: { warnType: "failurePropagation"; message: string; functionName?: string; param?: string; error?: unknown; }): 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), 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"; 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; /** * Await every in-flight remote POST. Remote sends are fire-and-forget * (see `post`), so call this at the end of a run — before the process * exits — to make sure detached telemetry is actually delivered. A * no-op when observability is off or nothing is in flight. */ flush(): Promise; } export declare function getStatelogClient(config: { host: string; traceId?: string; projectId: string; debugMode?: boolean; observability?: boolean; logFile?: string; }): StatelogClient;