@cubicecho/agent-core 2.24.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.
- package/README.md +10 -7
- package/dist/agent-loop.d.ts +84 -205
- package/dist/agent-loop.js +288 -597
- package/dist/calibration.d.ts +14 -10
- package/dist/calibration.js +30 -23
- package/dist/capabilities.d.ts +111 -42
- package/dist/capabilities.js +281 -138
- package/dist/client.d.ts +104 -35
- package/dist/client.js +205 -74
- package/dist/compaction.d.ts +103 -60
- package/dist/compaction.js +160 -86
- package/dist/config.d.ts +48 -12
- package/dist/config.js +20 -1
- package/dist/continuation.d.ts +24 -16
- package/dist/continuation.js +70 -44
- package/dist/digest.d.ts +7 -0
- package/dist/digest.js +18 -0
- package/dist/errors.d.ts +24 -15
- package/dist/errors.js +23 -15
- package/dist/events.d.ts +100 -63
- package/dist/events.js +250 -120
- package/dist/guards.d.ts +35 -4
- package/dist/guards.js +38 -5
- package/dist/hook-events.d.ts +21 -5
- package/dist/hook-events.js +23 -9
- package/dist/hooks.d.ts +96 -73
- package/dist/hooks.js +202 -116
- package/dist/index.d.ts +30 -26
- package/dist/index.js +29 -24
- package/dist/ledger.d.ts +62 -30
- package/dist/ledger.js +163 -56
- package/dist/platform.d.ts +13 -0
- package/dist/platform.js +13 -0
- package/dist/preselect.d.ts +222 -0
- package/dist/preselect.js +290 -0
- package/dist/request-body.d.ts +32 -0
- package/dist/request-body.js +47 -0
- package/dist/reset.d.ts +1 -0
- package/dist/reset.js +7 -6
- package/dist/retry.d.ts +29 -138
- package/dist/retry.js +69 -282
- package/dist/run-calls.d.ts +123 -0
- package/dist/run-calls.js +351 -0
- package/dist/run-turn.d.ts +36 -15
- package/dist/run-turn.js +74 -37
- package/dist/runtime.d.ts +23 -17
- package/dist/runtime.js +34 -24
- package/dist/schema-compat.d.ts +35 -18
- package/dist/schema-compat.js +340 -175
- package/dist/scope.d.ts +28 -5
- package/dist/scope.js +37 -10
- package/dist/side-task.d.ts +123 -40
- package/dist/side-task.js +181 -63
- package/dist/snapshot.d.ts +10 -5
- package/dist/snapshot.js +73 -30
- package/dist/spec.d.ts +56 -25
- package/dist/spec.js +494 -215
- package/dist/stream.d.ts +66 -16
- package/dist/stream.js +220 -97
- package/dist/thinking.d.ts +23 -8
- package/dist/thinking.js +92 -44
- package/dist/tokens.d.ts +164 -6
- package/dist/tokens.js +319 -6
- package/dist/tool-calls.d.ts +82 -17
- package/dist/tool-calls.js +198 -83
- package/dist/tool-loading.d.ts +101 -189
- package/dist/tool-loading.js +277 -371
- package/dist/wire.d.ts +81 -0
- package/dist/wire.js +73 -0
- package/llms.txt +76 -39
- package/package.json +4 -4
package/dist/tokens.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { FUNCTION_TOOL, PartType, Role } from './wire.js';
|
|
1
2
|
/**
|
|
2
3
|
* The characters one token is taken to be, where nothing has measured it.
|
|
3
4
|
*
|
|
5
|
+
* @remarks
|
|
4
6
|
* The fallback, not the rule: once a turn has come back with a reported prompt count, `runTurn`
|
|
5
7
|
* divides by what that endpoint's model was measured at instead. See `charsPerTokenFor`.
|
|
6
8
|
*/
|
|
@@ -10,16 +12,327 @@ export const CHARS_PER_TOKEN = 4;
|
|
|
10
12
|
* going to be one: a server that will not say how big its window is will not lend us its
|
|
11
13
|
* vocabulary either.
|
|
12
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
|
|
13
20
|
* The estimate runs low on tool schemas — JSON packs more tokens into a character than prose
|
|
14
21
|
* does — and that is the side to be wrong on wherever it guards a window, since the cost of
|
|
15
22
|
* guessing high is a run refused that would have worked, and the cost of guessing low is the
|
|
16
23
|
* endpoint's own refusal, which is where we were before the guard existed.
|
|
17
24
|
*
|
|
18
|
-
* Its own module because it is the one number several of these agree on
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param text Prose or serialised JSON — both counted the same way, which is why JSON reads low.
|
|
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.
|
|
24
30
|
*/
|
|
25
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.
|
|
333
|
+
*
|
|
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.
|
|
337
|
+
*/
|
|
338
|
+
export const messageTokens = (message, { charsPerToken } = {}) => Math.ceil(messageChars(message) / divisor(charsPerToken));
|
package/dist/tool-calls.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type OpenAI from
|
|
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,46 +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:
|
|
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:
|
|
35
|
+
constructor(kind: typeof ARGUMENTS_TRUNCATED | typeof ARGUMENTS_MALFORMED, message: string);
|
|
28
36
|
}
|
|
29
37
|
/**
|
|
30
38
|
* JSON as it was written, then repaired, then undefined. A string holding JSON is opened once.
|
|
31
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
|
|
32
45
|
* Read as written first, so the repair can only ever add to what parses: nothing valid is
|
|
33
46
|
* reinterpreted on its way through.
|
|
34
|
-
*
|
|
35
|
-
* @param text What a model wrote where JSON was asked for, with no prose around it.
|
|
36
47
|
*/
|
|
37
48
|
export declare function looseJson(text: string): unknown;
|
|
38
49
|
/**
|
|
39
50
|
* A tool call's arguments as the object the tool is handed. Empty is no arguments.
|
|
40
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
|
|
41
60
|
* Lenient where the model's meaning is plain and strict where it is not: an object already
|
|
42
61
|
* parsed passes through, JSON inside a string is opened, and the almost-JSON local models write —
|
|
43
62
|
* single quotes, `True`, bare keys, a trailing comma — is repaired. What is still not an object
|
|
44
63
|
* throws a `ToolArgumentsError`, and the loop hands its message back to the model as the tool's
|
|
45
64
|
* result so it can try again.
|
|
46
|
-
*
|
|
47
|
-
* @param raw The arguments as the model sent them: usually the streamed string, sometimes an
|
|
48
|
-
* object a server parsed already. Null, absent or blank is no arguments.
|
|
49
|
-
* @param options `finishReason`, the turn's. A turn that stopped at `"length"` makes a failure
|
|
50
|
-
* `truncated`, since a call cut off at the ceiling reads exactly like a malformed one.
|
|
51
65
|
*/
|
|
52
66
|
export declare function parseToolArguments(raw: unknown, { finishReason }?: {
|
|
53
67
|
finishReason?: string | null;
|
|
@@ -55,6 +69,12 @@ export declare function parseToolArguments(raw: unknown, { finishReason }?: {
|
|
|
55
69
|
/**
|
|
56
70
|
* Tool calls a model wrote into its reply as text, taken out of it and made into calls.
|
|
57
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
|
|
58
78
|
* A server whose tool-call parser does not match the model's chat template streams the call as
|
|
59
79
|
* content, and the run ends on what reads like a finished answer. The templates' own markers are
|
|
60
80
|
* looked for — `<tool_call>` (Hermes, Qwen, Qwen3-Coder's markup included), `[TOOL_CALLS]`
|
|
@@ -62,11 +82,6 @@ export declare function parseToolArguments(raw: unknown, { finishReason }?: {
|
|
|
62
82
|
* nothing but a JSON call, or holds one fenced one, provided every name in it is in `names`. Only
|
|
63
83
|
* text after the last `</think>` is searched, since a model deliberating about a call is not making
|
|
64
84
|
* one.
|
|
65
|
-
*
|
|
66
|
-
* @param content The turn's text.
|
|
67
|
-
* @param options `names`, the tools that exist. Without them only the templates' markers count.
|
|
68
|
-
* @returns The text with the calls taken out, and the calls, numbered `call_recovered_0` onward.
|
|
69
|
-
* No calls leaves the text as it was.
|
|
70
85
|
*/
|
|
71
86
|
export declare function recoverToolCalls(content: string, { names }?: {
|
|
72
87
|
names?: Iterable<string>;
|
|
@@ -74,3 +89,53 @@ export declare function recoverToolCalls(content: string, { names }?: {
|
|
|
74
89
|
content: string;
|
|
75
90
|
toolCalls: ToolCall[];
|
|
76
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
|
+
}
|