@sema-agent/core
Version:
Stateless, task-oriented AI agent core
54 lines (53 loc) • 3.75 kB
TypeScript
import type { AgentMessage } from "../internal/harness-types.js";
import { type ToolResultStore } from "./tool-result-store.js";
/**
* Per-message aggregate tool-result budget (design/64 §17.2 / §24 — CC `MAX_TOOL_RESULTS_PER_MESSAGE_CHARS`,
* `constants/toolLimits.ts:49`). A single turn's batch of N parallel tool results can each be under the
* per-tool offload threshold yet SUM past the model/API limit (10×40K = 400K). This caps the aggregate of
* one turn's tool_result batch: while over budget, the LARGEST fresh non-exempt result is offloaded to a
* preview (largest-first), repeating until the batch fits.
*
* **Simpler than CC by construction (§24.2):** a request-only, non-destructive transform (returns a new
* array; the durable session keeps full results) applied in the existing `harness.on("context")` hook —
* the same per-query point as `clearStaleToolResults` (the mirror of CC's query.ts:379). It is DETERMINISTIC
* (stable `ref = tr_<sessionId>~<toolCallId>` + deterministic {@link buildPreview}), so re-running it every
* query yields byte-identical previews → prompt-cache safe **without** CC's ContentReplacementState freeze
* machine (the determinism IS the freeze), and resume-free (the transcript holds originals; this re-applies
* on replay). Reuses design/30's offload store + preview format; with no store it falls back to a
* self-contained head+tail truncation preview that still bounds the request.
*/
/** CC's per-message cap (chars, not tokens — the "100K token" figure in old notes was unsourced). */
export declare const AGGREGATE_TOOL_RESULT_BUDGET_CHARS = 200000;
export interface AggregateBudgetOptions {
/** Per-batch char budget. Default {@link AGGREGATE_TOOL_RESULT_BUDGET_CHARS}. */
budgetChars?: number;
/** Tool names exempt from the budget (their own caps bound them) — e.g. read_file, read_tool_result. */
exemptTools?: ReadonlySet<string>;
/** The wire name a result's recorded tool name resolves to on the leg's roster (an exemption is judged on the tool the
* call ran, not the spelling the model used). Absent ⇒ the recorded name. */
toolNameOf?: (recorded: string) => string;
/** design/30 store for offload+read-back. Absent ⇒ self-contained truncation preview. */
store?: ToolResultStore;
/** Namespaces the stable ref so an offloaded result is globally addressable. */
sessionId: string;
/** C8: fired once per result actually capped (offloaded OR degraded to a truncation
* preview). `storeFallback` = the model LOST read-back for this result (store.put failed / no store)
* — the silent-degradation face this callback exists for. Must not throw (caller-side contract). */
onCapped?: (info: {
tool?: string;
sizeChars: number;
storeFallback: boolean;
}) => void;
/** Fired once per batch that is STILL over budget after both passes — i.e. the cap did what it
* could and the request is going out oversized anyway. Reachable when a batch holds more results than
* the budget can hold even at the minimum preview size (a compact preview has a floor, and N of them
* cannot be smaller than N × that floor). Before this existed the shortfall was entirely silent, so a
* caller could not tell "capped and fits" from "capped and still 2× over". Must not throw. */
onBudgetUnreachable?: (info: {
batchChars: number;
budgetChars: number;
results: number;
}) => void;
}
/** Cap each over-budget turn-batch (run of consecutive tool_result messages). Request-only; see file docs. */
export declare function capAggregateToolResults(messages: AgentMessage[], opts: AggregateBudgetOptions): Promise<AgentMessage[]>;