@cubicecho/agent-core 2.3.0 → 2.5.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.
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Tool arguments that could not be read as an object, with why.
3
+ *
4
+ * `truncated` is a call cut off at the reply ceiling, which no repair can finish and the fix for
5
+ * is a larger `maxTokens`; `malformed` is one the model wrote wrongly, which it can be told about
6
+ * and try again.
7
+ */
8
+ export class ToolArgumentsError extends Error {
9
+ /** Whether the model ran out of room or wrote something unreadable. */
10
+ kind;
11
+ /**
12
+ * @param kind Why the arguments could not be read.
13
+ * @param message What the model is handed back as the tool's result.
14
+ */
15
+ constructor(kind, message) {
16
+ super(message);
17
+ this.name = "ToolArgumentsError";
18
+ this.kind = kind;
19
+ }
20
+ }
21
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
22
+ /**
23
+ * Rewrites the almost-JSON local models write into JSON, in one pass that knows where strings are.
24
+ *
25
+ * Single-quoted strings become double-quoted, Python's `True`, `False` and `None` become their JSON
26
+ * spellings, bare keys are quoted, and a comma before a closing bracket is dropped. Nothing inside
27
+ * a string is touched, so an argument that happens to say `True` or `a, }` survives.
28
+ */
29
+ function repairJson(text) {
30
+ let out = "";
31
+ for (let i = 0; i < text.length;) {
32
+ const char = text[i];
33
+ if (char === '"' || char === "'") {
34
+ let body = "";
35
+ let j = i + 1;
36
+ for (; j < text.length && text[j] !== char; j++) {
37
+ if (text[j] === "\\" && j + 1 < text.length) {
38
+ // `\'` means nothing in JSON; a quote that needed escaping in single quotes does not.
39
+ body += char === "'" && text[j + 1] === "'" ? "'" : text[j] + text[j + 1];
40
+ j++;
41
+ }
42
+ else {
43
+ body += char === "'" && text[j] === '"' ? '\\"' : text[j];
44
+ }
45
+ }
46
+ out += `"${body}"`;
47
+ i = j + 1;
48
+ continue;
49
+ }
50
+ if (char === ",") {
51
+ const next = text.slice(i + 1).match(/^\s*([\]}])?/);
52
+ if (next?.[1]) {
53
+ i++;
54
+ continue;
55
+ }
56
+ }
57
+ const word = /[A-Za-z_$]/.test(char)
58
+ ? text.slice(i).match(/^[A-Za-z_$][\w$]*/)?.[0]
59
+ : undefined;
60
+ if (word) {
61
+ const python = { True: "true", False: "false", None: "null" }[word];
62
+ if (/^\s*:/.test(text.slice(i + word.length)))
63
+ out += `"${word}"`;
64
+ else
65
+ out += python ?? word;
66
+ i += word.length;
67
+ continue;
68
+ }
69
+ out += char;
70
+ i++;
71
+ }
72
+ return out;
73
+ }
74
+ /** JSON as it was written, then repaired, then undefined. A string holding JSON is opened once. */
75
+ function looseJson(text) {
76
+ for (const candidate of [text, repairJson(text)]) {
77
+ try {
78
+ const value = JSON.parse(candidate);
79
+ if (typeof value !== "string")
80
+ return value;
81
+ const inner = value.trim();
82
+ if (!/^[[{]/.test(inner))
83
+ return value;
84
+ return looseJson(inner) ?? value;
85
+ }
86
+ catch {
87
+ // The next candidate.
88
+ }
89
+ }
90
+ return undefined;
91
+ }
92
+ /**
93
+ * A tool call's arguments as the object the tool is handed. Empty is no arguments.
94
+ *
95
+ * Lenient where the model's meaning is plain and strict where it is not: an object already
96
+ * parsed passes through, JSON inside a string is opened, and the almost-JSON local models write —
97
+ * single quotes, `True`, bare keys, a trailing comma — is repaired. What is still not an object
98
+ * throws a `ToolArgumentsError`, and the loop hands its message back to the model as the tool's
99
+ * result so it can try again.
100
+ *
101
+ * @param raw The arguments as the model sent them: usually the streamed string, sometimes an
102
+ * object a server parsed already. Null, absent or blank is no arguments.
103
+ * @param options `finishReason`, the turn's. A turn that stopped at `"length"` makes a failure
104
+ * `truncated`, since a call cut off at the ceiling reads exactly like a malformed one.
105
+ */
106
+ export function parseToolArguments(raw, { finishReason } = {}) {
107
+ if (isRecord(raw))
108
+ return raw;
109
+ if (raw === null || raw === undefined)
110
+ return {};
111
+ const text = typeof raw === "string" ? raw.trim() : JSON.stringify(raw);
112
+ if (!text)
113
+ return {};
114
+ const parsed = looseJson(text);
115
+ if (isRecord(parsed))
116
+ return parsed;
117
+ if (finishReason === "length") {
118
+ throw new ToolArgumentsError("truncated", `the tool call was cut off at the reply ceiling before its arguments were complete; raise maxTokens: ${text.slice(0, 200)}`);
119
+ }
120
+ throw new ToolArgumentsError("malformed", parsed === undefined
121
+ ? `model produced invalid tool arguments: ${text.slice(0, 200)}`
122
+ : `model produced tool arguments that are not an object: ${text.slice(0, 200)}`);
123
+ }
124
+ /**
125
+ * Where the JSON value opening at `start` closes, one past its last character, or -1 when it
126
+ * never does. Brackets inside either kind of string are not counted.
127
+ */
128
+ function valueEnd(text, start) {
129
+ let depth = 0;
130
+ let quote = "";
131
+ for (let i = start; i < text.length; i++) {
132
+ const char = text[i];
133
+ if (quote) {
134
+ if (char === "\\")
135
+ i++;
136
+ else if (char === quote)
137
+ quote = "";
138
+ }
139
+ else if (char === '"' || char === "'") {
140
+ quote = char;
141
+ }
142
+ else if (char === "{" || char === "[") {
143
+ depth++;
144
+ }
145
+ else if (char === "}" || char === "]") {
146
+ depth--;
147
+ if (depth === 0)
148
+ return i + 1;
149
+ }
150
+ }
151
+ return -1;
152
+ }
153
+ /**
154
+ * The JSON value opening at or after `at`, past whitespace, and where it ends. An unclosed one
155
+ * runs to the end of the text; one that does not parse even repaired is undefined.
156
+ */
157
+ function readValue(text, at) {
158
+ const start = at + (text.slice(at).match(/^\s*/)?.[0].length ?? 0);
159
+ if (text[start] !== "{" && text[start] !== "[")
160
+ return undefined;
161
+ const closed = valueEnd(text, start);
162
+ const end = closed < 0 ? text.length : closed;
163
+ const value = looseJson(text.slice(start, end));
164
+ return value === undefined ? undefined : { value, end };
165
+ }
166
+ /** One call in any of the shapes templates write: `{name, arguments}`, `{name, parameters}`, `{function: {...}}`. */
167
+ function toCall(entry) {
168
+ if (!isRecord(entry))
169
+ return undefined;
170
+ const inner = isRecord(entry.function) ? entry.function : entry;
171
+ const name = inner.name;
172
+ if (typeof name !== "string" || !name)
173
+ return undefined;
174
+ const args = inner.arguments ?? inner.parameters ?? inner.args ?? {};
175
+ return { name, arguments: typeof args === "string" ? args : JSON.stringify(args) };
176
+ }
177
+ /** Every call in a value that is one call or a list of them, or undefined if any entry is not one. */
178
+ function toCalls(value) {
179
+ const entries = Array.isArray(value) ? value : [value];
180
+ const calls = entries.map(toCall);
181
+ return calls.length && calls.every((call) => call)
182
+ ? calls
183
+ : undefined;
184
+ }
185
+ /** A value read as text: JSON where it parses, the string where it does not. */
186
+ function scalar(text) {
187
+ try {
188
+ return JSON.parse(text);
189
+ }
190
+ catch {
191
+ return text;
192
+ }
193
+ }
194
+ /** `<tool_call>` blocks: Hermes and Qwen's JSON, and Qwen3-Coder's `<function=…>` markup. */
195
+ function taggedCalls(text) {
196
+ const found = [];
197
+ for (const match of text.matchAll(/<tool_call>/g)) {
198
+ const at = match.index + match[0].length;
199
+ const closing = /\s*<\/tool_call>/y;
200
+ const xml = text.slice(at).match(/^\s*<function=([^>\s]+)>([\s\S]*?)<\/function>/);
201
+ if (xml) {
202
+ const args = {};
203
+ for (const param of xml[2].matchAll(/<parameter=([^>\s]+)>\n?([\s\S]*?)\n?<\/parameter>/g)) {
204
+ args[param[1]] = scalar(param[2]);
205
+ }
206
+ closing.lastIndex = at + xml[0].length;
207
+ const end = closing.test(text) ? closing.lastIndex : at + xml[0].length;
208
+ found.push({
209
+ start: match.index,
210
+ end,
211
+ calls: [{ name: xml[1], arguments: JSON.stringify(args) }],
212
+ });
213
+ continue;
214
+ }
215
+ const read = readValue(text, at);
216
+ const calls = read && toCalls(read.value);
217
+ if (!read || !calls)
218
+ continue;
219
+ closing.lastIndex = read.end;
220
+ found.push({
221
+ start: match.index,
222
+ end: closing.test(text) ? closing.lastIndex : read.end,
223
+ calls,
224
+ });
225
+ }
226
+ return found;
227
+ }
228
+ /** Mistral's `[TOOL_CALLS] [...]`, and the newer `[TOOL_CALLS]name[ARGS]{...}`. */
229
+ function mistralCalls(text) {
230
+ const found = [];
231
+ for (const match of text.matchAll(/\[TOOL_CALLS\]/g)) {
232
+ const at = match.index + match[0].length;
233
+ const named = text.slice(at).match(/^\s*([\w.-]+)\[ARGS\]/);
234
+ if (named) {
235
+ const read = readValue(text, at + named[0].length);
236
+ if (!read || !isRecord(read.value))
237
+ continue;
238
+ found.push({
239
+ start: match.index,
240
+ end: read.end,
241
+ calls: [{ name: named[1], arguments: JSON.stringify(read.value) }],
242
+ });
243
+ continue;
244
+ }
245
+ const read = readValue(text, at);
246
+ const calls = read && toCalls(read.value);
247
+ if (read && calls)
248
+ found.push({ start: match.index, end: read.end, calls });
249
+ }
250
+ return found;
251
+ }
252
+ /** Llama 3's `<|python_tag|>{...}`, several calls separated by semicolons, up to `<|eom_id|>`. */
253
+ function pythonTagCalls(text) {
254
+ const found = [];
255
+ for (const match of text.matchAll(/<\|python_tag\|>/g)) {
256
+ const calls = [];
257
+ let end = match.index + match[0].length;
258
+ for (;;) {
259
+ const read = readValue(text, end);
260
+ const more = read && toCalls(read.value);
261
+ if (!read || !more)
262
+ break;
263
+ calls.push(...more);
264
+ end = read.end;
265
+ const separator = text.slice(end).match(/^\s*;/);
266
+ if (!separator)
267
+ break;
268
+ end += separator[0].length;
269
+ }
270
+ const eom = text.slice(end).match(/^\s*<\|eom_id\|>/);
271
+ if (eom)
272
+ end += eom[0].length;
273
+ if (calls.length)
274
+ found.push({ start: match.index, end, calls });
275
+ }
276
+ return found;
277
+ }
278
+ /**
279
+ * A bare JSON call — the whole reply, or its one fenced block — naming only tools that exist.
280
+ * Without the names this is too easily an answer that happens to be JSON.
281
+ */
282
+ function bareCalls(text, names) {
283
+ if (!names.size)
284
+ return [];
285
+ const known = (calls) => calls?.every((call) => names.has(call.name)) ? calls : undefined;
286
+ const start = text.search(/\S/);
287
+ if (start >= 0 && (text[start] === "{" || text[start] === "[")) {
288
+ const read = readValue(text, start);
289
+ const calls = read && !text.slice(read.end).trim() ? known(toCalls(read.value)) : undefined;
290
+ if (read && calls)
291
+ return [{ start, end: read.end, calls }];
292
+ }
293
+ const fences = [...text.matchAll(/```(?:json)?[ \t]*\n?([\s\S]*?)```/gi)];
294
+ if (fences.length !== 1)
295
+ return [];
296
+ const [fence] = fences;
297
+ const body = fence[1].trim();
298
+ const read = /^[[{]/.test(body) ? readValue(body, 0) : undefined;
299
+ const calls = read && !body.slice(read.end).trim() ? known(toCalls(read.value)) : undefined;
300
+ return calls ? [{ start: fence.index, end: fence.index + fence[0].length, calls }] : [];
301
+ }
302
+ /**
303
+ * Tool calls a model wrote into its reply as text, taken out of it and made into calls.
304
+ *
305
+ * A server whose tool-call parser does not match the model's chat template streams the call as
306
+ * content, and the run ends on what reads like a finished answer. The templates' own markers are
307
+ * looked for — `<tool_call>` (Hermes, Qwen, Qwen3-Coder's markup included), `[TOOL_CALLS]`
308
+ * (Mistral, both spellings) and `<|python_tag|>` (Llama 3) — and, failing those, a reply that is
309
+ * nothing but a JSON call, or holds one fenced one, provided every name in it is in `names`. Only
310
+ * text after the last `</think>` is searched, since a model deliberating about a call is not making
311
+ * one.
312
+ *
313
+ * @param content The turn's text.
314
+ * @param options `names`, the tools that exist. Without them only the templates' markers count.
315
+ * @returns The text with the calls taken out, and the calls, numbered `call_recovered_0` onward.
316
+ * No calls leaves the text as it was.
317
+ */
318
+ export function recoverToolCalls(content, { names = [] } = {}) {
319
+ const thought = content.toLowerCase().lastIndexOf("</think>");
320
+ const from = thought < 0 ? 0 : thought + "</think>".length;
321
+ const tail = content.slice(from);
322
+ let found = [...taggedCalls(tail), ...mistralCalls(tail), ...pythonTagCalls(tail)];
323
+ if (!found.length)
324
+ found = bareCalls(tail, new Set(names));
325
+ if (!found.length)
326
+ return { content, toolCalls: [] };
327
+ found.sort((a, b) => a.start - b.start);
328
+ let rest = "";
329
+ let cursor = 0;
330
+ const toolCalls = [];
331
+ for (const span of found) {
332
+ if (span.start < cursor)
333
+ continue;
334
+ rest += tail.slice(cursor, span.start);
335
+ cursor = span.end;
336
+ for (const call of span.calls) {
337
+ toolCalls.push({
338
+ id: `call_recovered_${toolCalls.length}`,
339
+ type: "function",
340
+ function: call,
341
+ });
342
+ }
343
+ }
344
+ rest += tail.slice(cursor);
345
+ return { content: `${content.slice(0, from)}${rest}`.trim(), toolCalls };
346
+ }
@@ -135,6 +135,25 @@ export declare function requestedNames(args: Record<string, unknown>): string[];
135
135
  * same number: this one is what the preselector is told, and that one is what it is held to.
136
136
  */
137
137
  export declare const preselectSystem: (maxPerLoad?: number) => string;
138
+ /**
139
+ * The shape a preselector's answer is held to where the server takes a schema: `{ tools: [...] }`.
140
+ *
141
+ * An object around the array rather than the array, because a structured answer's root has to be
142
+ * an object — OpenAI's strict mode and every tool-schema normaliser insist.
143
+ */
144
+ export declare const PRESELECT_SCHEMA: {
145
+ type: string;
146
+ properties: {
147
+ tools: {
148
+ type: string;
149
+ items: {
150
+ type: string;
151
+ };
152
+ };
153
+ };
154
+ required: string[];
155
+ additionalProperties: boolean;
156
+ };
138
157
  /** The preselection system prompt at the default cap, for a caller that never changes it. */
139
158
  export declare const PRESELECT_SYSTEM: string;
140
159
  /**
@@ -151,7 +170,8 @@ export declare const preselectInput: (catalog: CatalogServer[], prompt: string,
151
170
  /**
152
171
  * Resolves a preselection against the catalogue: unknown names dropped, count capped.
153
172
  *
154
- * @param names What the preselector replied. Unvalidated: a non-array gives none, and entries
173
+ * @param names What the preselector replied: `{ tools: [...] }` as `PRESELECT_SCHEMA` has it, or
174
+ * the bare array an older prompt asked for. Unvalidated: anything else gives none, and entries
155
175
  * that are not strings are dropped.
156
176
  * @param catalog The servers to resolve against.
157
177
  * @param maxPerLoad The most to keep, defaulting to `MAX_PER_LOAD`. The same number
@@ -272,9 +272,22 @@ const PRESELECT_PROMPT_CHARS = 2000;
272
272
  * same number: this one is what the preselector is told, and that one is what it is held to.
273
273
  */
274
274
  export const preselectSystem = (maxPerLoad = MAX_PER_LOAD) => "You choose tools. Below is a catalogue of tool names, then a request. Reply with a JSON " +
275
- "array of the names the request is likely to need — exact names from the catalogue, at most " +
276
- `${maxPerLoad}, and as few as could do the job. Reply with \`[]\` if the request can be ` +
277
- "answered without tools. Reply with the array alone — no prose, no explanation.";
275
+ 'object whose "tools" array holds the names the request is likely to need — exact names from ' +
276
+ `the catalogue, at most ${maxPerLoad}, and as few as could do the job. Reply with ` +
277
+ '`{"tools": []}` if the request can be answered without tools. Reply with the object alone — ' +
278
+ "no prose, no explanation.";
279
+ /**
280
+ * The shape a preselector's answer is held to where the server takes a schema: `{ tools: [...] }`.
281
+ *
282
+ * An object around the array rather than the array, because a structured answer's root has to be
283
+ * an object — OpenAI's strict mode and every tool-schema normaliser insist.
284
+ */
285
+ export const PRESELECT_SCHEMA = {
286
+ type: "object",
287
+ properties: { tools: { type: "array", items: { type: "string" } } },
288
+ required: ["tools"],
289
+ additionalProperties: false,
290
+ };
278
291
  /** The preselection system prompt at the default cap, for a caller that never changes it. */
279
292
  export const PRESELECT_SYSTEM = preselectSystem();
280
293
  /**
@@ -291,15 +304,19 @@ export const preselectInput = (catalog, prompt, maxPromptChars = PRESELECT_PROMP
291
304
  /**
292
305
  * Resolves a preselection against the catalogue: unknown names dropped, count capped.
293
306
  *
294
- * @param names What the preselector replied. Unvalidated: a non-array gives none, and entries
307
+ * @param names What the preselector replied: `{ tools: [...] }` as `PRESELECT_SCHEMA` has it, or
308
+ * the bare array an older prompt asked for. Unvalidated: anything else gives none, and entries
295
309
  * that are not strings are dropped.
296
310
  * @param catalog The servers to resolve against.
297
311
  * @param maxPerLoad The most to keep, defaulting to `MAX_PER_LOAD`. The same number
298
312
  * `preselectSystem` was given, or the model is being held to a cap it was never told about.
299
313
  */
300
314
  export function preselection(names, catalog, maxPerLoad = MAX_PER_LOAD) {
301
- if (!Array.isArray(names))
315
+ const list = names && typeof names === "object" && !Array.isArray(names)
316
+ ? names.tools
317
+ : names;
318
+ if (!Array.isArray(list))
302
319
  return [];
303
- const wanted = names.filter((name) => typeof name === "string");
320
+ const wanted = list.filter((name) => typeof name === "string");
304
321
  return expandNames(wanted, catalog, maxPerLoad).matched.slice(0, maxPerLoad);
305
322
  }
package/llms.txt CHANGED
@@ -9,6 +9,21 @@ Full prose, worked examples and the reasoning behind each seam are in README.md;
9
9
 
10
10
  ## Exports
11
11
 
12
+ ### agent-loop
13
+
14
+ The loop above a turn: send, run the tools the model asked for, send again, until it stops asking.
15
+
16
+ - `AgentLoopHooks` (type) — The hooks a loop runs around one question.
17
+ - `AgentLoopOptions` (type) — What `runAgentLoop` takes.
18
+ - `AgentLoopResult` (type) — What a finished loop hands back.
19
+ - `buildBody` — The one place a streamed request's body is decided from a config and what the endpoint and the model have refused.
20
+ - `preselect` — The tools a request is likely to need, picked by a small model before the run starts, or none.
21
+ - `preview` — A long tool argument or result cut to what a watcher needs, with the full length said.
22
+ - `resolveApiKey` — The key to send, where an endpoint may inherit one from the settings it overrides.
23
+ - `runAgentLoop` — Runs a question to its answer: one `runTurn` per step, the tools it asks for between them, until a turn asks for none.
24
+ - `ToolCallOutcome` (type) — What one tool call did, in the order the model asked.
25
+ - `ToolCallRequest` (type) — One call the model made, as `dispatch` is handed it.
26
+
12
27
  ### capabilities
13
28
 
14
29
  What an endpoint turned out not to support, and answering it when it says so.
@@ -37,6 +52,23 @@ The contract between whatever holds the tools and the loop that offers them to a
37
52
  - `resetClients` — Forgets every cached client and listing.
38
53
  - `timeoutMs` — Zero, less, or absent means no limit, which the SDK spells as `undefined`.
39
54
 
55
+ ### compaction
56
+
57
+ Keeping a long run inside its window: stale tool results cleared, and the oldest stretch folded into a summary the model writes itself.
58
+
59
+ - `COMPACT_AT` — The fraction of the window in use before a summary is worth its own round trip.
60
+ - `CompactionOptions` (type) — What `planCompaction` takes.
61
+ - `CompactionPlan` (type) — Where to cut, as `compactTranscript` takes it.
62
+ - `compactTranscript` — The transcript with the plan's stretch replaced by one system message holding its summary.
63
+ - `KEEP_RATIO` — The fraction of the window the kept tail may fill, leaving room for the run to grow again.
64
+ - `PruneOptions` (type) — What `pruneToolResults` takes.
65
+ - `planCompaction` — Where to fold a transcript that has grown into its window, or `undefined` when it should not be.
66
+ - `pruneToolResults` — The transcript with every tool result but the latest few replaced by a one-line stub.
67
+ - `SUMMARY_LEAD` — How a summary message opens, which is also how `planCompaction` knows one from a system prompt.
68
+ - `SUMMARY_PROMPT` — The summariser's instruction when the caller gives none.
69
+ - `summariser` — A summariser that asks `model` with `SUMMARY_PROMPT`, for `compactTranscript`.
70
+ - `summaryInput` — What the summariser is handed for a plan: the earlier summary if there was one, then each message as its role and at most 4000 characters of its text.
71
+
40
72
  ### config
41
73
 
42
74
  What this package needs to know about a caller's configuration.
@@ -73,19 +105,22 @@ What a run is doing, while it is doing it.
73
105
  Lifecycle hooks, from the host's side: what a session looks like to them, where their context lands in a request, and what is said about each one.
74
106
 
75
107
  - `assembleContext` — Builds the context a set of outcomes adds and the notes that go with it.
108
+ - `configureHooks` — Changes what hooks are held to, for a process whose windows are not the size these defaults were chosen for.
76
109
  - `Gathered` (type) — The context a set of outcomes adds to a request, and a note for each hook worth mentioning.
77
110
  - `gather` — Runs the hooks ahead of a request and builds what they add to it.
78
- - `HOOK_CONTEXT_TOKENS` — The most context all of a request's hooks add between them, in estimated tokens.
111
+ - `HOOK_CONTEXT_TOKENS` — The most context all of a request's hooks add between them by default, in estimated tokens.
79
112
  - `HOOK_EVENTS` — Every event a hook can be bound to, in the order a session meets them.
80
113
  - `HOOK_PREFACE` — Said once, above the blocks, so the model reads them as background rather than instructions.
81
114
  - `HookContext` (type) — What a host knows at an event.
82
115
  - `HookEvent` (type) — A point in a session a hook can be bound to.
83
116
  - `HookMessage` (type) — One message of a session as a hook is handed it.
84
117
  - `HookNote` (type) — One hook's line for whoever is watching: the context it added, or why it added none.
118
+ - `HookOptions` (type) — What hooks are held to across a process.
85
119
  - `HookOutcome` (type) — What one hook did.
86
120
  - `HookRunner` (type) — Runs one event's hooks.
87
121
  - `INJECT_EVENTS` — The events whose hooks run before a request, and so the only ones whose output can reach it.
88
122
  - `notify` — Runs the hooks for an event that reads what happened and adds nothing to a request.
123
+ - `resetHooks` — Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the next's.
89
124
  - `turnIndex` — Which turn of a session begins at a point, from 0: the user messages ahead of it.
90
125
  - `turnMessages` — A stretch of a transcript as a hook reads it: what the user and the assistant said, and nothing else.
91
126
  - `withContext` — The request, with the hooks' context added to this turn's question.
@@ -104,6 +139,7 @@ Everything about a request failing that is not about what the request said.
104
139
  - `EndpointSilent` — The endpoint stopped answering mid-request.
105
140
  - `isOverflow` — Whether a refusal means the request was too big, rather than merely refused.
106
141
  - `isTransient` — Whether a failed request is worth trying again.
142
+ - `messageTokens` — One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
107
143
  - `requestTokens` — What this request will cost the window, in tokens, near enough.
108
144
  - `SMALLEST_LIKELY_WINDOW` — The smallest window worth believing in, and the floor under `runTurn`'s guard.
109
145
  - `sleep` — A delay an abort cuts short, rejecting rather than resolving early.
@@ -127,7 +163,9 @@ JSON Schema compatibility for llama.cpp-backed servers.
127
163
 
128
164
  One-shot calls that support a run without being one: picking tools, naming a session, summarising a transcript, proposing follow-ups.
129
165
 
166
+ - `AskJsonOptions` (type) — What `askJson` takes besides a side task's options.
130
167
  - `ask` — Runs a side task and returns the reply text, thinking stripped.
168
+ - `askJson` — A side task whose answer is JSON matching a schema, parsed.
131
169
  - `clean` — Strips the quoting and list punctuation models decorate short answers with.
132
170
  - `listLines` — A list-shaped reply, one item per line, cleaned of the bullets and quotes models decorate them with.
133
171
  - `parseJson` — Models are asked for JSON and often answer with prose around it, or a fenced block.
@@ -135,6 +173,17 @@ One-shot calls that support a run without being one: picking tools, naming a ses
135
173
  - `SideTaskOptions` (type) — What a side task may be given.
136
174
  - `tryAsk` — A side task is never worth failing the work it supports.
137
175
 
176
+ ### snapshot
177
+
178
+ What endpoints and models refused, carried across a restart.
179
+
180
+ - `CAPABILITY_SNAPSHOT_VERSION` — The version `importCapabilities` accepts.
181
+ - `CapabilitySnapshot` (type) — Every latched refusal in the process, JSON-safe.
182
+ - `EndpointSnapshot` (type) — What one endpoint refused, and under it what each of its models did.
183
+ - `exportCapabilities` — Every refusal this process has latched, as a JSON-safe blob to store and hand back on boot.
184
+ - `importCapabilities` — Latches what a stored snapshot says was refused, on top of whatever this process has learned.
185
+ - `ModelSnapshot` (type) — What one model on an endpoint refused.
186
+
138
187
  ### stream
139
188
 
140
189
  Reading one streamed turn back into a message.
@@ -149,6 +198,15 @@ Reading one streamed turn back into a message.
149
198
 
150
199
  - `estimateTokens` — Rough token count.
151
200
 
201
+ ### tool-calls
202
+
203
+ Reading what a model meant by a tool call when it did not write one cleanly.
204
+
205
+ - `parseToolArguments` — A tool call's arguments as the object the tool is handed.
206
+ - `recoverToolCalls` — Tool calls a model wrote into its reply as text, taken out of it and made into calls.
207
+ - `ToolArgumentsError` — Tool arguments that could not be read as an object, with why.
208
+ - `ToolCall` (type) — A tool call as the loop handles it, recovered or streamed.
209
+
152
210
  ### tool-loading
153
211
 
154
212
  - `carryOver` — The tools to start the next turn with: recently used, newest last, capped.
@@ -161,6 +219,7 @@ Reading one streamed turn back into a message.
161
219
  - `loadResult` — What `load_tools` reports back: the descriptions, now that they are worth their tokens.
162
220
  - `MAX_CARRIED` — The most a conversation carries between turns.
163
221
  - `MAX_PER_LOAD` — The most a single `load_tools` call may pull in.
222
+ - `PRESELECT_SCHEMA` — The shape a preselector's answer is held to where the server takes a schema: `{ tools: [...] }`.
164
223
  - `PRESELECT_SYSTEM` — The preselection system prompt at the default cap, for a caller that never changes it.
165
224
  - `preselectInput` — The user message for a preselection call: the catalogue, then the request.
166
225
  - `preselection` — Resolves a preselection against the catalogue: unknown names dropped, count capped.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
5
5
  "keywords": [
6
6
  "openai",