@cubicecho/agent-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/retry.js ADDED
@@ -0,0 +1,91 @@
1
+ import OpenAI from "openai";
2
+ import { estimateTokens } from "./side-task.js";
3
+ /**
4
+ * Everything about a request failing that is not about what the request said.
5
+ *
6
+ * A run makes a request per tool iteration against an endpoint that may be a laptop's llama.cpp
7
+ * or a hosted API, and the two fail in different ways for different reasons. This is the part
8
+ * of the loop with nothing to do with the model's answer: whether the request was lost, whether
9
+ * it was too big to have been sent at all, and how long to wait before sending it again.
10
+ */
11
+ /** The endpoint stopped answering mid-request. Its own class so the retry can recognise it. */
12
+ export class EndpointSilent extends Error {
13
+ name = "EndpointSilent";
14
+ }
15
+ /**
16
+ * The request was bigger than the model will read. Its own class so nothing retries it: sending
17
+ * the same too-large request again is the same refusal, one round trip later.
18
+ */
19
+ export class ContextOverflow extends Error {
20
+ name = "ContextOverflow";
21
+ }
22
+ /** 1234 → "1.2k". The numbers in an overflow message are large and nobody reads the units digit. */
23
+ export const compact = (tokens) => tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
24
+ /**
25
+ * What this request will cost the window, in tokens, near enough.
26
+ *
27
+ * Characters over four, because there is no tokenizer here and there is not going to be one:
28
+ * a server that will not say how big its window is will not lend us its vocabulary either.
29
+ * The estimate runs low on tool schemas — JSON packs more tokens into a character than prose
30
+ * does — and that is the side to be wrong on, since the cost of guessing high is a run refused
31
+ * that would have worked, and the cost of guessing low is the endpoint's own refusal, which is
32
+ * where we were before this existed.
33
+ */
34
+ export const requestTokens = (body) => estimateTokens(JSON.stringify(body.messages)) +
35
+ (body.tools?.length ? estimateTokens(JSON.stringify(body.tools)) : 0);
36
+ /**
37
+ * Servers refuse an over-long request in their own words; these are the ones worth reading as
38
+ * that rather than as a broken request. Matched loosely — every one of them is some
39
+ * arrangement of "context" and "too long", and the arrangement is the part that varies.
40
+ */
41
+ const OVERFLOW = [
42
+ /context (size|length|window)/i,
43
+ /exceeds? the (available|maximum)/i,
44
+ /too (long|large) for/i,
45
+ /reduce the length/i,
46
+ ];
47
+ export const isOverflow = (detail) => OVERFLOW.some((pattern) => pattern.test(detail)) && /token|context/i.test(detail);
48
+ /**
49
+ * Below this, the window is nobody's business and is not asked for.
50
+ *
51
+ * Finding out what a model reads costs a listing against its endpoint, and a run whose whole
52
+ * request is a few thousand tokens fits anything anyone serves — spending a round trip to
53
+ * confirm that, on every run of every card, would be the cost of the guard falling on the
54
+ * runs that never needed it. A model in a window smaller than this exists, and a request that
55
+ * overruns one is left to the endpoint's own complaint, which reads properly now either way.
56
+ */
57
+ export const SMALLEST_LIKELY_WINDOW = 8192;
58
+ /**
59
+ * Whether a failed request is worth trying again.
60
+ *
61
+ * The question is whether the request was *refused or lost*, rather than answered with a
62
+ * complaint about its contents: a connection that never landed, a server too busy or too broken
63
+ * to answer, an endpoint that went quiet. A 400 for a malformed tool schema would fail exactly
64
+ * the same way on every attempt, and the two capability cases below are negotiated rather than
65
+ * retried blindly.
66
+ */
67
+ export function isTransient(error) {
68
+ if (error instanceof EndpointSilent)
69
+ return true;
70
+ if (error instanceof OpenAI.APIConnectionError)
71
+ return true;
72
+ if (!(error instanceof OpenAI.APIError))
73
+ return false;
74
+ const { status } = error;
75
+ return status === 408 || status === 409 || status === 429 || (status ?? 0) >= 500;
76
+ }
77
+ /** Exponential, with jitter so several tasks failing at once do not return in lockstep. */
78
+ export const backoffMs = (attempt) => Math.min(8000, 2 ** attempt * 500) * (0.5 + Math.random() / 2);
79
+ export const sleep = (ms, signal) => new Promise((resolve, reject) => {
80
+ const timer = setTimeout(() => {
81
+ signal?.removeEventListener("abort", onAbort);
82
+ resolve();
83
+ }, ms);
84
+ const onAbort = () => {
85
+ clearTimeout(timer);
86
+ reject(signal?.reason ?? new Error("aborted"));
87
+ };
88
+ if (signal?.aborted)
89
+ return onAbort();
90
+ signal?.addEventListener("abort", onAbort, { once: true });
91
+ });
@@ -0,0 +1,17 @@
1
+ import type OpenAI from "openai";
2
+ export declare const sanitizeTools: (tools: OpenAI.ChatCompletionTool[]) => OpenAI.ChatCompletionTool[];
3
+ /**
4
+ * The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
5
+ * `pattern` and most `format` values, both of which only ever narrowed a string the tool
6
+ * re-validates anyway.
7
+ */
8
+ export declare function relaxTools(tools: OpenAI.ChatCompletionTool[]): OpenAI.ChatCompletionTool[];
9
+ /**
10
+ * Does this failure look like the server could not build a grammar from our tool schemas?
11
+ *
12
+ * Every server words this differently — llama-server says "error parsing grammar", Lemonade
13
+ * says "Failed to initialize samplers: failed to parse grammar", others surface the converter
14
+ * by name. Since a grammar is only ever involved in constrained decoding, treat any mention of
15
+ * one as ours; the retry is cheap and latches after a single request.
16
+ */
17
+ export declare function isGrammarError(message: string): boolean;
@@ -0,0 +1,178 @@
1
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2
+ /** Lookahead and lookbehind: `(?=`, `(?!`, `(?<=`, `(?<!`. */
3
+ const LOOKAROUND = /\(\?<?[=!]/;
4
+ const PRIMITIVES = new Set(["object", "string", "number", "integer", "boolean", "array", "null"]);
5
+ const EMPTY_OBJECT = () => ({ type: "object", properties: {} });
6
+ /** Keys whose value is a schema, or a list of them — several are spelled both ways. */
7
+ const SCHEMA_KEYS = new Set([
8
+ "items",
9
+ "additionalProperties",
10
+ "not",
11
+ "if",
12
+ "then",
13
+ "else",
14
+ "contains",
15
+ "propertyNames",
16
+ "anyOf",
17
+ "oneOf",
18
+ "allOf",
19
+ "prefixItems",
20
+ ]);
21
+ /** Keys whose value is a name -> schema map. */
22
+ const SCHEMA_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions"]);
23
+ /**
24
+ * Coerces one schema position. Malformed MCP output sometimes puts a bare type name where a
25
+ * whole schema belongs, which the grammar converter reports as `Unrecognized schema: "object"`.
26
+ */
27
+ function asSchema(node) {
28
+ if (typeof node === "string")
29
+ return PRIMITIVES.has(node) && node !== "object" ? { type: node } : EMPTY_OBJECT();
30
+ if (typeof node === "boolean")
31
+ return node;
32
+ if (!isObject(node))
33
+ return EMPTY_OBJECT();
34
+ return normalize(node);
35
+ }
36
+ /** Recursively rewrites the shapes llama.cpp's grammar converter cannot represent. */
37
+ function normalize(node) {
38
+ const out = {};
39
+ for (const [key, value] of Object.entries(node)) {
40
+ // `type: ["string", "null"]` — the converter only accepts a single string type.
41
+ if (key === "type" && Array.isArray(value)) {
42
+ const names = value.filter((item) => typeof item === "string");
43
+ const concrete = names.filter((name) => name !== "null");
44
+ if (names.includes("null"))
45
+ out.nullable = true;
46
+ if (concrete.length === 1)
47
+ out.type = concrete[0];
48
+ else if (concrete.length > 1)
49
+ out.anyOf = concrete.map((name) => ({ type: name }));
50
+ else
51
+ out.type = "null";
52
+ }
53
+ else if (SCHEMA_KEYS.has(key)) {
54
+ out[key] = Array.isArray(value) ? value.map(asSchema) : asSchema(value);
55
+ }
56
+ else if (SCHEMA_MAPS.has(key) && isObject(value)) {
57
+ out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, asSchema(sub)]));
58
+ }
59
+ else {
60
+ out[key] = value;
61
+ }
62
+ }
63
+ collapseNullableUnion(out);
64
+ // A grammar is context-free; lookaround is not expressible in one at all, so no converter
65
+ // can accept it. Dropping it costs one advisory constraint on one string field.
66
+ if (typeof out.pattern === "string" && LOOKAROUND.test(out.pattern))
67
+ delete out.pattern;
68
+ // `{"type": "object"}` with no properties produces invalid GBNF.
69
+ if (out.type === "object" && !isObject(out.properties))
70
+ out.properties = {};
71
+ // Strict validators reject any sibling of `$ref`, and draft-07 ignores them, so a reference
72
+ // stands alone or not at all. This is not hypothetical tidying: collapsing `anyOf: [{$ref},
73
+ // {type: "null"}]` — the shape a schema-generated server emits at every optional argument —
74
+ // lands `nullable` right next to the `$ref` that survived. Whatever is dropped here was
75
+ // already unreadable to a conforming consumer; optionality still lives in the parent's
76
+ // `required`.
77
+ if ("$ref" in out)
78
+ return { $ref: out.$ref };
79
+ return out;
80
+ }
81
+ /**
82
+ * `{anyOf: [{type: "string"}, {type: "null"}]}` is how Pydantic-backed MCP servers spell an
83
+ * optional field. Optionality already lives in the parent's `required`, so keep the one real
84
+ * branch. A union with two real branches is meaningful and is left alone.
85
+ */
86
+ function collapseNullableUnion(node) {
87
+ for (const key of ["anyOf", "oneOf"]) {
88
+ const variants = node[key];
89
+ if (!Array.isArray(variants))
90
+ continue;
91
+ const concrete = variants.filter((item) => !(isObject(item) && item.type === "null"));
92
+ if (concrete.length !== 1 || concrete.length === variants.length)
93
+ continue;
94
+ delete node[key];
95
+ Object.assign(node, { nullable: true, ...(isObject(concrete[0]) ? concrete[0] : {}) });
96
+ }
97
+ }
98
+ /** Combinators at the top level of a parameters schema; strict backends reject them outright. */
99
+ const TOP_LEVEL_COMBINATORS = ["allOf", "anyOf", "oneOf", "enum", "not"];
100
+ function sanitizeParameters(parameters) {
101
+ if (!isObject(parameters))
102
+ return EMPTY_OBJECT();
103
+ const out = normalize(parameters);
104
+ for (const key of TOP_LEVEL_COMBINATORS)
105
+ delete out[key];
106
+ if (out.type !== "object")
107
+ out.type = "object";
108
+ if (!isObject(out.properties))
109
+ out.properties = {};
110
+ return out;
111
+ }
112
+ const mapTools = (tools, fn) => tools.map((tool) => tool.type === "function"
113
+ ? { ...tool, function: { ...tool.function, parameters: fn(tool.function.parameters) } }
114
+ : tool);
115
+ /**
116
+ * Cached against the tool object rather than recomputed.
117
+ *
118
+ * The agent loop rebuilds its tool array on every iteration of every step, and normalising a
119
+ * couple of dozen MCP schemas is the only walk in a run that is neither a request nor a query.
120
+ * The pool hands out the same definition objects for the life of a connection, so identity is
121
+ * exactly the right key: a reconnect makes new ones and they are normalised again.
122
+ */
123
+ const sanitized = new WeakMap();
124
+ export const sanitizeTools = (tools) => tools.map((tool) => {
125
+ const hit = sanitized.get(tool);
126
+ if (hit)
127
+ return hit;
128
+ const [clean] = mapTools([tool], sanitizeParameters);
129
+ sanitized.set(tool, clean);
130
+ return clean;
131
+ });
132
+ /**
133
+ * The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
134
+ * `pattern` and most `format` values, both of which only ever narrowed a string the tool
135
+ * re-validates anyway.
136
+ */
137
+ export function relaxTools(tools) {
138
+ const strip = (node) => {
139
+ if (Array.isArray(node))
140
+ return node.map(strip);
141
+ if (!isObject(node))
142
+ return node;
143
+ const out = {};
144
+ for (const [key, value] of Object.entries(node)) {
145
+ if (key === "pattern" || key === "format")
146
+ continue;
147
+ out[key] = strip(value);
148
+ }
149
+ return out;
150
+ };
151
+ return mapTools(tools, (parameters) => {
152
+ const stripped = strip(parameters);
153
+ return isObject(stripped) ? stripped : EMPTY_OBJECT();
154
+ });
155
+ }
156
+ /**
157
+ * Qwen chat templates raise this when the transcript has no user turn. Some servers wrap it
158
+ * in the same "unable to generate parser" wording as a real schema failure, and stripping
159
+ * keywords would not fix it.
160
+ */
161
+ const NO_USER_QUERY = "no user query found";
162
+ /**
163
+ * Does this failure look like the server could not build a grammar from our tool schemas?
164
+ *
165
+ * Every server words this differently — llama-server says "error parsing grammar", Lemonade
166
+ * says "Failed to initialize samplers: failed to parse grammar", others surface the converter
167
+ * by name. Since a grammar is only ever involved in constrained decoding, treat any mention of
168
+ * one as ours; the retry is cheap and latches after a single request.
169
+ */
170
+ export function isGrammarError(message) {
171
+ const text = message.toLowerCase();
172
+ if (text.includes(NO_USER_QUERY))
173
+ return false;
174
+ return (text.includes("grammar") ||
175
+ text.includes("unrecognized schema") ||
176
+ text.includes("json schema conversion failed") ||
177
+ (text.includes("unable to generate parser") && text.includes("template")));
178
+ }
@@ -0,0 +1,37 @@
1
+ import type { Endpoint } from "./config.ts";
2
+ export interface SideTaskOptions {
3
+ maxTokens?: number;
4
+ temperature?: number;
5
+ signal?: AbortSignal;
6
+ }
7
+ /** Runs a side task and returns the reply text, thinking stripped. Throws like any request. */
8
+ export declare function ask(config: Endpoint, model: string, system: string, user: string, { maxTokens, temperature, signal }?: SideTaskOptions): Promise<string>;
9
+ /**
10
+ * A side task is never worth failing the work it supports. Callers that can carry on without
11
+ * an answer use this and get `undefined` instead of an exception.
12
+ */
13
+ export declare function tryAsk<T>(label: string, run: () => Promise<T>): Promise<T | undefined>;
14
+ /**
15
+ * Models are asked for JSON and often answer with prose around it, or a fenced block. Pull out
16
+ * the first array or object rather than failing the task over a wrapper.
17
+ */
18
+ export declare function parseJson<T>(text: string): T | undefined;
19
+ /** Strips the quoting and list punctuation models decorate short answers with. */
20
+ export declare const clean: (line: string) => string;
21
+ /**
22
+ * A list-shaped reply, one item per line, cleaned of the bullets and quotes models decorate
23
+ * them with. Overlong items are dropped rather than truncated — a suggestion that has to be
24
+ * squinted at is worse than one fewer suggestion.
25
+ */
26
+ export declare const listLines: (text: string, max: number, maxChars: number) => string[];
27
+ /**
28
+ * Rough token count. Characters over four, because there is no tokenizer here and there is not
29
+ * going to be one: a server that will not say how big its window is will not lend us its
30
+ * vocabulary either.
31
+ *
32
+ * The estimate runs low on tool schemas — JSON packs more tokens into a character than prose
33
+ * does — and that is the side to be wrong on wherever it guards a window, since the cost of
34
+ * guessing high is a run refused that would have worked, and the cost of guessing low is the
35
+ * endpoint's own refusal, which is where we were before the guard existed.
36
+ */
37
+ export declare const estimateTokens: (text: string) => number;
@@ -0,0 +1,129 @@
1
+ import OpenAI from "openai";
2
+ import { getClient } from "./client.js";
3
+ /**
4
+ * One-shot calls that support a run without being one: picking tools, naming a session,
5
+ * summarising a transcript, proposing follow-ups. They share a shape — small prompt, short
6
+ * answer, no tools, no streaming — and none is ever worth failing the run it supports.
7
+ */
8
+ /**
9
+ * Reasoning models will happily spend a whole budget deliberating over a six-word answer and
10
+ * return empty content, so side tasks ask for thinking to be turned off. `reasoning_effort` is
11
+ * the OpenAI-compatible spelling and `chat_template_kwargs` the llama.cpp/vLLM one; servers
12
+ * disagree about which they take, so send both. One that rejects the unknown fields gets a
13
+ * single retry without them, and is not offered them again.
14
+ */
15
+ const NO_THINKING = {
16
+ reasoning_effort: "none",
17
+ chat_template_kwargs: { enable_thinking: false },
18
+ };
19
+ /**
20
+ * The endpoints that turned out not to take the hints, by base URL.
21
+ *
22
+ * Keyed rather than global for the reason the client cache is keyed: a refusal is a fact about
23
+ * the server on the other end, not about this process. A llama.cpp box and a cloud API are both
24
+ * reachable from one consumer over its lifetime, and the first one's refusal must not stop the
25
+ * second from ever being asked.
26
+ */
27
+ const noHints = new Set();
28
+ /**
29
+ * Whether a failure is the server complaining about the request, rather than failing to answer.
30
+ *
31
+ * The retry below used to catch everything, so an aborted first call — or a connection that
32
+ * never landed — latched the hints off for the life of the process and every later side task
33
+ * paid for it by burning a whole budget on deliberation. Only a 4xx says the fields were the
34
+ * problem; a timeout, a refused connection or a 500 say nothing about them at all.
35
+ */
36
+ function rejectedTheRequest(error) {
37
+ if (!(error instanceof OpenAI.APIError))
38
+ return false;
39
+ const status = error.status ?? 0;
40
+ return status >= 400 && status < 500;
41
+ }
42
+ /** Reasoning models that ignore the hints still fence their scratchpad; drop it. */
43
+ const stripThinking = (text) => text.replace(/<think>[\s\S]*?<\/think>/gi, "");
44
+ /** Runs a side task and returns the reply text, thinking stripped. Throws like any request. */
45
+ export async function ask(config, model, system, user, { maxTokens = 512, temperature = 0.3, signal } = {}) {
46
+ const send = (hints) => getClient(config).chat.completions.create({
47
+ model,
48
+ max_tokens: maxTokens,
49
+ temperature,
50
+ messages: [
51
+ { role: "system", content: system },
52
+ { role: "user", content: user },
53
+ ],
54
+ ...(hints ? NO_THINKING : {}),
55
+ }, { signal });
56
+ const hints = !noHints.has(config.baseUrl);
57
+ let response;
58
+ try {
59
+ response = await send(hints);
60
+ }
61
+ catch (error) {
62
+ if (!hints || !rejectedTheRequest(error))
63
+ throw error;
64
+ console.warn("[side-task] server rejected the no-thinking hints; retrying without them");
65
+ noHints.add(config.baseUrl);
66
+ response = await send(false);
67
+ }
68
+ return stripThinking(response.choices[0]?.message?.content ?? "").trim();
69
+ }
70
+ /**
71
+ * A side task is never worth failing the work it supports. Callers that can carry on without
72
+ * an answer use this and get `undefined` instead of an exception.
73
+ */
74
+ export async function tryAsk(label, run) {
75
+ try {
76
+ return await run();
77
+ }
78
+ catch (error) {
79
+ console.warn(`[side-task] ${label}:`, error.message);
80
+ return undefined;
81
+ }
82
+ }
83
+ /**
84
+ * Models are asked for JSON and often answer with prose around it, or a fenced block. Pull out
85
+ * the first array or object rather than failing the task over a wrapper.
86
+ */
87
+ export function parseJson(text) {
88
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
89
+ const body = (fenced?.[1] ?? text).trim();
90
+ const start = body.search(/[[{]/);
91
+ if (start < 0)
92
+ return undefined;
93
+ const end = Math.max(body.lastIndexOf("]"), body.lastIndexOf("}"));
94
+ if (end <= start)
95
+ return undefined;
96
+ try {
97
+ return JSON.parse(body.slice(start, end + 1));
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ }
103
+ /** Strips the quoting and list punctuation models decorate short answers with. */
104
+ export const clean = (line) => line
105
+ .trim()
106
+ .replace(/^(?:[-*•]|\d+[.)])\s*/, "")
107
+ .replace(/^["'`]+|["'`.]+$/g, "")
108
+ .trim();
109
+ /**
110
+ * A list-shaped reply, one item per line, cleaned of the bullets and quotes models decorate
111
+ * them with. Overlong items are dropped rather than truncated — a suggestion that has to be
112
+ * squinted at is worse than one fewer suggestion.
113
+ */
114
+ export const listLines = (text, max, maxChars) => text
115
+ .split("\n")
116
+ .map(clean)
117
+ .filter((line) => line.length > 0 && line.length <= maxChars)
118
+ .slice(0, max);
119
+ /**
120
+ * Rough token count. Characters over four, because there is no tokenizer here and there is not
121
+ * going to be one: a server that will not say how big its window is will not lend us its
122
+ * vocabulary either.
123
+ *
124
+ * The estimate runs low on tool schemas — JSON packs more tokens into a character than prose
125
+ * does — and that is the side to be wrong on wherever it guards a window, since the cost of
126
+ * guessing high is a run refused that would have worked, and the cost of guessing low is the
127
+ * endpoint's own refusal, which is where we were before the guard existed.
128
+ */
129
+ export const estimateTokens = (text) => Math.ceil(text.length / 4);
@@ -0,0 +1,85 @@
1
+ import type OpenAI from "openai";
2
+ import type { CatalogServer } from "./catalog.ts";
3
+ /**
4
+ * On-demand tool loading.
5
+ *
6
+ * A full tool definition is mostly JSON Schema, and it is sent on every request of every
7
+ * iteration whether or not the model wants it — a couple of connected servers can cost more
8
+ * tokens per request than the task's own prompt. So in on-demand mode the run starts with a
9
+ * bare *catalogue*: tool names only, appended to the system prompt, plus this one meta-tool.
10
+ * The model calls `load_tools` with what it needs, and the next round trip carries those real
11
+ * definitions.
12
+ *
13
+ * Names alone cost roughly a fortieth of what the schemas cost, so a run that needs no tools
14
+ * pays almost nothing, and a run that needs three pays for three.
15
+ */
16
+ export declare const LOAD_TOOLS = "load_tools";
17
+ /** One object for the life of the process — the agent loop asks for it on every iteration. */
18
+ export declare const LOAD_TOOLS_DEFINITION: OpenAI.ChatCompletionTool;
19
+ /** The catalogue as a plain grouped listing of names, loaded ones marked. */
20
+ export declare function catalogList(catalog: CatalogServer[], loaded?: ReadonlySet<string>): string;
21
+ /**
22
+ * The catalogue block appended to the system prompt. Names only — descriptions arrive on load.
23
+ *
24
+ * Loaded tools stay in the list, marked. Removing them reads as the tool having vanished the
25
+ * moment it was loaded, and the model loads again to get it back; hoisting them into a separate
26
+ * "already loaded" section splits a server's tools apart, and the model picks a sibling from
27
+ * the longer list instead.
28
+ */
29
+ export declare function catalogPrompt(catalog: CatalogServer[], loaded?: ReadonlySet<string>): string;
30
+ /**
31
+ * The most a single `load_tools` call may pull in.
32
+ *
33
+ * A wildcard like `gmail__*` matches 33 tools, and loading them all puts the model right back
34
+ * in the position on-demand loading exists to avoid — a tool array too large to choose from.
35
+ * Over-broad requests are refused with the matching names listed, so the next call can be
36
+ * precise.
37
+ */
38
+ export declare const MAX_PER_LOAD = 12;
39
+ /**
40
+ * The most a conversation carries between turns. Bounds the tool array no matter how long the
41
+ * conversation runs; least-recently-used names fall off the front.
42
+ *
43
+ * Only a multi-turn caller needs this — a run that starts from nothing each time has nothing to
44
+ * carry. See `carryOver`.
45
+ */
46
+ export declare const MAX_CARRIED = 16;
47
+ /** The tools to start the next turn with: recently used, newest last, capped. */
48
+ export declare const carryOver: (previous: string[], used: Set<string>) => string[];
49
+ /**
50
+ * Resolves requested names against the catalogue, expanding trailing `*` wildcards.
51
+ *
52
+ * Names are matched leniently. Catalogue entries are slug-qualified (`nas_fs__read_file`) and
53
+ * models routinely ask for the bare tool name, so an exact miss falls back to a suffix match
54
+ * on the `__` boundary — accepted only when it is unambiguous. Rejecting those outright just
55
+ * buys a wasted round trip while the model guesses the prefix, and pushes it toward
56
+ * shotgunning wildcards.
57
+ */
58
+ export declare function expandNames(requested: string[], catalog: CatalogServer[]): {
59
+ matched: string[];
60
+ unknown: string[];
61
+ overBroad: {
62
+ name: string;
63
+ hits: string[];
64
+ }[];
65
+ };
66
+ /** What `load_tools` reports back: the descriptions, now that they are worth their tokens. */
67
+ export declare function loadResult({ matched, unknown, overBroad }: ReturnType<typeof expandNames>, catalog: CatalogServer[]): string;
68
+ export declare const inCatalog: (catalog: CatalogServer[], name: string) => boolean;
69
+ /** `load_tools` arguments, defensively — a model may send a bare string or a nested object. */
70
+ export declare function requestedNames(args: Record<string, unknown>): string[];
71
+ /**
72
+ * Tool preselection.
73
+ *
74
+ * On-demand loading otherwise costs a round trip every run: the model reads the catalogue,
75
+ * calls `load_tools`, and only then can do the work. A small model reading the same catalogue
76
+ * usually names the right tools outright, so the task model finds them already loaded and
77
+ * starts working on its first step.
78
+ *
79
+ * A wrong guess is cheap — an unused definition is a few hundred tokens for one run — but a
80
+ * broad guess is not, so the same `MAX_PER_LOAD` cap applies here as to a `load_tools` call.
81
+ */
82
+ export declare const PRESELECT_SYSTEM: string;
83
+ export declare const preselectInput: (catalog: CatalogServer[], prompt: string) => string;
84
+ /** Resolves a preselection against the catalogue: unknown names dropped, count capped. */
85
+ export declare function preselection(names: unknown, catalog: CatalogServer[]): string[];