/** * LLMCall — the leaf primitive for a single LLM invocation (no tools). * * Pattern: Builder (GoF) → produces a Runner backed by a footprintjs FlowChart. * * Chart shape — outer client wrapper around an inner llm-subflow: * * Client → sf-llm-call → loopTo(client) * * Outer `Client` stage: * - First visit: receives args, writes userMessage to scope. * - Second visit (after the loop completes): $break()s with * scope.answer as the chart's TraversalResult. * * Inner `sf-llm-call` subflow (drill-down view): * Initialize → sf-system-prompt → sf-messages → call-llm * → [sf-thinking if handler] → extract-final * * NO `sf-tools` slot — LLMCall has no tools by design (that's Agent's * territory). Atomic LLMCall's lens chart is a clean 3-node top-level * view (Client + LLM + loop edge) that drills into the real flowchart * below. * * Loop semantics: LLMCall is one-shot. The loop fires once; the * second Client visit immediately breaks. The shape is identical to * chat-mode (future): swap `$break()` for `pause()` and the same * chart supports multi-turn conversation. * * Slot subflows write convention-keyed injections observed by * ContextRecorder. The call-llm stage typedEmits stream.llm_start * and stream.llm_end observed by StreamRecorder. When a * `ThinkingHandler` resolves for the provider, `sf-thinking` mounts * automatically (auto-wired by provider.name — same convention Agent * uses). * * Emits (through internally-attached recorders): * agentfootprint.stream.llm_start / llm_end * agentfootprint.context.injected / slot_composed * agentfootprint.stream.thinking_end (when sf-thinking mounted) */ import { type FlowchartCheckpoint, type RunOptions, type StructureRecorder } from 'footprintjs'; import type { GroupMetadata, GroupTranslator } from './translator.js'; import type { RunnerPauseOutcome } from './pause.js'; import type { LLMProvider, PricingTable } from '../adapters/types.js'; import { RunnerBase } from './RunnerBase.js'; export interface LLMCallOptions { readonly provider: LLMProvider; /** Human-friendly name shown in events/metrics. Default: 'LLMCall'. */ readonly name?: string; /** Stable id used for topology + events. Default: 'llm-call'. */ readonly id?: string; /** Model to request from the provider. */ readonly model: string; /** Optional sampling temperature. */ readonly temperature?: number; /** Optional max output tokens. */ readonly maxTokens?: number; /** * Per-slot context budgets, in characters (8.11.0). The LLMCall twin of * `AgentOptions.contextBudget` — two slots here, since an LLMCall has no * tools slot. * * Each slot warns (and emits `agentfootprint.context.budget_pressure`) when * it composes over its budget. **Nothing is truncated** — the full content * still reaches the LLM; the budget is a signal, not a limiter. Defaults: * `systemPrompt` 4000, `messages` 10000. */ readonly contextBudget?: { readonly systemPrompt?: number; readonly messages?: number; }; /** * Record the ASSEMBLED system prompt on the LLM call (9.50.0). The LLMCall * twin of `AgentOptions.recordSystemPrompt` — same field, same contract, * same default. **Opt-in, default OFF**: when `true`, * `agentfootprint.stream.llm_start` carries `systemPromptText`, the joined * prompt verbatim as sent. PRIVACY: with the dial on, the full prompt rides * into every recorder, sink and persisted recording — off, only * `systemPromptChars` (the length) is on the record. */ readonly recordSystemPrompt?: boolean; /** * Mint a receipt on the call (9.91.0). Default ON — the LLMCall twin of * `AgentOptions.recordReceipt`, same field, same contract, same default. * * A receipt is the fingerprint of what the model was actually handed, * committed at the call so a reader can check the rebuilt view against it. * `false` declines it, for the reason the agent's switch exists: the mint is * a SHA-256 per system piece, per message and per tool schema, plus one * commit-log value. Declining does not make the log unreadable — `servedAt` * still rebuilds the view; what is gone is the witness, and `servedAt` says * so with the same gap a pre-9.88 recording raises. * * @example decline the receipt in a bulk eval loop * new LLMCall({ provider, model, recordReceipt: false }) */ readonly recordReceipt?: boolean; /** * Pricing adapter. When set, LLMCall emits `agentfootprint.cost.tick` * after every LLM response with per-call and cumulative USD. Run-scoped * — the cumulative resets on each `.run()`. */ readonly pricingTable?: PricingTable; /** * Cumulative USD budget per run. When provided along with `pricingTable`, * LLMCall emits `agentfootprint.cost.limit_hit` with `action: 'warn'` * the first time cumulative USD crosses the budget. Execution continues * — consumers choose whether to abort by listening to the event. * * The object form `{ usd, onExceed }` is accepted for symmetry with `Agent`, * but `onExceed` must be `'warn'` here: halting means "stop at the next * iteration boundary", and one call has no next boundary. `'halt'` is * refused at build rather than silently ignored. */ readonly costBudget?: number | { readonly usd: number; readonly onExceed: 'warn' | 'halt'; }; /** * Optional build-time recorders threaded into footprintjs's * `flowChart()` factory. Each recorder observes per-node build * events (`onStageAdded` / `onSubflowMounted` / etc.) for this * LLMCall's internal chart (Initialize + slot mounts + CallLLM). When * omitted, no build-time observation is wired up. */ readonly structureRecorders?: readonly StructureRecorder[]; /** * Optional per-COMPOSITION translator (UI-agnostic). See * `core/translator.ts`. When attached, `runner.getUIGroup()` invokes * it with the LLMCall's `GroupMetadata` (kind `'LLMCall'`, id, name, * empty `members[]`, plus `extra.slots` with the three slot ids — * `system-prompt`, `messages`, `tools` — so Lens can render the slot * cards inside an LLMCall card without inspecting `buildTimeStructure`). * Returns `undefined` when omitted. */ readonly groupTranslator?: GroupTranslator; } export interface LLMCallInput { readonly message: string; } export type LLMCallOutput = string; export declare class LLMCall extends RunnerBase { readonly name: string; readonly id: string; private readonly provider; private readonly model; private readonly temperature?; private readonly maxTokens?; private readonly systemPromptValue; private readonly pricingTable?; /** Normalized at construction: a bare number is `{ usd, onExceed: 'warn' }`. */ private readonly costBudget?; /** Per-slot character budgets (8.11.0). Absent keys keep the slot default. */ private readonly contextBudget?; /** `LLMCallOptions.recordSystemPrompt` (9.50.0) — opt-in, default OFF. */ private readonly recordSystemPromptValue; /** `LLMCallOptions.recordReceipt` (9.91.0) — opt-OUT, default ON. */ private readonly recordReceiptValue; private readonly structureRecorders?; private readonly groupTranslator?; /** Auto-resolved from provider.name at construction time (same * convention Agent uses — see findThinkingHandler). When undefined, * sf-thinking is NOT mounted and the chart has zero thinking * overhead (build-time conditional mount). */ private readonly thinkingHandler?; private currentRunContext; constructor(opts: LLMCallOptions, systemPromptValue: string); static create(opts: LLMCallOptions): LLMCallBuilder; protected getGroupTranslator(): GroupTranslator | undefined; /** LLMCall has no nested-runner members (slots are subflows of * the LLMCall's own chart, not Runner instances). The slot ids * are surfaced via `extra` so Lens can render the slot cards * inside an LLMCall card without inspecting `buildTimeStructure`. * * TWO slots only — LLMCall does not have tools (that's Agent's * affordance). Atomic LLMCall renders as a clean 2-pill card in * collapsed (top-level) view. */ protected buildUIGroupMetadata(): GroupMetadata; run(input: LLMCallInput | string, options?: RunOptions): Promise; resume(checkpoint: FlowchartCheckpoint, input?: unknown, options?: RunOptions): Promise; private createExecutor; private finalizeResult; private buildChart; } /** * Tiny fluent builder. Validates required fields at build() time. */ export declare class LLMCallBuilder { private readonly opts; private systemPromptValue; /** Whether `.system()` has been called — see the refusal below for why the * flag exists separately from the value. */ private systemPromptSet; constructor(opts: LLMCallOptions); /** * Set the system prompt. Once per call — a second `.system()` used to * REPLACE the first in silence, so the instructions written first were * never sent and nothing said so. Join the parts yourself and pass one * string. */ system(prompt: string): this; build(): LLMCall; }