@cubicecho/agent-core 2.23.0 → 2.25.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.
Files changed (71) hide show
  1. package/README.md +14 -9
  2. package/dist/agent-loop.d.ts +84 -204
  3. package/dist/agent-loop.js +416 -643
  4. package/dist/calibration.d.ts +14 -10
  5. package/dist/calibration.js +30 -22
  6. package/dist/capabilities.d.ts +140 -35
  7. package/dist/capabilities.js +345 -152
  8. package/dist/client.d.ts +104 -35
  9. package/dist/client.js +210 -78
  10. package/dist/compaction.d.ts +103 -60
  11. package/dist/compaction.js +163 -93
  12. package/dist/config.d.ts +48 -12
  13. package/dist/config.js +20 -1
  14. package/dist/continuation.d.ts +25 -17
  15. package/dist/continuation.js +70 -47
  16. package/dist/digest.d.ts +7 -0
  17. package/dist/digest.js +18 -0
  18. package/dist/errors.d.ts +24 -15
  19. package/dist/errors.js +23 -15
  20. package/dist/events.d.ts +108 -58
  21. package/dist/events.js +265 -128
  22. package/dist/guards.d.ts +53 -0
  23. package/dist/guards.js +55 -0
  24. package/dist/hook-events.d.ts +38 -0
  25. package/dist/hook-events.js +36 -0
  26. package/dist/hooks.d.ts +105 -83
  27. package/dist/hooks.js +224 -139
  28. package/dist/index.d.ts +30 -25
  29. package/dist/index.js +29 -23
  30. package/dist/ledger.d.ts +62 -30
  31. package/dist/ledger.js +163 -56
  32. package/dist/platform.d.ts +13 -0
  33. package/dist/platform.js +13 -0
  34. package/dist/preselect.d.ts +222 -0
  35. package/dist/preselect.js +290 -0
  36. package/dist/request-body.d.ts +32 -0
  37. package/dist/request-body.js +47 -0
  38. package/dist/reset.d.ts +1 -0
  39. package/dist/reset.js +7 -6
  40. package/dist/retry.d.ts +40 -143
  41. package/dist/retry.js +79 -285
  42. package/dist/run-calls.d.ts +123 -0
  43. package/dist/run-calls.js +351 -0
  44. package/dist/run-turn.d.ts +44 -16
  45. package/dist/run-turn.js +74 -37
  46. package/dist/runtime.d.ts +23 -17
  47. package/dist/runtime.js +34 -24
  48. package/dist/schema-compat.d.ts +51 -11
  49. package/dist/schema-compat.js +375 -193
  50. package/dist/scope.d.ts +28 -5
  51. package/dist/scope.js +37 -10
  52. package/dist/side-task.d.ts +127 -39
  53. package/dist/side-task.js +189 -100
  54. package/dist/snapshot.d.ts +12 -7
  55. package/dist/snapshot.js +87 -65
  56. package/dist/spec.d.ts +66 -36
  57. package/dist/spec.js +514 -265
  58. package/dist/stream.d.ts +73 -16
  59. package/dist/stream.js +228 -98
  60. package/dist/thinking.d.ts +23 -8
  61. package/dist/thinking.js +96 -46
  62. package/dist/tokens.d.ts +171 -6
  63. package/dist/tokens.js +326 -6
  64. package/dist/tool-calls.d.ts +89 -15
  65. package/dist/tool-calls.js +205 -85
  66. package/dist/tool-loading.d.ts +109 -187
  67. package/dist/tool-loading.js +286 -372
  68. package/dist/wire.d.ts +81 -0
  69. package/dist/wire.js +73 -0
  70. package/llms.txt +87 -43
  71. package/package.json +4 -4
package/dist/tokens.js CHANGED
@@ -1,18 +1,338 @@
1
+ import { FUNCTION_TOOL, PartType, Role } from './wire.js';
2
+ /**
3
+ * The characters one token is taken to be, where nothing has measured it.
4
+ *
5
+ * @remarks
6
+ * The fallback, not the rule: once a turn has come back with a reported prompt count, `runTurn`
7
+ * divides by what that endpoint's model was measured at instead. See `charsPerTokenFor`.
8
+ */
9
+ export const CHARS_PER_TOKEN = 4;
1
10
  /**
2
11
  * Rough token count. Characters over four, because there is no tokenizer here and there is not
3
12
  * going to be one: a server that will not say how big its window is will not lend us its
4
13
  * vocabulary either.
5
14
  *
15
+ * @param text - Prose or serialised JSON — both counted the same way, which is why JSON reads low.
16
+ * @returns The length over `CHARS_PER_TOKEN`, rounded up: zero for an empty string, and one for
17
+ * anything shorter than a token.
18
+ *
19
+ * @remarks
6
20
  * The estimate runs low on tool schemas — JSON packs more tokens into a character than prose
7
21
  * does — and that is the side to be wrong on wherever it guards a window, since the cost of
8
22
  * guessing high is a run refused that would have worked, and the cost of guessing low is the
9
23
  * endpoint's own refusal, which is where we were before the guard existed.
10
24
  *
11
- * Its own module because it is the one number several of these agree on, and the module that
12
- * owns it should not be one that also does something. It was extracted from `side-task` to
13
- * break a cycle with `retry`; `side-task` no longer reads it, but `retry` and any consumer
14
- * sizing its own prompt still do, and a leaf with no imports is the right home for it.
25
+ * Its own module because it is the one number several of these agree on. It was extracted from
26
+ * `side-task` to break a cycle with `retry`; neither reads it now, but `hooks` does to cap a
27
+ * context block, as does any consumer sizing its own prompt. Everything else that sizes a request
28
+ * is below it — a message's characters, a tool block's, a request's, and the breakdown of a
29
+ * window — so the module imports nothing but the SDK's types and anything may read it.
30
+ */
31
+ export const estimateTokens = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);
32
+ /** Where `compact` starts counting in thousands. */
33
+ const THOUSAND = 1000;
34
+ /**
35
+ * A token count as a message shows it: 1234 → "1.2k".
36
+ *
37
+ * @param tokens - The count to render.
38
+ * @returns The count in thousands to one decimal place with a `k`, or the number as it is below a
39
+ * thousand.
40
+ *
41
+ * @remarks
42
+ * The numbers in an overflow message are large and nobody reads the units digit.
43
+ */
44
+ export const compact = (tokens) => (tokens >= THOUSAND ? `${(tokens / THOUSAND).toFixed(1)}k` : String(tokens));
45
+ /** What `{"role":"","content":""},` costs around a message's own text, in characters. */
46
+ const ENVELOPE = 25;
47
+ /** The same for `{"id":"","type":"function","function":{"name":"","arguments":""}},` in a call. */
48
+ const CALL_ENVELOPE = 66;
49
+ // `ENVELOPE` is the two keys every message has. These are the three that only some do, and each
50
+ // is the key with its punctuation and the comma after the value it holds — the value's own
51
+ // length is counted where the value is read. Applying `ENVELOPE` alone to these shapes left the
52
+ // keys out, which cost 4.5 tokens on every tool result: the message a tool-using run has most
53
+ // of, and short in the direction that lets an overflow through the guard meant to catch it.
54
+ /** What `"name":"",` costs around a message's name. */
55
+ const NAME_KEY = 10;
56
+ /** The same for `"tool_call_id":"",` around a tool result's call id. */
57
+ const TOOL_CALL_ID_KEY = 18;
58
+ /** The same for `"tool_calls":[]` around the calls; each call's own comma is in `CALL_ENVELOPE`. */
59
+ const TOOL_CALLS_KEY = 15;
60
+ // A content part is an object in the body the same way a message is, and its two keys are in the
61
+ // request exactly as `tool_call_id` was. Charging only `part.text` left them out, which is 6.5
62
+ // tokens per part — short in proportion to how finely the content is split rather than to how
63
+ // much it says, so a transcript a client appends block by block is worst hit.
64
+ /** What `{"type":"text","text":""},` costs around a text part. */
65
+ const TEXT_PART = 26;
66
+ /** The same for `{"type":"refusal","refusal":""},` around a refusal part. */
67
+ const REFUSAL_PART = 32;
68
+ /**
69
+ * The same for `"reasoning_content":"",` around an assistant message's scratchpad.
70
+ *
71
+ * @remarks
72
+ * Not in the SDK's types, and passed back by the caller who keeps it: gpt-oss and DeepSeek in
73
+ * thinking mode want the analysis behind a tool call on the next request. Left uncounted, the
74
+ * guard came up short by the whole scratchpad on exactly the runs that follow that rule.
75
+ */
76
+ const REASONING_KEY = 23;
77
+ /** The same for `"reasoning":"",`, OpenRouter's spelling of it. */
78
+ const REASONING_ALT_KEY = 15;
79
+ /**
80
+ * The divisor an option asked for, or the fallback when it asked for nothing usable.
81
+ *
82
+ * @param charsPerToken - What the caller gave. Absent, zero, negative or `NaN` is nothing usable.
83
+ * @returns `charsPerToken` when it is above zero, otherwise `CHARS_PER_TOKEN`.
84
+ */
85
+ const divisor = (charsPerToken) => charsPerToken !== undefined && charsPerToken > 0 ? charsPerToken : CHARS_PER_TOKEN;
86
+ /**
87
+ * How many characters one message is worth: its keys, and its content in whichever shape.
88
+ *
89
+ * @param message - The message as it will be sent.
90
+ * @returns The characters of its role, its content — a string, or the text and refusal parts of a
91
+ * list — and its name, call id, reasoning and tool calls, plus a constant for each key and part
92
+ * that carries them. An image or audio part adds nothing.
93
+ */
94
+ export function messageChars(message) {
95
+ let chars = message.role.length + ENVELOPE;
96
+ const { content } = message;
97
+ if (typeof content === 'string') {
98
+ chars += content.length;
99
+ }
100
+ else if (Array.isArray(content)) {
101
+ for (const part of content) {
102
+ // Text and refusal parts carry their own strings; an image or an audio part carries a URL
103
+ // or a blob, and neither is priced by its length anyway — a vision model does not charge
104
+ // an image by its base64 length, so counting the data URL would overshoot by more than
105
+ // leaving the part out undershoots.
106
+ if (part.type === PartType.Text) {
107
+ chars += TEXT_PART + part.text.length;
108
+ }
109
+ else if (part.type === PartType.Refusal) {
110
+ chars += REFUSAL_PART + part.refusal.length;
111
+ }
112
+ }
113
+ }
114
+ if ('name' in message && typeof message.name === 'string') {
115
+ chars += NAME_KEY + message.name.length;
116
+ }
117
+ if ('tool_call_id' in message && typeof message.tool_call_id === 'string') {
118
+ chars += TOOL_CALL_ID_KEY + message.tool_call_id.length;
119
+ }
120
+ const { reasoning_content: reasoning, reasoning: alternate } = message;
121
+ if (typeof reasoning === 'string') {
122
+ chars += REASONING_KEY + reasoning.length;
123
+ }
124
+ if (typeof alternate === 'string') {
125
+ chars += REASONING_ALT_KEY + alternate.length;
126
+ }
127
+ if ('tool_calls' in message && Array.isArray(message.tool_calls)) {
128
+ chars += TOOL_CALLS_KEY;
129
+ for (const call of message.tool_calls) {
130
+ chars += CALL_ENVELOPE + call.id.length;
131
+ if (call.type === FUNCTION_TOOL) {
132
+ chars += call.function.name.length + call.function.arguments.length;
133
+ }
134
+ }
135
+ }
136
+ return chars;
137
+ }
138
+ /**
139
+ * The tools half, cached against the array.
140
+ *
141
+ * @remarks
142
+ * Serialising two dozen JSON schemas to measure them, on every turn, to get the same number every
143
+ * time, was the more expensive half of this function.
144
+ *
145
+ * The array is the key, so this pays only a caller that hands the same one back. That is not what
146
+ * a turn built through `sanitizeTools` or `relaxTools` does — both are a `map`, so each build
147
+ * allocates a fresh array however stable the tools inside it are, and those builds miss here every
148
+ * time. It is not free to change: keying on the tools instead would hit for them, at the cost of
149
+ * the array-level memoisation `tests/retry.test.ts` pins, which deliberately holds a mutated array
150
+ * to its first reading. Sizing happens once per turn either way, so the miss costs one walk of the
151
+ * schemas rather than a walk per attempt.
152
+ *
153
+ * Characters rather than tokens, so a calibrated divisor applies to the schemas without walking
154
+ * them again.
155
+ */
156
+ const toolLengths = new WeakMap();
157
+ /**
158
+ * How many characters a tool array is worth, measured once per array.
159
+ *
160
+ * @param tools - The tool definitions as they will be sent. An empty array is worth nothing.
161
+ * @returns The length of the array serialised as JSON, as it was the first time this array was
162
+ * asked about — a change made to it since is not seen.
163
+ */
164
+ export function toolsChars(tools) {
165
+ if (!tools.length) {
166
+ return 0;
167
+ }
168
+ const hit = toolLengths.get(tools);
169
+ if (hit !== undefined) {
170
+ return hit;
171
+ }
172
+ // Schemas are arbitrarily shaped, so this one really is a serialisation — but it happens once
173
+ // per tool array rather than once per turn.
174
+ const length = JSON.stringify(tools).length;
175
+ toolLengths.set(tools, length);
176
+ return length;
177
+ }
178
+ /**
179
+ * How many characters a request is worth: the walk `requestTokens` divides, without the division.
180
+ *
181
+ * @param body - The request as it was sent, tools included.
182
+ * @returns The sum of `messageChars` over the messages. The tools are not in it, whether or not
183
+ * the body carries them; `toolsChars` counts those.
184
+ *
185
+ * @remarks
186
+ * What calibration reads a reported prompt count against, since a ratio is only as good as the
187
+ * character count it was taken over agreeing with the one it is later applied to.
188
+ */
189
+ export function requestChars(body) {
190
+ let chars = 0;
191
+ for (const message of body.messages) {
192
+ chars += messageChars(message);
193
+ }
194
+ return chars;
195
+ }
196
+ /**
197
+ * What this request will cost the window, in tokens, near enough.
198
+ *
199
+ * @param body - The request as it will be sent, tools included.
200
+ * @param [options] - The divisor, `CHARS_PER_TOKEN` when none is given.
201
+ * @returns Whole tokens: the messages' and the tools', each rounded up on its own and added.
202
+ *
203
+ * @remarks
204
+ * See `estimateTokens` for why it is characters over four and which way it is wrong on purpose,
205
+ * and `charsPerTokenFor` for the divisor a model's own reported usage has measured instead.
206
+ *
207
+ * Summed by walking the body rather than by serialising it. `JSON.stringify` on the messages
208
+ * built the entire transcript into a string on every call and threw it away having read nothing
209
+ * but its `.length` — against a transcript that grows by a turn each turn, and one the SDK is
210
+ * about to serialise again to send. What the walk misses is JSON's own punctuation and the keys,
211
+ * which the envelope constants put back — one per key and one per content part, rather than one
212
+ * per message, since the keys only some shapes carry and the parts a client appends block by
213
+ * block are most of what a tool-using transcript is made of. What is left is a message's
214
+ * escaping, which is not a constant and is small against an estimate that is already characters
215
+ * over four.
216
+ */
217
+ export const requestTokens = (body, { charsPerToken } = {}) => {
218
+ // Characters first and the division once at the end, rather than a rounded count per message:
219
+ // `Math.ceil` on every one of a few hundred messages is a few hundred tokens of pure rounding.
220
+ // The tools are divided on their own, as they were when their tokens were cached, so the
221
+ // uncalibrated count is the one it always was.
222
+ const per = divisor(charsPerToken);
223
+ return Math.ceil(requestChars(body) / per) + Math.ceil(toolsChars(body.tools ?? []) / per);
224
+ };
225
+ /** The parts, in the order a readout reads them. */
226
+ const PARTS = ['system', 'tools', 'history', 'toolResults'];
227
+ /**
228
+ * What each part of a request is worth in characters, by the same walk `requestTokens` divides.
229
+ *
230
+ * @param body - The request as it will be sent, tools included.
231
+ * @returns A new breakdown in characters. A `developer` message counts as `system`, and anything
232
+ * that is neither that nor a `tool` message as `history`.
233
+ *
234
+ * @remarks
235
+ * Exact and additive: the parts sum to `total`, which is `requestChars` plus `toolsChars`. The
236
+ * conversion to tokens is `contextTokens`' business, because that is where an estimate and a
237
+ * reported count have to be told apart.
238
+ */
239
+ export function contextChars(body) {
240
+ const out = {
241
+ system: 0,
242
+ tools: toolsChars(body.tools ?? []),
243
+ history: 0,
244
+ toolResults: 0,
245
+ total: 0,
246
+ };
247
+ for (const message of body.messages) {
248
+ const chars = messageChars(message);
249
+ // Every system message and not just the leading one: a host that appends guidance, or a
250
+ // hook that injects a preface, has put more of the window there and wants to be told so.
251
+ if (message.role === Role.System || message.role === Role.Developer) {
252
+ out.system += chars;
253
+ }
254
+ else if (message.role === Role.Tool) {
255
+ out.toolResults += chars;
256
+ }
257
+ else {
258
+ out.history += chars;
259
+ }
260
+ }
261
+ out.total = out.system + out.tools + out.history + out.toolResults;
262
+ return out;
263
+ }
264
+ /**
265
+ * Shares `total` out over these parts by their character counts, the largest absorbing the
266
+ * rounding so they add up to it exactly rather than to within a few tokens of it.
267
+ *
268
+ * @param chars - What each part measured, in characters. Not written to.
269
+ * @param over - The parts to share between. One left out stays zero in the result.
270
+ * @param total - What to share out, in tokens.
271
+ * @returns A new breakdown whose `total` is `total` as given. Every part is zero where the parts
272
+ * measured nothing or `total` is not above zero.
273
+ */
274
+ function share(chars, over, total) {
275
+ const out = { system: 0, tools: 0, history: 0, toolResults: 0, total };
276
+ const measured = over.reduce((sum, part) => sum + chars[part], 0);
277
+ if (measured <= 0 || total <= 0) {
278
+ return out;
279
+ }
280
+ const absorber = over.reduce((a, b) => (chars[b] > chars[a] ? b : a));
281
+ let assigned = 0;
282
+ for (const part of over) {
283
+ if (part === absorber) {
284
+ continue;
285
+ }
286
+ out[part] = Math.round((chars[part] / measured) * total);
287
+ assigned += out[part];
288
+ }
289
+ out[absorber] = Math.max(0, total - assigned);
290
+ return out;
291
+ }
292
+ /**
293
+ * What each part of a request costs the window, in tokens, adding up to the whole.
294
+ *
295
+ * @param body - The request as it will be sent, tools included.
296
+ * @param [options] - The divisor, and the reported prompt count when there is one.
297
+ * @returns A new breakdown in tokens. Its `total` is the reported count when one above zero was
298
+ * given, and `requestTokens` of the same body otherwise.
299
+ *
300
+ * @remarks
301
+ * Shares rather than four independent estimates, because a readout whose parts do not add up to
302
+ * the total beside them is a readout nobody trusts. Nothing in the round trip reports anything
303
+ * finer than a prompt count — a completion says how many tokens it read and not a word about
304
+ * where they came from — so the proportions are an estimate whatever the total is.
305
+ *
306
+ * Without a reported count the total is `requestTokens`, and the tools are counted the way it and
307
+ * `TurnMetrics.toolSchemaTokens` count them rather than shared out, so the two agree by
308
+ * construction and an operator does not read one number for the tool block in the metrics and a
309
+ * different one here. With a reported count every part is a share of it, the tools included:
310
+ * that number is the server's, and the point of using it is that the parts sum to what was
311
+ * charged.
312
+ */
313
+ export function contextTokens(body, { charsPerToken, promptTokens } = {}) {
314
+ const chars = contextChars(body);
315
+ if (promptTokens !== undefined && promptTokens > 0) {
316
+ return share(chars, PARTS, promptTokens);
317
+ }
318
+ const per = divisor(charsPerToken);
319
+ const tools = Math.ceil(chars.tools / per);
320
+ const rest = Math.ceil((chars.total - chars.tools) / per);
321
+ const out = share(chars, ['system', 'history', 'toolResults'], rest);
322
+ out.tools = tools;
323
+ out.total = rest + tools;
324
+ return out;
325
+ }
326
+ /**
327
+ * One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
328
+ *
329
+ * @param message - The message as it will be sent.
330
+ * @param [options] - The divisor, `CHARS_PER_TOKEN` when none is given.
331
+ * @returns `messageChars` over the divisor, rounded up per message, where `requestTokens` rounds
332
+ * once over them all.
15
333
  *
16
- * @param text Prose or serialised JSON — both counted the same way, which is why JSON reads low.
334
+ * @remarks
335
+ * For the arithmetic that weighs part of a transcript against a window — `planCompaction`'s kept
336
+ * tail — where `estimateTokens` on the text alone would leave out the calls and the envelope.
17
337
  */
18
- export const estimateTokens = (text) => Math.ceil(text.length / 4);
338
+ export const messageTokens = (message, { charsPerToken } = {}) => Math.ceil(messageChars(message) / divisor(charsPerToken));
@@ -1,4 +1,4 @@
1
- import type OpenAI from "openai";
1
+ import type OpenAI from 'openai';
2
2
  /**
3
3
  * Reading what a model meant by a tool call when it did not write one cleanly.
4
4
  *
@@ -8,37 +8,60 @@ import type OpenAI from "openai";
8
8
  * but as text, in the model's own template, because the server's tool-call parser was written for
9
9
  * a different one. Both were being handled in every consumer, differently, or not at all.
10
10
  */
11
+ /** How much of a call's unreadable arguments an error quotes back. */
12
+ export declare const ARGUMENT_PREVIEW_CHARS = 200;
13
+ /** The `ToolArgumentsError` kind for a call cut off at the reply ceiling. */
14
+ export declare const ARGUMENTS_TRUNCATED: 'truncated';
15
+ /** The `ToolArgumentsError` kind for a call the model wrote wrongly. */
16
+ export declare const ARGUMENTS_MALFORMED: 'malformed';
11
17
  /** A tool call as the loop handles it, recovered or streamed. */
12
18
  export type ToolCall = OpenAI.ChatCompletionMessageFunctionToolCall;
13
19
  /**
14
20
  * Tool arguments that could not be read as an object, with why.
15
21
  *
22
+ * @remarks
16
23
  * `truncated` is a call cut off at the reply ceiling, which no repair can finish and the fix for
17
24
  * is a larger `maxTokens`; `malformed` is one the model wrote wrongly, which it can be told about
18
25
  * and try again.
19
26
  */
20
27
  export declare class ToolArgumentsError extends Error {
28
+ readonly name = "ToolArgumentsError";
21
29
  /** Whether the model ran out of room or wrote something unreadable. */
22
- readonly kind: "truncated" | "malformed";
30
+ readonly kind: typeof ARGUMENTS_TRUNCATED | typeof ARGUMENTS_MALFORMED;
23
31
  /**
24
- * @param kind Why the arguments could not be read.
25
- * @param message What the model is handed back as the tool's result.
32
+ * @param kind - Why the arguments could not be read.
33
+ * @param message - What the model is handed back as the tool's result.
26
34
  */
27
- constructor(kind: "truncated" | "malformed", message: string);
35
+ constructor(kind: typeof ARGUMENTS_TRUNCATED | typeof ARGUMENTS_MALFORMED, message: string);
28
36
  }
37
+ /**
38
+ * JSON as it was written, then repaired, then undefined. A string holding JSON is opened once.
39
+ *
40
+ * @param text - What a model wrote where JSON was asked for, with no prose around it.
41
+ * @returns The value — `null` for a JSON `null` — or `undefined` where neither the text nor its
42
+ * repair parses. A string holding an object or array that does not parse stays the string.
43
+ *
44
+ * @remarks
45
+ * Read as written first, so the repair can only ever add to what parses: nothing valid is
46
+ * reinterpreted on its way through.
47
+ */
48
+ export declare function looseJson(text: string): unknown;
29
49
  /**
30
50
  * A tool call's arguments as the object the tool is handed. Empty is no arguments.
31
51
  *
52
+ * @param raw - The arguments as the model sent them: usually the streamed string, sometimes an
53
+ * object a server parsed already. Null, absent or blank is no arguments.
54
+ * @param [options] - `finishReason`, the turn's. A turn that stopped at `"length"` makes a failure
55
+ * `truncated`, since a call cut off at the ceiling reads exactly like a malformed one.
56
+ * @returns `raw` itself where it was already an object, otherwise what its text parsed to, and a
57
+ * new empty object for no arguments.
58
+ *
59
+ * @remarks
32
60
  * Lenient where the model's meaning is plain and strict where it is not: an object already
33
61
  * parsed passes through, JSON inside a string is opened, and the almost-JSON local models write —
34
62
  * single quotes, `True`, bare keys, a trailing comma — is repaired. What is still not an object
35
63
  * throws a `ToolArgumentsError`, and the loop hands its message back to the model as the tool's
36
64
  * result so it can try again.
37
- *
38
- * @param raw The arguments as the model sent them: usually the streamed string, sometimes an
39
- * object a server parsed already. Null, absent or blank is no arguments.
40
- * @param options `finishReason`, the turn's. A turn that stopped at `"length"` makes a failure
41
- * `truncated`, since a call cut off at the ceiling reads exactly like a malformed one.
42
65
  */
43
66
  export declare function parseToolArguments(raw: unknown, { finishReason }?: {
44
67
  finishReason?: string | null;
@@ -46,6 +69,12 @@ export declare function parseToolArguments(raw: unknown, { finishReason }?: {
46
69
  /**
47
70
  * Tool calls a model wrote into its reply as text, taken out of it and made into calls.
48
71
  *
72
+ * @param content - The turn's text.
73
+ * @param [options] - `names`, the tools that exist. Without them only the templates' markers count.
74
+ * @returns The text with the calls taken out, and the calls, numbered `call_recovered_0` onward.
75
+ * No calls leaves the text as it was.
76
+ *
77
+ * @remarks
49
78
  * A server whose tool-call parser does not match the model's chat template streams the call as
50
79
  * content, and the run ends on what reads like a finished answer. The templates' own markers are
51
80
  * looked for — `<tool_call>` (Hermes, Qwen, Qwen3-Coder's markup included), `[TOOL_CALLS]`
@@ -53,11 +82,6 @@ export declare function parseToolArguments(raw: unknown, { finishReason }?: {
53
82
  * nothing but a JSON call, or holds one fenced one, provided every name in it is in `names`. Only
54
83
  * text after the last `</think>` is searched, since a model deliberating about a call is not making
55
84
  * one.
56
- *
57
- * @param content The turn's text.
58
- * @param options `names`, the tools that exist. Without them only the templates' markers count.
59
- * @returns The text with the calls taken out, and the calls, numbered `call_recovered_0` onward.
60
- * No calls leaves the text as it was.
61
85
  */
62
86
  export declare function recoverToolCalls(content: string, { names }?: {
63
87
  names?: Iterable<string>;
@@ -65,3 +89,53 @@ export declare function recoverToolCalls(content: string, { names }?: {
65
89
  content: string;
66
90
  toolCalls: ToolCall[];
67
91
  };
92
+ /**
93
+ * One call the model made, as `dispatch` and `onToolCall` are handed it.
94
+ *
95
+ * @remarks
96
+ * A `call_tool` arrives as the tool it names — that tool's name and arguments, under the
97
+ * `call_tool`'s id — so a dispatcher is the same in every discovery mode.
98
+ */
99
+ export interface ToolCallRequest {
100
+ id: string;
101
+ name: string;
102
+ /**
103
+ * Parsed by `parseToolArguments`, repairs and all. Empty for a call whose arguments could not be
104
+ * read, which only `onToolCall` is ever handed.
105
+ */
106
+ args: Record<string, unknown>;
107
+ /**
108
+ * The arguments as the model wrote them, before any repair. Through `call_tool`, the inner
109
+ * arguments as JSON text.
110
+ */
111
+ raw: string;
112
+ }
113
+ /** What one tool call did, in the order the model asked. */
114
+ export interface ToolCallOutcome {
115
+ /**
116
+ * The id the model's reply gave the call, which is what tells two calls to one tool apart.
117
+ * Distinct within a step — `streamTurn` mints one where the server sent none or repeated one —
118
+ * but nothing stops a server using the same id again in a later step. `runAgentLoop` always
119
+ * sets it; optional so an outcome built before it existed still compiles.
120
+ */
121
+ id?: string;
122
+ /** The tool that was called. For a `call_tool`, the tool it named. */
123
+ name: string;
124
+ /** False when the arguments did not parse, the tool threw, or `load_tools` loaded nothing. */
125
+ ok: boolean;
126
+ }
127
+ /**
128
+ * One call's whole answer, as `onToolResult` is handed it.
129
+ *
130
+ * @remarks
131
+ * The `tool-result` event carries the same answer cut by `preview`, which is right for a readout
132
+ * and wrong for a host that renders or stores the result: this is the text the model reads.
133
+ */
134
+ export interface ToolCallResult extends ToolCallOutcome {
135
+ id: string;
136
+ /**
137
+ * What went into the transcript for this call, uncut: what the tool returned, what it threw, the
138
+ * `load_tools` answer, or why the arguments could not be read.
139
+ */
140
+ content: string;
141
+ }