/** * adapters/types — the Ports of the hexagonal architecture. * * Pattern: Adapter (GoF, Design Patterns ch. 4) + Ports-and-Adapters * (Cockburn, 2005). * Role: Contracts for every external dependency the library reaches for: * LLM providers, memory stores, context sources, embeddings, * guardrails, policy engines, pricing tables. * Emits: N/A (interfaces only). * * Concrete adapters (AnthropicProvider, PineconeStore, LlamaGuardDetector, * ...) implement these contracts. `core/` and `core-flow/` depend only on * these interfaces — never on concrete adapters. */ import type { ContextRole, ContextSlot, ContextSource } from '../events/types.js'; import type { ThinkingBlock } from '../thinking/types.js'; export interface LLMMessage { readonly role: ContextRole; readonly content: string; /** For `role: 'tool'` — the tool_use id this result corresponds to. */ readonly toolCallId?: string; /** For `role: 'tool'` — the tool name this result corresponds to. */ readonly toolName?: string; /** * For `role: 'assistant'` only — the tool calls the LLM requested in this * turn. Required for providers (Anthropic, OpenAI) that need to round-trip * tool_use blocks across iterations: when the next `complete()` includes * a `role: 'tool'` message, the provider reconstructs the matching * `tool_use` block on the previous assistant turn from this field. * Empty array on text-only turns; undefined for non-assistant roles. */ readonly toolCalls?: readonly { readonly id: string; readonly name: string; readonly args: Readonly>; }[]; /** * v2.14 — Thinking blocks emitted by the LLM on assistant turns. * * Required for Anthropic extended-thinking + tool-use flows: signed * blocks MUST be echoed BYTE-EXACT in subsequent assistant turns or * Anthropic's API rejects with 400. The framework persists blocks * here so the AnthropicProvider's serializer (Phase 4b) can restore * them on the next request. * * **Persistence model — DIFFERENT from `ephemeral`:** * - `ephemeral` messages: NOT persisted to scope.history * - `thinkingBlocks`: PERSISTED (required for signature round-trip) * * Visible to recorders + audit by default. Use * `RedactionPolicy.thinkingPatterns` (Phase 3) to scrub sensitive * reasoning content before audit-log adapters fire. * * Empty array OR undefined when no thinking is present (most calls). */ readonly thinkingBlocks?: readonly ThinkingBlock[]; /** * v2.13 — PERSISTENCE flag (NOT a visibility flag). When `true`: * • The message IS sent to the LLM as part of the next request * (visible to the model, counts toward its context window). * • The message is OBSERVABLE via narrative/recorders/audit log * (visible to humans for debugging + forensics). * • The message is NOT persisted to `scope.history` after the gate * loop that produced it completes — long-term memory writes, * `getNarrative()` snapshots, and downstream consumers see only * non-ephemeral messages. * * Use case: Instructor-style schema retry. The reliability gate * appends `{ role: 'user', content: feedbackForLLM, ephemeral: true }` * before retry — the LLM sees the validation feedback for the next * call, but the conversation history (and any memory persistence * downstream) sees only the final accepted exchange. * * Audit-trail safety: ephemeral DOES NOT mean invisible to security * review. `getNarrative()`, recorders, and the typed-event stream all * see ephemeral messages; only the persistent conversation log filters * them out. An attacker cannot use the ephemeral marker to construct * audit-invisible prompts. */ readonly ephemeral?: boolean; } export interface LLMToolSchema { readonly name: string; readonly description: string; readonly inputSchema: Readonly>; } export interface LLMRequest { readonly systemPrompt?: string; readonly messages: readonly LLMMessage[]; readonly tools?: readonly LLMToolSchema[]; readonly model: string; readonly temperature?: number; readonly maxTokens?: number; readonly stop?: readonly string[]; readonly signal?: AbortSignal; /** * Cache markers (v2.6+) — provider-agnostic prefix-cache hints * populated by `CacheStrategy.prepareRequest` after the agent's * CacheGate decider routes to `apply-markers`. Each marker * identifies a cacheable prefix in `system` / `tools` / `messages`. * * Providers that support caching (Anthropic, Bedrock-Claude) read * this field and translate to their wire format. Providers without * cache support (OpenAI auto-cache, Mock, NoOp) ignore it. */ readonly cacheMarkers?: readonly import('../cache/types.js').CacheMarker[]; /** * v2.14 — request the LLM emit reasoning/thinking content on this call. * * Activation: presence of this field tells the provider to ASK for * thinking. Anthropic translates to `thinking: { type: 'enabled', * budget_tokens: budget }` on the wire. OpenAI ignores (o1/o3 * thinking is selected at the model id level, not per-request). * * `budget` is the maximum reasoning tokens the model may spend. * Anthropic requires it; recommended range 1024-32000 for * claude-sonnet-4-5 / opus-4-5. Models that don't support extended * thinking will reject the request with HTTP 400 — pick a supported * model when setting this field. * * Independent from `LLMMessage.thinkingBlocks` (the response side): * - `request.thinking` = activation (consumer ASKS for thinking) * - `message.thinkingBlocks` = round-trip (consumer ECHOES prior * assistant turn's signed blocks back to the model) * * Set via `AgentBuilder.thinking({ budget })` — applied to every * LLM call the agent makes. Leave undefined to call without thinking * (the v2.13 default). */ readonly thinking?: { readonly budget: number; }; } export interface LLMResponse { readonly content: string; readonly toolCalls: readonly { readonly id: string; readonly name: string; readonly args: Readonly>; }[]; readonly usage: { readonly input: number; readonly output: number; readonly cacheRead?: number; readonly cacheWrite?: number; /** * v2.14 — count of reasoning/thinking tokens used by the model. * Distinct from `output` (which is visible-content tokens). * * Semantics: * - `undefined` — provider doesn't expose / no thinking enabled * on this call / call without extended thinking * - `0` — thinking enabled but model produced no * thinking tokens this call * - `>0` — actual reasoning token count (billing-relevant * for both Anthropic extended thinking and * OpenAI o1/o3 reasoning_tokens) * * Cost dashboards reading `cost.tick` events should track this * separately from `output` — pricing differs (Anthropic charges * extended thinking at output rates; OpenAI o1/o3 reasoning tokens * are billed as a separate line item). */ readonly thinking?: number; }; readonly stopReason: string; readonly providerRef?: string; /** * v2.14 — Provider-specific raw thinking data, opaque to the * framework. Providers that support extended thinking populate this * with their native shape (Anthropic: array of `{type, thinking, * signature}` blocks; OpenAI: `reasoning_summary` value; custom: * whatever the provider emits). The framework hands this to a * configured `ThinkingHandler.normalize(rawThinking)` to produce * the normalized `ThinkingBlock[]` that lands on * `LLMMessage.thinkingBlocks`. * * Undefined when the provider has no thinking content for this call * — most calls (gpt-4o, claude without extended thinking enabled, * etc.). The thinking subflow's stage early-returns in this case. */ readonly rawThinking?: unknown; } export interface LLMChunk { readonly tokenIndex: number; /** Token text. Empty for the terminal chunk (`done: true`). */ readonly content: string; /** True only for the final chunk in a stream. */ readonly done: boolean; /** * Authoritative response payload, populated ONLY on the final chunk * (`done: true`). Carries `toolCalls`, `usage`, `stopReason` — the * fields that drive the ReAct loop. The `content` mirrors the * concatenation of all non-terminal chunks; consumers can use * either source. * * Streaming providers SHOULD populate this. Older providers that * yield only text and end with `done: true` (no `response`) are * still supported — Agent falls back to `complete()` for the * authoritative payload in that case. */ readonly response?: LLMResponse; /** * v2.14 — streaming thinking-content tokens. Parallel to `content` * but for the model's reasoning chain rather than visible output. * Set on chunks that carry thinking deltas (Anthropic emits these * via `content_block_delta` events with `delta.type === 'thinking_delta'`); * undefined or empty on chunks that carry only visible-content tokens. * * Frameworks: this field drives `agentfootprint.stream.thinking_delta` * events when a `ThinkingHandler.parseChunk()` returns one. Consumers * who want to render thinking-as-it-streams subscribe to that event. * * Default consumer behavior: thinking tokens are not surfaced to end * users unless a consumer explicitly subscribes to the * `agentfootprint.stream.thinking_delta` event (or renders it through a * live-status strategy). */ readonly thinkingDelta?: string; } /** * v7.8 — what a resilience decorator DID during one provider call. * * Plain data only (strings + numbers), so a report drops straight into a * typed event payload and survives `structuredClone`. Field names are * deliberately 1:1 with the declared event payloads * (`FallbackTriggeredPayload`, `ErrorRetriedPayload`, * `ErrorRecoveredPayload`) so the in-run call site maps a report to an * event without renaming or synthesizing anything. * * Produced by exactly one decorator per `kind`: * • `'fell-back'` ← `withFallback` * • `'retried'` / `'recovered'` ← `withRetry` * • nothing ← `withCircuitBreaker` (no declared event for a breaker * transition; a trip is visible via the enclosing fallback's `reason`) */ export type ResilienceReport = { readonly kind: 'fell-back'; /** Provider tried first, which failed. */ readonly primary: string; /** Provider called instead — the one that actually served. */ readonly fallback: string; /** Message of the error that triggered the fallback. */ readonly reason: string; } | { readonly kind: 'retried'; /** 1-based number of the attempt ABOUT TO START (2 = first retry). */ readonly attempt: number; readonly maxAttempts: number; /** Message of the error that caused this retry. */ readonly lastError: string; readonly backoffMs: number; /** * Classification OF THE ERROR — **not** of the predicate's * reasoning. `shouldRetry` returns a bare boolean, so when a custom * predicate is in force the decorator cannot know *why* it said yes; * this field reports what the error looked like instead, derived * from the same `status`/`statusCode` fields `defaultShouldRetry` * inspects. One of: `'http-429'` | `'http-5xx'` | `'http-4xx'` | * `` `http-${code}` `` | `'no-status'`. */ readonly reason: string; } | { readonly kind: 'recovered'; /** 1-based attempt that finally succeeded. Always >= 2. */ readonly attempt: number; readonly totalDurationMs: number; }; /** * v7.8 — optional per-call hooks the CALLER hands a provider. * * Lets a resilience decorator report what it did to whoever invoked it, * without the decorator knowing anything about runs, scopes, or events. * The channel rides the CALL (not the factory) because decorators are * constructed by the consumer before any run exists. * * Passed by agentfootprint's in-run LLM call sites, which translate each * report into an already-declared typed event with real correlation ids. * Outside a run nothing passes hooks, so `hooks` is `undefined` and every * report site short-circuits — standalone decorator behaviour is * unchanged. * * ⚠ **IF YOU WRITE A PROVIDER WRAPPER, FORWARD THIS PARAMETER.** It is the * one silent-failure trap in the design. A wrapper that declares * `complete(req)` and calls `inner.complete(req)` still type-checks * perfectly — `hooks` is optional, and TypeScript has never rejected an * implementation for taking FEWER parameters than its signature — so * dropping it produces no compile error, no runtime error, and no test * failure. What it produces is a decorated provider that goes DARK the * moment it is placed underneath: the reports still happen, and nothing * receives them. Every wrapper shipped in this library forwards (the three * `src/resilience/` decorators, and all eight class-form / Azure wrappers * in `src/adapters/llm/`), so the trap can only be sprung by a * consumer-authored wrapper — `myWrapper(withRetry(p))`. There is no way to * police it from here; the only defence is this note and the one in the * resilience guide's "honest limits". */ export interface LLMCallHooks { /** * Called once per resilience decision (a fallback, a retry, a * recovery). Decorators forward this hook inward unchanged, so a * stack of decorators produces one concatenated report stream with no * duplication. */ readonly onResilience?: (report: ResilienceReport) => void; } export interface LLMProvider { readonly name: string; /** * `hooks` (v7.8) is optional and additive — implementations may declare * `complete(req)` with no second parameter and stay assignable. A LEAF * provider (one that talks to a vendor) may ignore it. A WRAPPER must * forward it, or everything it wraps goes silently dark — see the * `LLMCallHooks` docs above. */ complete(req: LLMRequest, hooks?: LLMCallHooks): Promise; stream?(req: LLMRequest, hooks?: LLMCallHooks): AsyncIterable; } export interface ResolveCtx { readonly userMessage: string; readonly turnIndex: number; readonly iterIndex: number; readonly availableBudgetTokens: number; readonly signal?: AbortSignal; } export interface ContextContribution { readonly contentSummary: string; readonly rawContent?: string; readonly score?: number; readonly rank?: number; readonly asRole?: ContextRole; readonly sectionTag?: string; readonly reason: string; } export interface ContextSourceAdapter { readonly id: string; readonly targetSlot: ContextSlot; readonly source: ContextSource; resolve(ctx: ResolveCtx): Promise; } export interface EmbeddingProvider { readonly name: string; readonly dimension: number; embed(inputs: readonly string[], kind: 'query' | 'document'): Promise; } export interface RiskContext { readonly slot?: ContextSlot; readonly source?: ContextSource; readonly turnIndex?: number; readonly iterIndex?: number; } export interface RiskResult { readonly flagged: boolean; readonly severity: 'low' | 'medium' | 'high' | 'critical'; readonly category: 'pii' | 'prompt_injection' | 'runaway_loop' | 'cost_overrun' | 'hallucination_flag'; readonly evidence: Readonly>; readonly suggestedAction: 'warn' | 'redact' | 'abort'; } export interface RiskDetector { readonly name: string; check(content: string, context: RiskContext): Promise; } /** * One entry in the in-flight tool-call sequence delivered to * `PermissionChecker.check()` since v2.12. Lets sequence-aware * policies (exfil chain detection, idempotency limits, cost guards) * inspect what the agent has already dispatched this run. * * Derived from `scope.history` at check time — single source of truth, * survives `agent.resumeOnError(checkpoint)` correctly. */ export interface ToolCallEntry { /** Tool name dispatched. */ readonly name: string; /** Tool args passed to `tool.execute(args, ctx)`. */ readonly args: Readonly> | undefined; /** ReAct iteration the call was dispatched on. */ readonly iteration: number; /** * Optional source identifier — `'local'` for tools registered via * `.tool(...)` / `staticTools(...)`, or the `ToolProvider.id` for * tools resolved through a `discoveryProvider`. Lets cross-hub * exfil rules match on origin, not just name. */ readonly providerId?: string; } export interface PermissionRequest { readonly capability: 'tool_call' | 'memory_read' | 'memory_write' | 'external_net' | 'user_data'; readonly actor: string; readonly target?: string; readonly context?: Readonly>; /** * v2.12 — Sequence of tool calls already dispatched this run, in * call order. EMPTY for non-`tool_call` capabilities. Sequence-aware * policies (forbidden chains, idempotency limits) read this to make * decisions that single-call governance cannot. */ readonly sequence?: readonly ToolCallEntry[]; /** * v2.12 — Full conversation history at check time. Lets policies * inspect prior assistant content / tool results without maintaining * parallel state via event subscription. */ readonly history?: readonly LLMMessage[]; /** * v2.12 — Current ReAct iteration (1-based). Lets policies fire * different rules per iteration without external counters. */ readonly iteration?: number; /** * v2.12 — Caller identity from `agent.run({ identity })`. Permission * predicates can role-check on `identity.principal` / `identity.tenant`. */ readonly identity?: { readonly tenant?: string; readonly principal?: string; readonly conversationId: string; }; /** * v2.12 — Optional abort signal propagated from `agent.run({ env: { signal } })`. * Async checkers (Redis lookups, hub-backed allowlists) MUST honor this * — when the agent run is cancelled, in-flight checks should abort. */ readonly signal?: AbortSignal; } /** * v2.12 — content shape mirroring `LLMMessage.content`. Future-compatible * with multi-modal `tool_result` blocks once `LLMMessage` widens. */ export type ToolResultContent = string; export interface PermissionDecision { /** * v2.12 — `'halt'` is NEW. Terminates the run cleanly with a typed * `PolicyHaltError`. The framework writes a synthetic `tool_result` * (using `tellLLM`) to `scope.history` BEFORE throwing, so: * • Anthropic / OpenAI tool_use ↔ tool_result pairing is satisfied * • The conversation history is consistent for `resumeOnError` * • Lens / `getNarrative()` shows what the LLM was told * * `'deny'` keeps existing semantics: synthetic tool_result + LLM * continues and can pick differently. */ readonly result: 'allow' | 'deny' | 'halt' | 'gate_open'; readonly policyRuleId?: string; readonly rationale?: string; readonly gateId?: string; /** * v2.12 — telemetry tag (machine-readable, stable across versions). * Surfaces on `agentfootprint.permission.halt.reason` for routing * alerts (e.g. `'security:exfiltration'` → PagerDuty, * `'cost:context-bloat'` → Slack channel). */ readonly reason?: string; /** * v2.12 — content delivered to the LLM as the synthetic `tool_result` * on `'deny'` and `'halt'`. When omitted, defaults to a deliberately * generic `"Tool '${name}' is not available in this context."` — * NEVER falls back to `reason` (which is telemetry, not user-facing). */ readonly tellLLM?: ToolResultContent; } export interface PermissionChecker { readonly name: string; check(request: PermissionRequest): Promise | PermissionDecision; } export type TokenKind = 'input' | 'output' | 'cacheRead' | 'cacheWrite'; export interface PricingTable { readonly name: string; /** USD per ONE token for the given model+kind. */ pricePerToken(model: string, kind: TokenKind): number; } //# sourceMappingURL=types.d.ts.map