UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

112 lines (111 loc) • 6.14 kB
import type { TaskResult } from "../core/types.js"; /** * Shared CUMULATIVE-STATS constructor for the multi-leg agent compositions (cascade rungs, repair-loop * attempts, verify legs). Each runs `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`). * Independent copies of this literal is how that hole survived in one copy while the others were fixed — * hence one constructor. * * OWN and NESTED stay DISJOINT (the cascade R-5 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): a plain `0` when every leg was priced and cost nothing measurable. * * Ignored entirely when {@link anyLegUnpriced} is set — see there. */ costMicroUsd: number; /** * 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; /** Cumulative side-call (permission classifier, mods) LLM cost — the "of which" line of `costBreakdown`, INSIDE * `llmRootMicroUsd`, never a partition member. Undefined until a leg booked one: the cumulative key is then absent too. */ sideCallMicroUsd?: 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 each hand-maintain the same dozen `+=` lines, so a new stats field still has to be * remembered in three places — the same duplication one level down from the hole this module exists to * close. New call sites take this factory instead. Migrating the two originals onto it is a separate change * (their folds are equivalent to this one today, but rewriting 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;