/** * Federated Budget Tracking * * Tracks token spending across distributed agent swarms. Each `FederatedBudget` * instance enforces a global ceiling shared among all agents that call `spend()`. * * When an optional `BlackboardBackend` is supplied, the budget state is written * to the blackboard after every mutation. Wiring a `CrdtBackend` or `RedisBackend` * as the underlying backend therefore gives automatic cross-node synchronization * with no extra configuration. * * Architecture: * - In-memory `spent` map keyed by `agentId` holds per-agent cumulative totals. * - `spend()` is synchronous and enforces both the global ceiling and an optional * per-agent ceiling in a single check. * - A `blackboard` backend (if supplied) stores a JSON snapshot under `budgetKey` * after every `spend()` / `reset()` / `setCeiling()` call so distributed nodes * can read the latest state. * - `loadFromBlackboard()` deserializes a previously saved snapshot so a * restarted node can recover its prior accumulated spend. * * @example * ```typescript * import { FederatedBudget } from 'network-ai'; * * const budget = new FederatedBudget({ ceiling: 10_000 }); * * budget.spend('agent-1', 3000); // { allowed: true, remaining: 7000 } * budget.spend('agent-2', 8000); // { allowed: false, remaining: 7000 } * budget.remaining(); // 7000 * budget.getSpendLog(); // { 'agent-1': 3000 } * ``` * * @example With blackboard persistence * ```typescript * import { CrdtBackend } from 'network-ai'; * import { FederatedBudget } from 'network-ai'; * * const node = new CrdtBackend('node-a'); * const budget = new FederatedBudget({ ceiling: 50_000, blackboard: node }); * * budget.spend('agent-1', 1000); * // State is now stored in node under 'federated-budget' * // Sync node to other CrdtBackend nodes to propagate the spend. * ``` * * @module FederatedBudget * @version 1.0.0 * @license MIT */ import type { BlackboardBackend } from './blackboard-backend'; /** * Result returned by {@link FederatedBudget.spend}. */ export interface SpendResult { /** Whether the spend was allowed (i.e. did not breach any ceiling). */ allowed: boolean; /** Remaining tokens in the global pool after this call. */ remaining: number; /** Reason the spend was denied, if `allowed` is `false`. */ deniedReason?: 'global_ceiling' | 'per_agent_ceiling'; } /** * A single entry in the spend log returned by {@link FederatedBudget.getSpendLog}. */ export interface SpendLogEntry { /** Agent that made the spend. */ agentId: string; /** Number of tokens spent in this transaction. */ tokens: number; /** ISO timestamp of the spend. */ timestamp: string; } /** * Construction options for {@link FederatedBudget}. */ export interface FederatedBudgetOptions { /** * Global token ceiling shared across all agents. * No single call to `spend()` may push the cumulative total above this value. * Must be a positive integer. */ ceiling: number; /** * Optional per-agent ceiling. * When set, each individual agent is also capped at this many cumulative tokens, * independently of the global ceiling. * Must be a positive integer if provided. */ perAgentCeiling?: number; /** * Optional blackboard backend. * When provided, budget state is persisted under `budgetKey` after every * mutation so distributed nodes can observe the latest spend. * * Pair with a `CrdtBackend` or `RedisBackend` for automatic multi-node sync. */ blackboard?: BlackboardBackend; /** * Key used to store the budget snapshot on the blackboard. * Defaults to `'federated-budget'`. */ budgetKey?: string; /** * Agent identifier used as the `agentId` when writing to the blackboard. * Defaults to `'federated-budget-tracker'`. */ blackboardAgent?: string; } /** * Federated token-budget tracker for distributed agent swarms. * * Enforces a shared global ceiling across all agents and optionally an * individual per-agent ceiling. State can be persisted to any * `BlackboardBackend` for cross-node visibility. */ export declare class FederatedBudget { private readonly _ceiling; private _dynamicCeiling; private readonly _perAgentCeiling; private readonly _spent; private _totalSpent; private readonly _log; private readonly _blackboard; private readonly _budgetKey; private readonly _bbAgent; constructor(options: FederatedBudgetOptions); /** * Attempt to spend `tokens` on behalf of `agentId`. * * The spend is allowed only when: * 1. `totalSpent + tokens <= ceiling` (global ceiling not breached), AND * 2. `agentSpent + tokens <= perAgentCeiling` if a per-agent ceiling is set. * * When allowed the internal counters are updated and the state is persisted * to the blackboard (if configured). * * @param agentId Identifier of the spending agent. Must be a non-empty string. * @param tokens Number of tokens to spend. Must be a positive integer. * @returns `SpendResult` with `allowed`, `remaining`, and optional `deniedReason`. */ spend(agentId: string, tokens: number): SpendResult; /** * Remaining tokens in the global pool. */ remaining(): number; /** * Total tokens spent across all agents. */ getTotalSpent(): number; /** * Tokens spent by a specific agent. Returns `0` if the agent has no spend. */ getAgentSpent(agentId: string): number; /** * Per-agent spend totals as a plain object: `{ agentId: totalTokens }`. */ getSpendLog(): Record; /** * Detailed transaction log — every individual `spend()` call in order. */ getTransactionLog(): SpendLogEntry[]; /** * Current global ceiling (may differ from the original if `setCeiling()` was called). */ getCeiling(): number; /** * Per-agent ceiling, or `undefined` if none was configured. */ getPerAgentCeiling(): number | undefined; /** * Dynamically adjust the global ceiling. * * The new ceiling must be a positive finite number. It may be set below * the current `totalSpent` (no previously approved spends are reversed, but * future spends will be denied until tokens are freed by a `reset()`). * * @param ceiling New global ceiling. */ setCeiling(ceiling: number): void; /** * Reset all spend counters and clear the transaction log. * * The global ceiling is preserved (its current value after any `setCeiling()` * calls). After a reset `remaining() === getCeiling()`. * * The blackboard entry (if configured) is updated with the reset state. */ reset(): void; /** * Spawn a child budget whose ceiling is drawn from this budget's remaining pool. * * RLM-style recursive budget propagation: the child gets an isolated spend * tracker capped at `childCeiling` (or the full remaining amount if omitted). * Call `commit()` on the returned handle to propagate the child's total spend * back to the parent as a single atomic `spend()` call. * * This enables recursive sub-agent trees where each level can only consume * what its parent has left, and costs bubble back up the chain automatically. * * @example * ```typescript * const root = new FederatedBudget({ ceiling: 10_000 }); * const { budget: child, commit } = root.spawnChild('sub-agent-A', 3000); * child.spend('worker-1', 800); * child.spend('worker-2', 600); * commit(); // propagates 1400 tokens back to root * root.getTotalSpent(); // 1400 * ``` * * @param spenderId agentId recorded on the parent when `commit()` is called. * @param childCeiling Optional upper bound for the child (defaults to `remaining()`). * @returns Object with the child `FederatedBudget` and a `commit` function. */ spawnChild(spenderId: string, childCeiling?: number): { budget: FederatedBudget; commit: () => SpendResult; }; /** * Restore budget state from the blackboard backend. * * Reads the entry stored under `budgetKey`, deserializes the snapshot, and * replaces the current in-memory state. Useful when a node restarts and * needs to recover its prior accumulated spend. * * No-op (returns `false`) when no blackboard is configured or no entry exists. * * @returns `true` if state was successfully loaded, `false` otherwise. */ loadFromBlackboard(): boolean; /** Serialize current state to the blackboard, if one is configured. */ private _persist; } //# sourceMappingURL=federated-budget.d.ts.map