/** * Strategy interface types for the v2.8 grouped-enabler architecture. * * Pattern: Strategy + Bridge + Hexagonal port. See the design memo * `docs/inspiration/strategy-everywhere.md`. * * Four groups, four typed strategy interfaces. Each follows the same * shape (one canonical contract, locked at the type level): * * 1. `name: string` — registry key for auto-registration * 2. `capabilities: {...}` — what this strategy supports * 3. the hot-path method — sync, side-effect-only. Named per * group: `exportEvent` (observability), * `recordCost`, `renderStatus`, * `renderGraph`. There is no `onEvent`. * 4. `flush?(): Promise` — optional batch flushing * 5. `stop?(): void` — optional teardown * * Design constraints (from the panel review): * - **PASSIVE / non-blocking by construction.** Strategies are * observers — they NEVER block the agent loop. Async work * (HTTP shipment, disk I/O, batching) is the STRATEGY's internal * concern: buffer in `exportEvent` (sync), drain in `flush()` * (async OK). The dispatcher never awaits the hot-path call. * - `exportEvent` MUST be sync `void`. MUST NOT throw. Errors caught + * routed to `_onError` at the dispatch layer; one bad strategy * never breaks the agent loop. * - Idempotent registration — registering the same `name` twice * replaces, doesn't double-fire. * - `stop()` is idempotent — halts everything that strategy enabled, * nothing else, calling twice is a no-op. Since 8.12.0 the framework * also guarantees it is DELIVERED at most once, however many places * share the strategy. * - `flush()` is optional, may be sync OR async — strategies that * don't batch can omit it. Flush is the ONLY async path; the hot * path is always sync. * * **Who calls `flush()` / `stop()` (8.12.0).** Three doors, and no * hidden fourth: * * - `const telemetry = agent.enable.observability({...})` returns a * handle. It is still the `Unsubscribe` function it always was — * `telemetry()` detaches, and detaching still never stops your * strategy — and it now also carries `telemetry.flush()` and * `telemetry.stop()`. * - `await agent.shutdown()` drains and releases everything enabled * on that agent, in the one correct order. * - `standingAgent({...})`'s `handle.close()` flushes by default * (`shutdown: 'flush'`), because the last batch reaching the sink * when a server stops is what everyone already assumed happened. * * `agent.run()` still never flushes: telemetry timing is not the agent * loop's business. Opt into a per-run drain with * `enable.observability({ flushOn: 'run-end' })`, which fires a flush * when a run ends but never gates the run on it. */ import type { Unsubscribe } from '../events/dispatcher.js'; import type { AgentfootprintEvent, AgentfootprintEventType } from '../events/registry.js'; import type { StepGraph } from '../recorders/observability/FlowchartRecorder.js'; import type { StatusState } from '../recorders/observability/status/statusTemplates.js'; /** * What every `enable.*` strategy call returns (8.12.0). * * **It is still the `Unsubscribe` function.** `telemetry()` detaches the * subscription and nothing else, exactly as before — every call site written * against the old return type keeps compiling and keeps behaving identically. * What is new is that the function also carries two methods, so the object * that knows which strategy is attached is the object you can drain: * * ```ts * const telemetry = agent.enable.observability({ strategy: cloudwatch }); * * await telemetry.flush(); // ship what is buffered; keep exporting * telemetry(); // detach; strategy keeps running (unchanged law) * telemetry.stop(); // release the strategy — timers, clients, buffers * ``` * * Or let the scope do it, which cannot be got wrong: * * ```ts * await using telemetry = agent.enable.observability({ strategy: cloudwatch }); * ``` * * `flush()` enforces the shutdown ORDER internally, which is the part no * consumer could do from outside: it drains the detach hop first (events * scheduled on a `detach` driver have not reached your strategy yet), then the * strategy's own buffer. `stop()` is refcounted — see `BaseStrategy.stop`. */ export type StrategyHandle = Unsubscribe & AsyncDisposable & { /** * Ship everything this subscription has produced: first the events still * queued on a `detach` driver, then the strategy's own buffer. Safe to * call repeatedly and concurrently; never throws at you (a failing * exporter reports through its own `_onError`). */ flush(): Promise; /** * Release this subscription AND, if it was the last one pointing at the * strategy, the strategy itself — timers cleared, clients closed. A * strategy another runner still shares is left running; stopping is * delivered at most once, ever. * * Does not flush first: call `flush()` before it, or use * `agent.shutdown()` / `await using`, which do. */ stop(): void; }; /** `StrategyHandle` under the name the observability door returns it. */ export type ObservabilityHandle = StrategyHandle; /** * Common base every strategy carries. Per-group strategies extend this * with their own typed hot-path method (`exportEvent`, `recordCost`, * `renderStatus`, `renderGraph`) + capability shape. */ export interface BaseStrategy { /** Registry key. Conventionally lowercase-kebab: `'datadog'`, * `'agentcore'`, `'cloudwatch'`. Used to look up the strategy from * config + de-dupe registrations. */ readonly name: string; /** Optional batch flush — drain whatever the hot path buffered. * Returns `void` for sync sinks (Pino-style) OR `Promise` for * async sinks (Datadog HTTP batch, OTel BatchSpanProcessor). * * **Called for you on shutdown (8.12.0), never during a run.** The * handle returned by `enable.observability()` carries `.flush()`, * `await agent.shutdown()` calls it for everything enabled on that * agent, and a `standingAgent` flushes on `close()` by default. * Before 8.12.0 nothing called it, and a batching exporter * (CloudWatch, X-Ray) dropped its final batch whenever the process * exited inside its flush window. * * Write it to be safe to call at any time, including twice * concurrently and after your own `stop()` — the shipped AWS * adapters are (a stop cancels the timer and stops accepting; it * does not discard what was already accepted). * * ```ts * const telemetry = agent.enable.observability({ strategy }); * // … * await telemetry.flush(); // ship what is buffered, keep running * await agent.shutdown(); // drain + release everything enabled * ``` * * What still does NOT call it: `agent.run()`. Awaiting an exporter * inside a run would make telemetry a run-latency term. Opt into a * per-run drain with `enable.observability({ flushOn: 'run-end' })` * — it fires when a run ends, and does not gate the run. */ flush?(): void | Promise; /** Optional teardown — close clients, clear timers, release handles. * Idempotent: calling twice is a no-op. Strategies that open no * external resources can omit this. * * **Terminal**: after it, events are dropped and there is no * restart. So the framework is careful about who may call it: * * - The `Unsubscribe` — `telemetry()` — NEVER stops your strategy. * It releases one subscription, exactly as it always did. This is * what lets one strategy instance be enabled, unsubscribed and * enabled again (the audit-export pattern) without losing the * second half of its record. * - `telemetry.stop()` and `agent.shutdown()` DO stop it — but only * once the last subscription pointing at it has been released, * so one runner's shutdown can never blind another runner that * still shares the strategy. * - Delivered AT MOST ONCE per strategy instance, whoever asks. * * Flush first, then stop — both doors above already do. */ stop?(): void; /** * Optional event-type filter. When set, the dispatcher only forwards * events whose `type` is in this set — saves the strategy from * filtering itself + reduces hot-path allocations. * * Per AWS CloudWatch panel review: storage cost scales with size, * strategies need to declare what they consume so the framework * doesn't force them to inspect everything. */ readonly relevantEventTypes?: readonly AgentfootprintEventType[]; /** * Optional config validator. Called ONCE at registration time — * throws if the strategy's options are invalid (wrong API key shape, * missing peer dep, unreachable endpoint). Saves customer-support * "why is my dashboard empty?" tickets. * * Per New Relic panel review. */ validate?(): void; /** * Optional error sink. Called when this strategy itself errors — * e.g., HTTP 401 from Datadog, malformed config in pino. Default * dispatcher behavior is to swallow + log to console (so one bad * exporter doesn't kill the agent loop). Consumers wire this when * they want to surface vendor errors in their own tooling. * * **This is a property you ASSIGN AFTER CONSTRUCTION, not a * constructor option**, because it lives on the strategy object * rather than in any factory's options: * * ```ts * const strategy = cloudwatchObservability({ logGroupName: '/agent' }); * strategy._onError = (err, event) => myLogger.warn(err, event?.type); * ``` * * Since 8.11.0 the AWS adapters (`cloudwatchObservability`, * `agentcoreObservability`, `xrayObservability`, `otelObservability`) * also accept an `onError` option in their factory options — that is * the preferred door, since it is wired before the first delivery can * fail. `_onError` stays for strategies you write yourself and for * adapters that take no options. * * Per New Relic panel review. */ _onError?(error: Error, event?: AgentfootprintEvent): void; } /** * Capabilities a strategy declares — matches OTel's 4-signal model * (events / logs / traces / metrics). A strategy can opt into any * subset. `compose([...])` ORs the children's capabilities. * * - `events: true` → wide structured events (Honeycomb / OTel * events / Datadog wide events). agentfootprint * events are this shape natively — most * strategies should default to `events: true`. * - `logs: true` → flat log records (pino, console, CloudWatch * Logs). The strategy reduces a wide event to * a single log line. * - `traces: true` → strategy maps events to spans (parent/child * via `runtimeStageId`). * - `metrics: true` → strategy aggregates counters / gauges * (CloudWatch metrics, Mimir, Prometheus). */ export interface ObservabilityCapabilities { readonly events?: boolean; readonly logs?: boolean; readonly traces?: boolean; readonly metrics?: boolean; } /** * The single hot-path entry every observability strategy implements. * Receives every typed agent event. MUST be sync + side-effect-only + * non-throwing. * * Strategies that batch should buffer in `exportEvent` and drain in * `flush()` — which the consumer, not the framework, must call. */ export interface ObservabilityStrategy extends BaseStrategy { readonly capabilities: ObservabilityCapabilities; /** * Translate the typed agentfootprint event into the vendor's wire * format and ship it to the destination (Datadog API, OTel exporter, * pino stream, CloudWatch PutLogEvents, etc.). * * MUST be sync `void`. Buffer internally; drain in `flush()`. */ exportEvent(event: AgentfootprintEvent): void; } /** * What a cost strategy receives every time the cost recorder fires. * Carries enough info for the strategy to decide whether to ship to * billing, log a warning, trigger a circuit breaker, etc. */ export interface CostTick { readonly cumulativeInputTokens: number; readonly cumulativeOutputTokens: number; readonly cumulativeCostUsd: number; readonly recentInputTokens: number; readonly recentOutputTokens: number; readonly recentCostUsd: number; readonly model: string; /** * The provider that billed it, by `LLMProvider.name` — the other half of * attribution beside {@link model}, since one model id can be served by * several providers at different prices (Bedrock and Anthropic both answer * to a Claude model id). * * Optional because `CostTickPayload.provider` is: a window/compaction * strategy reports summarizer spend through `WindowStrategyResult.spend`, * and the provider is read from that strategy's `billing` declaration, which * the seam leaves optional. Absent means "the strategy that billed this did * not say", never "the agent's provider" — so a billing sink must not * substitute one. */ readonly provider?: string; /** Loop turn this spend belongs to, from the event envelope (`meta.iterIndex`). */ readonly iteration?: number; /** footprintjs stage key this spend belongs to, from the event envelope. */ readonly runtimeStageId?: string; } export interface CostCapabilities { /** Strategy supports per-tick streaming. `false` for batch-only sinks. */ readonly streaming?: boolean; /** Strategy supports budget enforcement (will throw / break the loop * when budget exceeded). Most strategies are observe-only. */ readonly enforcement?: boolean; } export interface CostStrategy extends BaseStrategy { readonly capabilities: CostCapabilities; /** * Translate the cost tick into the vendor's wire format and ship it * (Stripe billing API, accounting webhook, internal metrics sink). * * MUST be sync `void`. Buffer internally; drain in `flush()`. */ recordCost(tick: CostTick): void; } /** * What a status strategy receives every time `selectStatus` * returns a new state. The renderer has already resolved templates to * a final string; strategies decide where to send it. */ export interface StatusUpdate { /** Rendered status line (already template-resolved). */ readonly line: string; /** Underlying state for strategies that want to format their own * view (e.g., emit different colors per state in a TUI). */ readonly state: StatusState; } export interface LiveStatusCapabilities { /** Strategy supports streaming partial tokens (vs only state * transitions). */ readonly streaming?: boolean; } export interface LiveStatusStrategy extends BaseStrategy { readonly capabilities: LiveStatusCapabilities; /** * Render the rendered status line to the strategy's destination * (chat bubble callback, stdout, webhook). * * MUST be sync `void`. */ renderStatus(update: StatusUpdate): void; } /** * What a Lens strategy receives — the live StepGraph each time the * boundary recorder fires an event that changes the visible structure. * Strategies decide how to render: DOM (browser), TUI (CLI), JSON * (capture for replay). */ export interface LensUpdate { readonly graph: StepGraph; /** Whether this is the FINAL update (run finished). Strategies that * buffer for animation can flush here. */ readonly final: boolean; } export interface LensCapabilities { /** Strategy renders to a UI (browser DOM, TUI). */ readonly interactive?: boolean; /** Strategy serializes for replay / export. */ readonly serializable?: boolean; } export interface LensStrategy extends BaseStrategy { readonly capabilities: LensCapabilities; /** * Render the live StepGraph to the strategy's destination (DOM, * TUI, JSON serializer). * * MUST be sync `void`. */ renderGraph(update: LensUpdate): void; } /** * Discriminated union for the `compose([...])` combinator and the * registry. Lets the registry hold one Map while * preserving type narrowing per-group via the `kind` discriminator. */ export type AnyStrategy = ({ readonly kind: 'observability'; } & ObservabilityStrategy) | ({ readonly kind: 'cost'; } & CostStrategy) | ({ readonly kind: 'liveStatus'; } & LiveStatusStrategy) | ({ readonly kind: 'lens'; } & LensStrategy); export type StrategyKind = AnyStrategy['kind']; /** * Cost-of-on knob (per Datadog panel review). Each tier is a soft * suggestion — strategies decide what to do per tier (e.g., a * `pino` strategy might gzip on `firehose`, an OTel strategy might * raise its `BatchSpanProcessor` interval). */ export type ObservabilityTier = 'minimal' | 'standard' | 'firehose'; /** * Detach mode — controls whether the strategy's hot-path call * (e.g. `exportEvent`) runs sync inside the agent loop or is deferred * onto a `footprintjs/detach` driver so the loop never blocks. * * Three semantics: * * - `'forget'` — `detachAndForget`. Discard the handle. Pure * fire-and-forget telemetry. Errors land on the * (discarded) handle and go silent unless the * strategy's own `_onError` surfaces them. Use for * high-volume exports where dropping a single event * is acceptable. * * - `'join-later'` — `detachAndJoinLater`. The driver returns a * `DetachHandle`; we deliver it to your * `onHandle` callback so you can `await` later * (graceful shutdown, tests, backpressure). * * - omitted (default sync) — strategy hot-path runs inline, same as * every release before v2.8. * * For graceful shutdown — `await handle.flush()` (or `agent.shutdown()`). * Since 8.12.0 that drains the events this subscription scheduled on the * driver BEFORE flushing the strategy, which is the order that matters and * the one you could not write yourself: a detached event has not reached your * strategy's buffer yet, so flushing the strategy alone ships nothing. * `flushAllDetached()` (from `'footprintjs/detach'`) remains the * process-wide hammer for detached work this library did not schedule. */ export interface DetachOptions { /** The driver to schedule on. Required — there is no library * default. Pick by environment: `microtaskBatchDriver` (cross- * runtime, default for in-process), `setImmediateDriver` (Node), * `setTimeoutDriver` (cross-runtime, configurable delay), * `sendBeaconDriver` (browser, survives page-unload), etc. */ readonly driver: import('footprintjs/detach').DetachDriver; /** `'forget'` discards the handle (pure telemetry). `'join-later'` * delivers the handle to `onHandle` for later awaiting. Default * `'forget'`. */ readonly mode?: 'forget' | 'join-later'; /** Required when `mode === 'join-later'`. Receives every minted * handle. Push to a closure-local array if you want * `Promise.all(handles.map(h => h.wait()))` later, or keep a * rolling window for backpressure. */ readonly onHandle?: (handle: import('footprintjs/detach').DetachHandle) => void; } /** * Common options every group accepts. Per-group enablers extend with * their own keys (e.g., `templates` for liveStatus, `budget` for cost). */ export interface CommonStrategyOptions { /** * Strategy implementation. Defaults differ per group: * - observability → `console()` * - cost → `inMemorySink()` * - liveStatus → consumer-provided callback * - lens → `browser()` (when in DOM) / `noop()` (else) */ readonly strategy?: BaseStrategy; /** 0..1 — fraction of events to forward. 1.0 = all, 0 = none. * Per-Datadog panel review: every observability enabler accepts * this. */ readonly sampleRate?: number; /** Opt-in detach. When set, the strategy's hot-path call (e.g. * `exportEvent`) is scheduled on the given driver instead of * running inline — agent loop never blocks on slow exporters. * See `DetachOptions` for the three semantics. */ readonly detach?: DetachOptions; /** * When to flush automatically (8.12.0). Default `'manual'` — the framework * flushes on shutdown (`handle.flush()`, `agent.shutdown()`, a * `standingAgent` closing) and at no other time. * * `'run-end'` additionally fires a flush when a run finishes: an Agent's * `agent.turn_end`, a composition's `composition.exit`. Use it for scripts * and short-lived processes (a Lambda, a cron job) where "the process may * vanish right after the answer" is the normal case. * * **It fires the flush; it does not gate the run.** `run()` resolves when * the run is done, exactly as it does today — the drain happens alongside, * and a process that exits in the same breath can still outrun it. The * honest one-liner for that case is `await handle.flush()` (or * `await agent.shutdown()`) after the run; this option shrinks the window, * it does not close it. Nothing was made to await inside `run()` because * telemetry must never become a term in run latency. */ readonly flushOn?: 'manual' | 'run-end'; } //# sourceMappingURL=types.d.ts.map