agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
66 lines (65 loc) • 3.48 kB
TypeScript
/**
* strategies/lifecycle — who is still using a strategy, and who may stop it.
*
* A strategy is a plain object a consumer builds and hands to `enable.*`. The
* same object can legitimately be handed over more than once: to two runners
* (an Agent and the Sequence that contains it), twice to one runner, or to the
* same runner again after an unsubscribe — the shipped audit-export example
* does exactly that, deliberately, to collect two runs into one hash chain.
*
* `stop()` is terminal in every adapter we ship: events exported afterwards
* are dropped and there is no restart. So "stop it when someone lets go of it"
* is wrong — it would silently un-record the second half of that example. This
* module holds the one fact that makes stopping safe:
*
* **how many live subscriptions still point at this strategy.**
*
* Three laws, and they are the whole module:
*
* 1. An `Unsubscribe` NEVER stops a strategy. It releases one subscription.
* That is the compatibility law: code written before this existed keeps
* the behaviour it was written against.
* 2. A strategy is stopped only when the last subscription has been released
* AND something explicitly asked for teardown (`handle.stop()`,
* `agent.shutdown()`). Sharing a strategy therefore cannot produce a
* half-stopped pipeline where one runner's shutdown blinds another's.
* 3. `stop()` reaches a strategy AT MOST ONCE, ever. Adapters are already
* idempotent; this makes the guarantee the framework's rather than the
* adapter author's.
*
* A `WeakMap` keyed on the strategy object holds the count, so nothing here
* retains a strategy that the consumer has dropped, and cross-runner sharing
* needs no global list.
*/
import type { BaseStrategy } from './types.js';
/**
* `Symbol.asyncDispose` where the runtime has it (Node ≥ 20.4 — every engine
* this package supports), and the registered fallback where it does not, so a
* handle's disposal method is never keyed on `undefined`. ONE owner, because
* two spellings of this key would make `await using` work on a handle and not
* on the tracked wrapper around it.
*/
export declare const ASYNC_DISPOSE: symbol;
/** One more live subscription. Called by every `attach*Strategy`. */
export declare function retainStrategy(strategy: BaseStrategy): void;
/**
* One fewer live subscription. Called by every `Unsubscribe`.
*
* Deliberately does NOT stop the strategy at zero — see law 1. Idempotent
* below zero: calling an unsubscribe twice releases one subscription, not two.
*/
export declare function releaseStrategy(strategy: BaseStrategy): void;
/** Live subscriptions pointing at this strategy. Diagnostic + tests. */
export declare function subscriptionCount(strategy: BaseStrategy): number;
/** Has this strategy already been stopped? Diagnostic + tests. */
export declare function isStopped(strategy: BaseStrategy): boolean;
/**
* Stop the strategy IF nothing is still subscribed to it and it has not been
* stopped before. Returns what actually happened, so a caller can say why it
* did nothing rather than guessing.
*
* Errors from the strategy's own `stop()` are swallowed: teardown runs on the
* shutdown path, and one vendor's broken `stop()` must not prevent the next
* strategy — or the process — from finishing.
*/
export declare function stopStrategyIfUnused(strategy: BaseStrategy): 'stopped' | 'still-subscribed' | 'already-stopped';