import OpenAI from 'openai'; import { type Pace } from './pace.js'; import { Trajectory } from './trajectory.js'; import type { TrajectoryEvent } from './trajectory.js'; import type { Plan } from './plan.js'; import { type ProjectInfo } from './project.js'; import { type PluginEntry } from './plugins.js'; import { type MCPClientEntry } from './mcp.js'; import { type Assembled } from './prompt-budget.js'; import { CheckpointRecorder } from './checkpoint.js'; export { resolveMaxTurns } from './run-limits.js'; import type { Message, AgentConfig, ExecutionContext, TokenStats } from './types.js'; /** * Turns a provider failure into something a user can act on. * * "429 status code (no body)" was the actual message for a Gemini quota error. The body was * there and said exactly what was wrong, but Google's OpenAI-compatible layer wraps it in an * array — `[{"error":{...}}]` — which the OpenAI SDK does not recognise, so it reports no body * and throws the explanation away. The array shape is unwrapped here, and the HTTP status is * translated in case no message survives at all. */ export declare function describeProviderError(err: unknown, provider: string, model: string, /** * What the caller knows about the request. Only facts — the re-route advice used to invent an * image and an alias from a single regex match, and told people to remove an attachment they had * never sent. */ about?: { hadImage?: boolean; credential?: string; }): string; /** Session totals as they stood before a request, so its usage can be added rather than assigned. */ export interface UsageBaseline { prompt: number; completion: number; total: number; } /** * Folds one request's reported usage into the session totals. * * Every ReAct turn is a separate request, and the usage on its stream describes that request * alone. Assigning it to the session totals — which is what happened — meant a twenty-turn run * reported the last turn's tokens as the whole session: 665 tokens for eight minutes of work that * wrote six modules, with cost understated by the same factor. * * The sum is written as baseline-plus-reported rather than as a running increment, so a provider * that sends usage more than once on the same stream updates the turn's contribution instead of * counting it twice. */ export declare function accumulateUsage(stats: TokenStats, baseline: UsageBaseline, usage: { prompt_tokens?: number | null; completion_tokens?: number | null; total_tokens?: number | null; }): void; /** * How long one whole turn may take, as a backstop behind the idle watchdog. * * This was a flat five minutes, written to stop a seventeen-minute hang, and for a hosted API it * is a reasonable number: nothing legitimate takes that long when the weights are on somebody * else's accelerator. On a model served from this network it is simply wrong. * * Reported as a run ending mid-work with nothing to say for itself. qwen3-coder:30b through Ollama * across a split that was 81% CPU averaged about thirteen minutes a turn — most of it prompt * processing, all of it healthy. The cap killed the request, the failure went down the * provider-error path, the backoff woke to an already-aborted signal, and the run broke out of the * turn loop and reported "no turn limit was set". Which was true, and had nothing to do with it. * * The idle watchdog below is the instrument that actually detects a hang: two minutes with no * chunk means the connection is dead, whatever the hardware. A turn that is streaming steadily for * a quarter of an hour is slow, not stuck, and telling those apart by total elapsed time cannot be * done — which is why this is a generous backstop rather than a limit anybody should hit. */ export declare function turnTimeoutMs(opts: { local: boolean; configured?: number; }): number; /** * Whether a provider failure is worth retrying. * * Routers make this necessary: a request to an "auto/*" alias is load-balanced, so one attempt * can land on a backend that is out of quota or, as seen in practice, a model the gateway does * not actually support — "[401]: Model hy3-free is not supported". Retrying reaches a different * backend, so a six-turn run is no longer lost to one bad route. A genuine credential problem is * not retried, since every attempt would fail identically. */ export declare function isRetryableProviderError(err: unknown): boolean; /** * How long to wait before retrying, in milliseconds. * * Providers usually say when the window reopens — "Too many requests, please wait before trying * again. (reset after 5s)" is verbatim from one. The backoff used to be a flat 1s, 2s, 3s, which * spends every retry inside a five-second window and reports a hard failure the moment the third * one lands. Eight parallel agents all failed that way, having burned 949k tokens between them. * * Jitter matters as much as the delay. Parallel agents fail together, so without it they retry in * lockstep and collide again on exactly the request that rate-limited them. */ /** * Whether a refusal is about what the account may use rather than about the key. * * A 401 and a 403 look the same from outside and mean quite different things underneath: a key that * is wrong, and a key that is right for an account which has not paid for this model. Only the * first is fixed by looking at the key, and telling somebody to check a working key while their * balance is the problem costs them the afternoon. * * Read from the provider's own words, in the languages providers answer in. 充值 is top up, 余额 is * balance, 权限 is permission. */ export declare function looksLikeEntitlement(err: unknown): boolean; /** The provider's own sentence about it, for saying what it said rather than paraphrasing. */ export declare function entitlementReason(err: unknown): string; /** * Whether nothing could be reached at all, as opposed to something answering unhappily. * * The distinction decides how long to keep trying. A 429 or a 500 is a provider that is up and * telling you something; a connection that cannot be made is usually the link, and the link usually * comes back. Carries no status by definition — an HTTP status means an HTTP response happened. */ /** * Waits, unless the turn is stopped first. * * A plain setTimeout is fine for a second and wrong for thirty: while waiting out an outage the * stop button and ctrl-c have to work, and a timer that cannot be interrupted turns "stop" into * "stop in half a minute". The listener is removed either way, so a long run does not accumulate * one per attempt. */ export declare function sleepUnlessAborted(ms: number, signal?: AbortSignal): Promise; export declare function isConnectivityFailure(err: unknown): boolean; export declare function retryDelayMs(err: unknown, attempt: number, random?: () => number): number; /** * Whether a provider is saying the model cannot do tool calling. * * Ollama says it plainly — "gemma3:1b does not support tools" with a 400 — but a local model that * nominally accepts the tools array and then streams nothing says the same thing by behaving that * way. Both are recoverable: the request works without the tools, so it is worth one retry before * declaring the model dead. */ /** * Said once when a model writes its tool calls as text. * * Worth saying at all: the calls are being honoured, but through a fallback, and a model using * the structured field is the more reliable arrangement when one is available. */ export declare function textToolCallsNotice(model: string): string; export declare function looksLikeToolsUnsupported(err: unknown): boolean; /** * Detects a provider or gateway error that arrived as assistant *content* with HTTP 200, rather * than as an error status. Routers do this: the text * * "You have run out of credits for . Please visit ... to upgrade. Request ID: ..." * * came back as a normal completion, so the loop saw a reply with no tool calls and reported the * task complete. Five turns of that produced a successful run with an empty directory, which is * the worst way for an agent to fail. Treated as a failure, the run says what actually happened. */ export declare function looksLikeProviderNotice(text: string): boolean; /** * True when a run is about to finish having changed nothing, although it was asked to build * something. This is structural rather than linguistic: the wording an agent uses to report that * it did not do the work is unbounded ("no files were created or changed yet" defeated a list of * phrases twice), but "asked to implement, never called a writing tool" is unambiguous. */ export declare function finishedWithoutBuilding(task: string, usedMutatingTool: boolean): boolean; /** * True when assistant text announces work it has not done yet, rather than reporting a * conclusion. Some models emit "Now let me read X" as a standalone turn with no tool call, and * others write a long status report admitting they stopped early; treating either as a final * answer ends the run mid-investigation. */ /** * Whether a plan step is a checkpoint with the person rather than work to do. * * These are steps the model writes when it means to stop and let the user decide: present a * proposal, get approval, wait for confirmation, ask before proceeding. KONECK's "do not stop while * steps remain" nudge would drive straight through them, which is how a proposal got "approved" by * nobody. So they are recognised and the run stops at them. * * Deliberately about waiting on a person, not about caution in general. "Review the code" is work * the model does; "wait for the user to review" is a stop. The tell is a second person or an act of * permission — present/show/ask/confirm/approve/await, aimed at the user. */ export declare function stepAwaitsUser(text: string): boolean; /** * Does the reply itself offer to act and wait for the person's word? * * The plan-step gate above catches "present the proposal to the user". It does not catch the case * reported here: asked to check and report, the model surveyed, wrote "No files were changed — this * was read-only. Say the word and I'll close the gaps", and then — because its own plan still had * five coding steps on it — KONECK's "do not stop while steps remain" nudge pushed it straight into * writing a service layer nobody had asked for. The model asked permission and KONECK answered for * the user. * * So an offer held open for the person ends the turn. Only phrasings that mean the work has NOT been * done pending an answer — say the word, shall I, want me to, your call — and only in the closing, * since a long report may discuss asking somebody in the middle of it. A bare courtesy ("let me know * if anything looks wrong") is not enough on its own; it has to be a conditional offer. * * Erring towards stopping is deliberate. Stopping when the person meant carry on costs one word to * fix; carrying on when they meant stop writes files they never asked for. */ export declare function replyAwaitsUser(text: string): boolean; export declare function announcesMoreWork(text: string): boolean; export { buildClient } from './providers.js'; /** * `git status --short`, summarised rather than pasted. * * Measured in a real workspace: 1,334 tokens of it, a third of the whole window, listing files in * other projects. What the model needs is whether the tree is dirty and roughly where; a complete * inventory is something it can ask for with a tool when it matters. */ export declare function summariseGitStatus(raw: string, maxLines?: number): string; /** The first line of a tool failure, short enough for one row. */ export declare function failureReason(result: string, max?: number): string; /** Beyond this, notes crowd out the actual task. Roughly 500 words. */ export declare const MAX_MEMORY_CHARS = 4000; /** A small, explicit presentation preference—not a hidden change to capability or safety. */ export declare function personalityInstruction(personality: AgentConfig['personality']): string; /** * Caps the notes fed back into the prompt, keeping the most recent. * * The file is appended to and never pruned, so an agent left running in one folder can grow it * without limit — and the older an entry is, the likelier it is to be about something abandoned. */ export declare function truncateMemory(memory: string, limit?: number): string; export declare function buildSystemPrompt(ctx: ExecutionContext, memory?: string | null, project?: ProjectInfo | null, agentInstructions?: string | null, /** * Whether the window is too small for guidance. * * The markdown-shaping rules are worth about 300 tokens and they improve a large model's output. * On a 4,096-token window they are 7% of everything the model will ever see, spent on how to * format a table it has no room to write. Identity and the rules that keep it safe stay. */ lean?: boolean, /** * A ceiling on the whole prompt, in tokens, or undefined for no ceiling. * * Set only when the window cannot hold everything. Above it, nothing is dropped and this is the * same prompt it always was. */ budgetTokens?: number, /** Framework guidance detected for the project — first to go when there is no room for it. */ extraPrompt?: string, /** User-selected presentation style; it never changes tools, approvals, or safety rules. */ personality?: AgentConfig['personality']): Assembled; /** * Which optional tools can actually do anything here. * * A tool that is offered but cannot work is worse than one that is absent: the model calls it, * gets an error explaining what to configure, and then tries to configure it — searching for how * to obtain the very API key whose absence made the search fail. One real run spent two minutes * and eight turns on that circle and wrote nothing anyone asked for. */ export interface ToolAvailability { /** Whether the working directory is a git repository. */ git: boolean; /** Whether a search provider key is present. */ webSearch: boolean; } export declare function availableToolNames(names: readonly string[], available: ToolAvailability): string[]; export declare function detectSearchKey(env?: NodeJS.ProcessEnv): boolean; export { AGENT_CONCURRENCY, MAX_AGENT_CONCURRENCY, resolveAgentConcurrency, toolsUnsupportedNotice } from './engine-facts.js'; /** * Maps over items with at most `limit` running at once, preserving input order in the results. * * Promise.all starts everything immediately, which is the whole problem here. */ export declare function mapWithLimit(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise): Promise; /** * Removes image parts from the history, returning how many went. * * Used when a provider rejects them: the words are still worth sending, and leaving the pictures * in place would make every subsequent turn fail the same way. */ /** * Whether this conversation is actually sending a picture. * * Asked because the re-route advice used to assume one. It read a provider's "no active * credentials" complaint and explained it as an alias resolving to a vision-capable upstream, * then advised removing the attachment — to people whose request contained no image at all. The * cheapest way to stop asserting it was to be able to check it. */ /** The key a run will use, as a fingerprint and a source. Never the key. Never throws. */ export declare function describeCredential(config: AgentConfig): string | undefined; export declare function carriesImage(messages: readonly Message[]): boolean; export declare function stripImages(messages: Message[]): number; /** * The one detail worth showing beside a tool in a trajectory. * * A path or a command says which call this was; the whole argument object does not fit and, on a * write, is the entire file. */ export declare function toolDetailFor(name: string, argsJson: string): string; /** * What to say when a provider returns nothing several times over. * * The old wording asserted that tool calling was off and the model unusable. Both were wrong for * the case that actually happens: a model which had called a tool successfully a turn earlier, * with its tools still enabled. Saying so sent people looking for a different model when the * problem was almost certainly the window. * * Ollama is called out specifically because its default serving context is far smaller than the * length its models are trained for — 4,096 against qwen3-coder's 32,768 — and it truncates an * over-long prompt silently rather than refusing it. KONECK's own prompt is around 3,600 tokens * before a conversation starts, so on the default there is almost no room to work in, and the * symptom is exactly this: nothing comes back. */ export declare function emptyResponseAdvice(provider: string, model: string, count: number, toolsWorked: boolean, endpoint?: string, fixedTokens?: number, platform?: NodeJS.Platform, /** * Which runtime was measured to be serving. Decides which fix is named. * * `served`, not `runtime`: there is already a boolean called that a few lines down, meaning * "is this a thing that serves its own weights at all" — a different question with a confusingly * similar name. */ served?: AgentConfig['servedRuntime']): string; export declare function runReActLoop(messages: Message[], client: OpenAI, config: AgentConfig, effectiveCwd: string, stats: TokenStats, mcpClients: MCPClientEntry[], plugins: PluginEntry[], openaiTools: OpenAI.Chat.ChatCompletionTool[], turnOffset?: number, abortSignal?: AbortSignal, checkpoint?: CheckpointRecorder, /** * The plan, held by the session rather than by this loop. * * It used to be declared here, and this runs once per user message — so saying "continue" wiped * the plan and the model wrote a new one from nothing. A run that had done five of eleven steps * started again at one of five, having forgotten both what it had done and what was left. */ planRef?: { current: Plan; }, /** Where timings are recorded. Absent in a sub-agent, whose detail is its own business. */ trajectory?: Trajectory, /** The window the provider reported, filled in asynchronously after the session started. */ detectedContext?: { current: number; }, /** * Set by `compactNow()` to compact before the next request, whatever the window says. * * Owned by the session rather than declared here, because this runs once per user message and * the request is made between two of them — declared here it would be reset before it was read. */ compactRequest?: { pending: boolean; }): Promise; export interface AgentSession { readonly id: string; readonly messages: Message[]; readonly stats: TokenStats; readonly completed: boolean; /** The plan as it stands, for saving with the session. */ readonly plan: Plan; /** What happened, with timings, for inspection after the fact. */ readonly trajectory: readonly TrajectoryEvent[]; /** MCP capabilities that connected for this session. Environment values are never exposed. */ readonly mcpServers: readonly { name: string; tools: readonly { name: string; description?: string; }[]; }[]; /** `imagePaths` sends pictures explicitly; paths written in the text are attached anyway. */ /** * A diagnostic report for this session, carrying nothing of the work itself. * * KONECK already recorded where every second went — a timing for each request and each tool call * — and nobody could read it. Asked why a small task took 31 minutes and 1.2 million tokens, the * answer was in the trajectory and had no way out of the machine. */ report(): import('./report.js').Report; send(userText: string, signal?: AbortSignal, imagePaths?: readonly string[]): Promise; /** * Changes how hard the model thinks, from the next request on. * * Live rather than at construction, because the alternative is rebuilding the session — which * throws away the conversation — or waiting until the next one, which is not what somebody who * just chose "low" because they are waiting means by it. */ setPace(pace: Pace): void; /** * Compact the conversation before the next request, whether or not it is near the limit. * * Compaction already happens on its own when the window fills. This is for the other case: a * session that is about to change subject, where carrying the last three hours of a different * problem costs money and attention on every turn to come. Deferred to the next request rather * than done here, because summarising is itself a model call and the caller is usually mid- * sentence when they ask. */ compactNow(): void; /** * The provider and model actually in use, now. * * Not the ones the session was created with. Failover replaces them mid-run, and everything that * reported them read the config the caller still held — so a run that moved from glm to gemini * and finished there was written down as a glm session. The record named a model that had not * done most of the work. */ readonly running: { provider: string; model: string; }; } /** * Image paths a prompt refers to that actually exist on disk. * * Existence is the test, so an invented filename in prose is left alone rather than reported as a * missing attachment. Checked synchronously because it happens once per prompt, on a handful of * words, before any request goes out. */ export declare function imagesMentionedIn(text: string, cwd: string): string[]; export declare function createAgentSession(config: AgentConfig, initialMessages?: Message[], /** * Totals the restored history already accounts for. * * Without this a resumed session counted from zero, so the first save after resuming wrote back * fewer turns and tokens than the session had already used — the history was intact but its own * record of itself shrank each time it was resumed. `turnOffset` already reads `stats.turns`, * so seeding it also keeps turn numbering continuous instead of restarting at 1. */ initialStats?: Partial, /** A plan restored with a session, so resuming continues the work rather than re-planning it. */ initialPlan?: Plan): Promise; /** * The session being continued, when `runAgent` is resuming one. * * Without this the CLI's `--resume` had the same fault the chat UI did: it minted a fresh id, so * the follow-up was written to a new file and the session you resumed stayed frozen at the state * you found it in. Every resume forked another copy, none of which knew about the others. */ export interface ResumeTarget { id: string; turns: number; totalTokens: number; /** When the session first began — kept, so it does not creep forward on every save. */ startedAt?: string; /** The original task, so a session is still identifiable by what it set out to do. */ task?: string; } /** * Whether the model is being served by a local runtime on this machine. * * Matters because the extra passes after a run — a self-critique round trip and a quality gate — * are cheap against a hosted model and expensive against a local one, where a single turn can * take minutes of CPU. Judged by the runtime rather than by the address: OmniRoute listens on * localhost too, but the models behind it are hosted, so a loopback URL proves nothing. */ export declare function isLocalRuntime(provider: string): boolean; export declare function runAgent(task: string, config: AgentConfig, initialMessages?: Message[], resume?: ResumeTarget): Promise; //# sourceMappingURL=engine.d.ts.map