UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

112 lines (111 loc) • 6.26 kB
import type { TaskResult } from "../core/types.js"; /** * Shared CUMULATIVE-STATS constructor for the multi-leg agent compositions (cascade rungs, repair-loop * attempts, teacher student attempts). All three run `runner.runTask` MORE THAN ONCE and must report the * OPERATION's totals, not the last leg's. * * The invariant this exists to hold in ONE place: **every field the accumulator tracks is OVERRIDDEN over * the base stats**. A field left un-overridden silently leaks the LAST leg's value dressed up as a * cumulative total (the cascade R1-m5 / R3 trap; repair-loop shipped exactly that hole on `humanReview`). * Three independent copies of this literal is how that hole survived in one copy while the other two were * fixed — hence one constructor, three call sites. * * OWN and NESTED stay DISJOINT (the cascade R-5 / teacher A-7 convention): a consumer adds * `stats.X + stats.nested.X` for the true total; nested cost is never folded into `costMicroUsd`. */ /** Cumulative delegated-subagent (nested) totals, summed across every leg. */ export interface CumulativeNestedTotals { tokens: number; turns: number; tasks: number; costMicroUsd: number; } /** * Cumulative human-review (design/91 C2 axis) burden, summed across every leg. `gates` is the ordered * concatenation of each leg's gate entries — the element shape is the canonical one published on * {@link TaskResult}, so a leg's entry passes through with `toolName`/`toolArg` intact. */ export interface CumulativeHumanReviewTotals { count: number; totalWaitMs: number; gates: NonNullable<TaskResult["stats"]["humanReview"]>["gates"]; } /** * The running totals a multi-leg composition accumulates. Every field is REQUIRED: an optional field here * would re-open the exact "forgot to override one" hole this module exists to close — a caller that has * nothing to report passes zeros. */ export interface CumulativeStatsAccumulator { tokens: number; turns: number; promptTokens: number; /** RB-457-a: the cache-INCLUSIVE input total — its own leg, no longer derivable from `promptTokens` (the * cache-MISS sum). Also the cache-hit-rate denominator. */ totalInputTokens: number; cachedTokens: number; outputTokens: number; cacheWriteTokens: number; cacheWriteTokensLong: number; /** * Cumulative own LLM cost (micro-USD) EXACTLY as the caller wants it published. Callers differ on the * zero case on purpose and this constructor does not adjudicate: cascade/repair-loop publish a plain `0` * (a run happened, it cost nothing measurable), teacher collapses `0` to `undefined` (the RB-368 * "unpriced spend is absent, not free" posture). Unifying the two is a BEHAVIOR change, not a refactor. * * Ignored entirely when {@link anyLegUnpriced} is set — see there. */ costMicroUsd: number | undefined; /** * TRUE when ANY contributing leg published no `costMicroUsd`, i.e. it ran on a model with neither a * `RunnerDeps.pricing` entry nor a `Model.cost` declaration (`stats.costMicroUsd` is contractually * ABSENT there — types.ts / RB-368). * * Accumulating such a leg as `?? 0` publishes a total that is not one: the operation's real cost is * unknown, and reporting the priced legs' partial sum makes "no price table" indistinguishable from * "declared free" — the exact conflation the single-leg face refuses. So the knownness travels with the * money: when this is set, `costMicroUsd` AND `costBreakdown` are both omitted from the cumulative * stats, matching what a single unpriced `runTask` already reports. Token/turn totals are unaffected * (they are knowable regardless of pricing). */ anyLegUnpriced: boolean; /** Cumulative root-agent LLM cost for the `costBreakdown` taxonomy. */ llmRootMicroUsd: number; /** Cumulative within-task compaction LLM cost for the `costBreakdown` taxonomy. */ compactionMicroUsd: number; nested: CumulativeNestedTotals; humanReview: CumulativeHumanReviewTotals; } /** * Build the cumulative stats over a base (the FINAL leg's stats — it carries the non-accumulated fields * such as `model`) with every accumulated field overridden. * * `cacheHitRate` is RE-DERIVED from the cumulative cached/total-input totals rather than spread from the * base: a ratio is not additive, so the last leg's rate is never the operation's rate. */ export declare function buildCumulativeStats(base: TaskResult["stats"], acc: CumulativeStatsAccumulator): TaskResult["stats"]; /** A running {@link CumulativeStatsAccumulator} with the per-leg fold built in. */ export interface CumulativeStatsTracker { /** Fold ONE leg's stats into the running totals. Call once per leg, in dispatch order. */ add(s: TaskResult["stats"]): void; /** Build the cumulative stats over `base` — conventionally the FINAL leg's stats (it carries the * non-accumulated fields such as `model`). */ build(base: TaskResult["stats"]): TaskResult["stats"]; /** How many legs have been folded in. A composition that ran exactly ONE leg can use this to hand that * leg's own stats back untouched: the cumulative account over one leg IS that leg, and re-deriving it * would rewrite a caller's object (0 → `undefined` normalization, a re-derived `cacheHitRate`) for no * gain. */ readonly legs: number; } /** * Create a {@link CumulativeStatsTracker}. * * {@link buildCumulativeStats} made the OUTPUT literal single-source, but the INPUT fold stayed copied: * cascade / repair-loop / teacher each hand-maintain the same dozen `+=` lines, so a new stats field still * has to be remembered in four places — the same duplication one level down from the hole this module * exists to close. New call sites take this factory instead. Migrating the three originals onto it is a * separate change (their folds are equivalent to this one today, but rewriting three shipped compositions * is not part of a cost-accounting bug fix). * * OWN and NESTED stay DISJOINT here too: `add` never folds a leg's `nested.*` into the own totals. */ export declare function createCumulativeStatsTracker(): CumulativeStatsTracker;