agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
96 lines (95 loc) • 4.22 kB
TypeScript
/**
* cost — shared cost-accounting helper emitted by LLMCall + Agent.
*
* Pattern: Strategy (PricingTable port) + Event emission (typedEmit).
* Role: core/ layer. When a runner is configured with a PricingTable,
* every LLM response drives a `cost.tick` event carrying per-call
* tokens/USD plus cumulative run totals. When a `costBudget` is
* also set, the first crossing emits `cost.limit_hit` with
* `action: 'warn'` (library never auto-aborts; consumers decide).
* Emits: agentfootprint.cost.tick
* agentfootprint.cost.limit_hit
*/
import type { PricingTable } from '../provider-contract/types.js';
export interface CostAccountingScope {
cumTokensInput: number;
cumTokensOutput: number;
cumEstimatedUsd: number;
costBudgetHit: boolean;
}
/**
* What a `costBudget` accepts, and what it does when crossed (8.14.0).
*
* A bare `number` is `{ usd, onExceed: 'warn' }` — byte-for-byte what every
* release before 8.14.0 did — so no existing agent changes behaviour.
*/
export type CostBudget = number | {
readonly usd: number;
readonly onExceed: 'warn' | 'halt';
};
/** Normalized form. */
export interface ResolvedCostBudget {
readonly usd: number;
readonly onExceed: 'warn' | 'halt';
}
/**
* Normalize a `costBudget`, refusing a shape that cannot mean anything.
*
* `'halt'` is refused for `LLMCall`: halting means "stop at the next iteration
* boundary", and one call has no next boundary. Accepting it there would give
* a consumer a stop button wired to nothing — the same silent no-op
* {@link assertCostBudgetHasPricing} exists to remove.
*
* @param runner the class name for the message — `'Agent'` or `'LLMCall'`.
*/
export declare function resolveCostBudget(runner: 'Agent' | 'LLMCall', costBudget: CostBudget | undefined): ResolvedCostBudget | undefined;
/**
* Refuse a `costBudget` with no `pricingTable` to measure it against (8.13.0).
*
* The budget is denominated in USD and {@link emitCostTick} returns on its first
* line when there is no pricing table, so before this refusal the pair emitted
* NOTHING — no `cost.tick`, and no `cost.limit_hit` however much a run spent. A
* budget that cannot be crossed is not a lenient budget; it is a guard rail that
* was never installed, and it looks identical from the outside to one that
* simply has not been hit yet.
*
* Shared by BOTH runners that take the pair. Leaving the sibling class with the
* same silent no-op would be a governance fix that itself drops half the cases.
*
* @param runner the class name for the message — `'Agent'` or `'LLMCall'`.
*/
export declare function assertCostBudgetHasPricing(runner: 'Agent' | 'LLMCall', pricingTable: PricingTable | undefined, costBudget: CostBudget | undefined): void;
type Usage = {
readonly input: number;
readonly output: number;
readonly cacheRead?: number;
readonly cacheWrite?: number;
};
/**
* Who billed this call, and for what.
*
* One argument rather than two so the pair cannot drift: a caller that knows
* the model always has the slot for the provider in front of it, and the one
* caller that genuinely cannot name a provider (a window strategy that reports
* `spend` without declaring `billing`) omits it deliberately instead of by
* forgetting a parameter.
*/
type BilledBy = {
/** `LLMProvider.name`. Omitted only where the caller genuinely cannot know it. */
readonly provider?: string;
/** The model id `pricePerToken` is asked about. */
readonly model: string;
};
/**
* Emit `cost.tick` for the just-completed LLM response and, if the
* consumer set a `costBudget`, emit a one-shot `cost.limit_hit` the first
* time cumulative USD crosses the budget. Does nothing when no
* `pricingTable` is configured — zero overhead on runs without costing.
*
* Scope must carry the running cumulative counters; callers seed them
* in their Seed stage.
*/
export declare function emitCostTick(scope: CostAccountingScope & {
$emit: (name: string, payload?: unknown) => void;
}, pricingTable: PricingTable | undefined, costBudget: ResolvedCostBudget | undefined, billedBy: BilledBy, usage: Usage): void;
export {};