/** * Helpers for one tool invocation inside the tool loop: the frame the * body runs in, how a failure is classified for the model, and the * constants that decide when a tool is removed. prompt.ts decides when * to call them. */ import { type SpillReaders, type ToolIdentity } from "./toolOutputStore.js"; import type { FuncParam, CallType } from "./agencyFunction.js"; import type { AliasCorrection, CanonicalKeysResult } from "../utils/parameterAliases.js"; import { type Run } from "./asyncContext.js"; import type { MessageThread } from "./state/messageThread.js"; import type { StateStack } from "./state/stateStack.js"; /** Unwrap a SUCCESS Result before it goes back to the model: the LLM * should see the tool's value, not the `{__type, success, value}` * envelope (a wrapped envelope makes the model re-derive `.value` and, * worse, reconstruct wrapped objects when echoing them into later tool * arguments — the compile→run CompiledProgram bug). Only the LLM-facing * message unwraps; the Result cached on the branch for Agency code is * untouched. Failures/rejections never reach this point (handled * upstream in the invoke path), but pass through unchanged * defensively. */ export declare function unwrapToolResultForLlm(result: any, toolName: string): any; /** A failed tool is removed only after this many failures (the circuit * breaker against retry spirals), or immediately on the destructive tier. */ export declare const MAX_TOOL_FAILURES = 5; /** Consecutive rejections that remove a tool. Only consecutive: a * successful call resets the count, so a narrow reject rule brushed * against during otherwise-approved work never removes the tool, while * a model rephrasing a refused call with nothing approved in between * loses it quickly. */ export declare const MAX_TOOL_REJECTIONS = 5; export declare const REJECTION_SUFFIX = "Do not call this tool with the same arguments again; the call will not be executed."; export declare const REJECTION_REMOVAL_SUFFIX = "This tool has been rejected too many times and can no longer be called."; export type FailureTier = "destructive" | "neverStarted" | "idempotent" | "neutral"; /** * Run the tool body with `branchStack` recorded on the frame as the tool * invocation's stack, so endTurn() and handBack() mark this invocation * from anywhere inside the body, including its parallel, fork, and async * branches, whose frames copy the slot. */ export declare function runAsToolInvocation(run: Run, branchStack: StateStack, invoke: (run: Run) => Promise): Promise; /** * Run `invoke` in a copy of the current ALS frame whose `threads` slot is * a fresh, empty ThreadStore. The body keeps the frame's `ctx` and * `stack`, which branch-aware cancellation and per-branch state depend * on. It must not keep `threads`: an `llm()` call in the body would push * onto the outer prompt's thread, whose last message is the assistant's * tool call, a shape OpenAI rejects ("An assistant message with * 'tool_calls' must be followed by tool messages"). A handoff function is * the exception; see runInvokeStep. * * The store is bare, not `withDefaultActive`, so a leaf tool that never * calls llm() does not log a phantom default thread. */ export declare function invokeOnFreshThreadStore(run: Run, invoke: (run: Run) => Promise): Promise; /** * Run `invoke` in a copy of the current ALS frame whose `threads` slot is * a view of the caller's store with `thread` active. The view has its own * active stack, so two prompts running at once (two `async llm()` calls, * say) cannot interleave pushes and pops on a shared one. */ export declare function invokeOnThread(run: Run, thread: MessageThread, scopeKey: string, invoke: (run: Run) => Promise): Promise; /** Classify a tool failure. Most-specific fact wins: a started destructive * operation, then a proved-nothing-ran, then the tool's own idempotent * declaration, else neutral. */ export declare function failureTier(f: { destructiveRan?: boolean; neverStarted?: boolean; }, markers?: { idempotent?: boolean; }): FailureTier; export declare const TIER_SUFFIX: Record; /** Default cap on characters of a single tool result fed back to the * LLM. A recursive `ls`/`grep` can return megabytes; without a cap one * tool call can blow the context window. The FULL result is still * returned to Agency code — only what the model sees is truncated. */ export declare const DEFAULT_TOOL_RESULT_CHARS = 100000; /** Coerce an arbitrary tool result to the string the LLM would see. * Strings pass through; everything else is JSON-stringified, with a * `String()` fallback for values JSON can't represent (e.g. circular). */ export declare function stringifyToolResult(result: any, indent?: number): string; /** What the call's `spillToolResultsOver` says: save a result longer than * `over` in the run, and name the readers by the names the model sees. */ export type SpillSettings = { over: number; run: Run; readers: SpillReaders; }; /** The spill settings of one `llm()` call, or null when * `spillToolResultsOver` is unset or not above 0. The tool list is read * only to refuse a call whose model could not read a saved result back, * and to name the readers; it never changes what happens to a result. * `branchDefaults` is what `setLlmOptions` set, which may not carry the * option: as a default, it would make every later call without the * readers throw. `LlmDefaults` has no such key, but a TypeScript caller * can still put one there. See docs/dev/stdlib/spill.md. */ export declare function spillSettings(run: Run, perCall: number | undefined, branchDefaults: object, tools: ToolIdentity[]): SpillSettings | null; /** Cap a tool result for the LLM. `cap` is `maxToolResultChars`, a ceiling * on what the model receives (`0` or less, or non-finite, means none). * With `spill` set, a result over the spill value or the cap is saved in * the run's output store and replaced by a notice that fits the cap; * without it, a result over the cap is cut. A reader's own result is * never saved again. A result within the limits is returned as the * ORIGINAL value, so smoltalk serializes it exactly as before. */ export declare function capToolResultForLlm(result: any, cap: number, spill: SpillSettings | null, tool: ToolIdentity, args: Record): any; /** What a saved tool result is labelled for a human: the tool's name and * its first string argument, which for the tools that spill is the URL or * the path. A fixed rule, not a lookup in the trace renderer's table, so * the runtime does not depend on the renderer. */ export declare function spillSource(tool: ToolIdentity, args: Record): string; /** `capToolResultForLlm` with one `llm()` call's cap and spill settings * fixed: what the loop runs every tool result, failure message, and * rejection reason through. */ export declare function toolResultCapper(cap: number, spill: SpillSettings | null): (result: any, tool: ToolIdentity, args: Record) => any; /** LLMs routinely emit an explicit `null` for an optional tool argument * they chose not to set (e.g. `bash(command, cwd: null, timeout: null)`). * Agency default-parameter values only fill `undefined`, so a `null` would * sail past the default into the function body — `bash(cwd: null)` reaches * `applyAgentCwd` → `path.resolve(base, null)` and throws. Drop any argument * whose value is `null` when its parameter declares a default, so the call * behaves exactly as if the LLM had omitted the key (the default applies). * Params without a default keep their value untouched: a required arg passed * `null` still surfaces its normal type error for the model to correct, and * an intentionally-nullable param is left alone. */ export declare function dropNullDefaultedArgs(args: Record | null | undefined, params: readonly FuncParam[]): Record; /** * The model's arguments as the function will see them: alias keys renamed * to the parameter (`url` to `baseUrl`), then null-valued defaulted keys * dropped. Never throws. On a conflict between two keys for one parameter * the arguments stay as written, and the invoke reports the conflict as a * failure that never ran. `argCorrections` lists the renames for the log. */ export declare function toolArgumentsFor(handler: { params: readonly FuncParam[]; canonicalizeNamedArgs(args: Record): CanonicalKeysResult; } | null, toolCall: { arguments?: Record | null; }): { namedArgs: Record; argCorrections: AliasCorrection[]; }; /** A named call whose arguments came from outside the program (a model, an * HTTP body, the command line), so an alias key is renamed to the * parameter. A call compiled from Agency source never gets this. */ export declare function callFromOutside(namedArgs: Record): CallType; /** Provider APIs (Anthropic, OpenAI) reject an LLM request whose tool list * contains duplicate names, and tool-call dispatch here matches handlers by * name — so duplicate names are always a bug. They're easy to introduce * by accident because `.partial()` / `.describe()` preserve the base * function's name (e.g. `skillsDir` returns `read.partial(dir)`, so four * skill tools are all named `read`). Catch it before the request hits the * wire with a message that names the collision and points at `.rename()`, * instead of an opaque transport-layer 400 that never reaches the statelog. */ export declare function assertUniqueToolNames(tools: { name: string; }[]): void;