// Shared, cross-cutting type shapes for bare-agent, consumed from JSDoc via // /** @typedef {import('../types').Provider} Provider */ (path is relative to the .js file) // These describe the structural contracts that flow between components (provider // <-> loop <-> tools). Per-file option bags live as local @typedef blocks in // each module; only the genuinely shared shapes belong here. /** * Token accounting returned by a provider's generate(), normalized to one neutral shape across * providers. `inputTokens` is always the UNCACHED prompt remainder — total prompt = * inputTokens + cacheReadTokens + cacheCreationTokens. Providers whose API folds cached tokens * into the prompt count (OpenAI `prompt_tokens`, Gemini `promptTokenCount`) subtract them out here; * Anthropic's `input_tokens` is already the remainder. The cache tiers price differently from * uncached input (read is cheaper, Anthropic's creation is a premium) — see `estimateCost`. Absent * cache fields mean the provider/model didn't cache (local models, short prompts); treat as 0. */ export interface Usage { /** Uncached prompt tokens, billed at the model's full input rate. */ inputTokens: number; /** Completion tokens (includes provider "thinking"/reasoning tokens where billed as output). */ outputTokens: number; /** Prompt tokens served from cache — a cheaper tier (OpenAI ~0.5×, Gemini ~0.25×, Anthropic ~0.1×). */ cacheReadTokens?: number; /** Prompt tokens written to cache — a premium tier (Anthropic ~1.25×; OpenAI/Gemini have no write surcharge → 0). */ cacheCreationTokens?: number; } /** * Canonical per-run counters returned on a Loop result as `result.metrics` (Feature 3 — the meter). * Present on every run, gate-wired or not. `tokens` is CUMULATIVE across all rounds and all four tiers * (the run total — unlike `result.usage`, which is the last round only and kept for back-compat). */ export interface RunMetrics { /** Rounds (LLM turns) executed. */ turns: number; /** Total tool calls the model made (every invocation, including denied/unknown). */ toolCalls: number; /** * BA-16 — turns that happened INSIDE a cycle-owning provider's own session (CLIPipe native tool * mode). Such a session is ONE Loop round however many turns it really took, so `turns` alone * would report a 14-turn session as 1 — a round count that reads far cheaper and shorter than the * run actually was. 0 for every provider whose cycle the Loop drives. */ sessionTurns: number; /** Per-tool invocation counts, keyed by tool name. */ byTool: Record; /** Cumulative token spend across all rounds (incl. summarize calls), by tier. */ tokens: { input: number; output: number; cacheCreation: number; cacheRead: number }; /** Cumulative USD over priced rounds; null ONLY if nothing could be priced (explicit-unknown, not free). */ costUsd: number | null; /** Count of rounds genuinely unpriceable (no usage, or a runaway non-finite estimate) — the loud-unpriced signal. */ unpricedRounds: number; /** BA-21: count of priced rounds whose rate was a built-in guesstimate (rateSource:'tier' or 'default'), not a real/supplied rate. */ estimatedRounds: number; /** Spawn-tool invocations this run (the `spawn` tool count — counts every call, incl. denied/failed). */ spawned: number; /** * CE-activity rollup, derived in-place from Stream events (loop:trim, loop:summarize) — a * convenience view, not a second source. `compactions`: destructive trim evictions. `summaries`: * ctx.summarize calls. `tokensTrimmed`: APPROXIMATE (~4 chars/token) tokens evicted from the * canonical transcript — an estimate, since evicted spans have no exact provider count (§3.10). * (The memory.* footprint stays deferred — its source crosses component independence — §3.10.) */ context: { compactions: number; summaries: number; tokensTrimmed: number }; /** * §3.6 memory footprint — the memory ops bareagent INITIATES this run, via the loop-lent * `ctx.recordMemoryOp` hook (bounded per run; result.metrics is a copy taken at run end). `stashed`: * lossless parks to the stash table/in-process. `episodes`: stance writes on compact. The Memory * wrapper is metered symmetrically and opt-in (0 unless the caller threads the run's ctx): `recalls` * = Memory.search reads, `stored` = Memory.store writes. `facts` = durable facts written by `remember` * (the consolidation pass) — its honest producer; DISJOINT from `stored` (a distilled fact counts once, * as a fact). litectx's own episode→fact promotion stays litectx-internal to surface, not this counter. */ memory: { stashed: number; episodes: number; recalls: number; stored: number; facts: number }; /** Wall-clock duration of the run in ms. */ durationMs: number; } /** A single tool invocation requested by the model. `arguments` is parsed JSON. */ export interface ToolCall { id: string; name: string; arguments: any; } /** Normalized result every provider.generate() resolves to. */ export interface GenerateResult { text: string; toolCalls: ToolCall[]; /** Normalized token usage, or `null` when the provider reported NO usage block (BA-24) — an * unpriceable round, distinct from a present block whose fields are legitimately 0. */ usage: Usage | null; /** Model id the response was produced by; preferred over Provider.model for cost accounting. */ model?: string | null; /** * Why generation ended, normalized across providers (BA-6). Before this, no provider read its native * finish-reason field, so a round the API CUT OFF at the token cap was indistinguishable from one the * model chose to end — and the Loop, whose rule is "no tool calls ⇒ final answer", returned the * truncation as a clean finish with `error: null`. * * - `'end_turn'` — the model finished on its own. The only clean finish. * - `'max_tokens'` — CUT OFF at the output cap. The Loop returns `error: 'truncated:max_tokens'` * (preserving the partial text) and REFUSES to execute any tool call the round carries: a complete * call always arrives as `'tool_use'`, so one riding a `'max_tokens'` round was cut off * mid-generation with arguments missing — the BA-4 file-zeroing mechanism. * - `'tool_use'` — stopped to call a tool, and the call is COMPLETE. * - `'stop_sequence'` / `'refusal'` / `'pause_turn'` / `'context_exceeded'` — reported, not acted on. * - `null` — the provider didn't report one (e.g. CLIPipe) or the value is unrecognized. Reproduces * pre-BA-6 behavior exactly, so an unmapped provider degrades to the status quo. */ stopReason?: string | null; /** * BA-16 — present ONLY when the provider ran its own multi-turn session for this single call * (`Provider.ownsCycle`), e.g. CLIPipe native tool mode, where the CLI executes the caller's tools * natively over MCP and keeps going until it answers or hits a bound. * * It exists so the Loop can stay honest about a call that was not one turn: * - `turns` / `toolCalls` — what really happened, so `metrics` cannot report a 14-turn session as 1. * - `error` — a terminal the provider detected INSIDE the session (a turn bound, a deny/stuck * streak, a broken tool bridge). The Loop surfaces it as the run's `error`, never merely as a * field: every downstream consumer branches on `error` as the sole success signal, so surfacing * alone would let a session in which no tool call ever succeeded propagate as converged. * - `usageReported` — the provider ALREADY forwarded this call's usage per internal turn, so the * Loop must not forward the summed total again and bill the gate twice. */ session?: { turns: number; toolCalls: number; error: string | null; usageReported: boolean; }; /** * True when the requested `temperature` was rejected by the model (400, unsupported/deprecated) and * the request was retried without it (BA-10). The response was produced at the model's DEFAULT * temperature, not the one requested — callers reporting an effective temperature must honor this. */ temperatureDropped?: boolean; /** * Present ONLY when `thinkingMode` ('on'|'off') was set on the provider/call. The outcome — "not * supported" is an ANSWER, never an error; the call always runs: * `'applied'` — a shape was accepted and, for 'off', the reply shows zero thinking (for 'on' always * `'applied'` once accepted: an adaptive model may legitimately choose not to think); * `'unsupported'` — this provider/model cannot do the requested mode (measured, every shape rejected, * DeepSeek 'on' + forced toolChoice, or CLIPipe): the call ran WITHOUT our thinking field (the model's * default behavior; DeepSeek forced-tool case sends `thinking:{type:'disabled'}`); * `'ignored'` — 'off' was sent and accepted but the reply shows thinking anyway (warns once; round kept); * `'unknown'` — accepted but the reply's thinking signal is unreadable (e.g. OpenAI without `reasoning_tokens`). * `'ignored'`/`'unsupported'` warn once per provider instance. `Loop.run()` surfaces the WORST status * across rounds (ignored > unsupported > unknown > applied). */ thinkingStatus?: 'applied' | 'unsupported' | 'ignored' | 'unknown'; /** * Legacy boolean, DERIVED from `thinkingStatus` (present with it): applied⇒`true`, ignored⇒`false`, * unsupported⇒`false`, unknown⇒`null`. Prefer `thinkingStatus`. On `Loop.run()` it is derived from the worst status. */ thinkingApplied?: boolean | null; /** * BA-27 — present ONLY when the model emitted a tool call whose `function.arguments` was * syntactically-broken JSON (an extra brace, a truncated object). The billed round already * succeeded, so rather than throw (which loses the round's usage and hangs metering), the provider * returns NO usable tool calls (`toolCalls: []`) plus this marker. A caller treats it as "no usable * tool call" and retries; `usage`/`model` still flow so the round is metered. The JSON is NEVER * repaired. Absent on a clean round. OpenAI-compatible + Ollama string-arguments only; Anthropic * arrives pre-parsed and cannot hit this. * * `rawArguments` (BA-27 follow-up) is present only when the provider was * constructed with `exposeMalformedArgs: true` (default off, mirrors `exposeErrorBody`) AND the * raw `function.arguments` was a string: the FULL raw value verbatim, uncapped (model-generated; may be * large or sensitive). */ malformedToolCall?: { name: string | undefined; error: string; rawArguments?: string; /** @deprecated never set since 0.50.0 (rawArguments is now uncapped) */ rawTruncated?: true; }; /** * BA-7 — provider-native content blocks the normalized `{text, toolCalls}` shape cannot express * (Anthropic `thinking` / `redacted_thinking`), captured opaquely so the Loop can put them on the * transcript and the provider can replay them on the next round. * * Present only when the response actually carried such blocks — so a provider that returns none * leaves both the result and the resulting message byte-identical to pre-BA-7. See `Message.providerBlocks` * for the replay contract (the provider/model tag is enforced; a signature is model-bound). */ providerBlocks?: { provider: string; model: string; blocks: any[] }; /** * Authoritative per-call cost in USD, reported by the provider itself — e.g. CLIPipeProvider * `parse:'claude-json'` surfacing the claude CLI's own `total_cost_usd`, a real price with no local * rate table. When a FINITE number the Loop prefers it over `estimateCost` and treats the round as * priced (feeding bareguard's USD budget axis). `0` is a valid priced value (a subscription/marginal-$0 * run) — distinct from omitted/null, which means "couldn't price" and falls back to the rate table. */ costUsd?: number; } /** A conversation message in OpenAI chat format. */ export interface Message { role: string; content?: string | null; tool_calls?: any[]; tool_call_id?: string; /** * BA-7 — provider-native content blocks that this OpenAI-shaped message cannot express, carried * verbatim so they can be replayed to the provider that issued them. * * Today this is Anthropic `thinking` / `redacted_thinking`. Anthropic's contract is that such * blocks are echoed back UNCHANGED (`signature` included) when continuing a tool-use conversation; * before BA-7 there was no field on this type that could hold one, so they were silently dropped. * * OPAQUE by design — the Loop never reads `blocks`, and the provider re-emits their bytes rather * than re-serializing a parsed shape (a `redacted_thinking` block cannot survive a round-trip * through parsed fields). The `provider`/`model` tag is enforced on replay: a thinking signature is * bound to the model that produced it, so a mismatch drops the blocks and degrades to the lossy * pre-BA-7 request rather than risking a 400. * * The normalized `content` / `tool_calls` remain the source of truth: only blocks that have no * normalized representation live here, so an `assemble`/`trim` seam that rewrites this message is * never silently undone by a stale cached copy of its text. */ providerBlocks?: { provider: string; model: string; blocks: any[] }; [key: string]: any; } /** A callable tool exposed to the loop/provider. */ export interface ToolDef { name: string; description?: string; parameters?: Record; /** MCP tools only: the server's own behavioural hints (readOnlyHint, ...), passed through unmodified. Untrusted metadata, never an authorization signal. Absent when the server sent none; a non-object value from the server is treated as absent. */ annotations?: Record; execute(args: any): any | Promise; } /** Minimal LLM provider contract the Loop and Planner depend on. */ export interface Provider { /** Model id, surfaced for cost estimation. */ model?: string | null; /** Provider name, surfaced in onLlmResult. */ name?: string | null; /** * BA-16 — true when the provider runs its OWN multi-turn cycle inside a single `generate()` call * (CLIPipe native tool mode), executing the caller's tools itself rather than returning * `toolCalls` for the Loop to run. * * The Loop reads this to REFUSE options it could never honor rather than accept them and leave * them silently dead: `assemble`/`trim` (the provider owns the transcript) and, most importantly, * a Loop-level `policy` — no tool call reaches the Loop, so that fence would simply not be there * while the run still looked governed. Such a provider must carry its own `policy`. */ ownsCycle?: boolean; /** Gate chokepoint for a cycle-owning provider — same contract as `Loop({policy})`. */ policy?: ((tool: string, args: any, ctx?: any) => any) | null; generate( messages: Message[], tools?: ToolDef[], options?: Record, ): Promise; } /** Swappable persistence backend for Memory. */ export interface Store { store(content: any, metadata?: Record): any; search(query: string, options?: Record): any; get(id: any): any; delete(id: any): any; } /** Opaque per-run blob forwarded to policy/record callbacks. */ export type Ctx = any;