@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.
Files changed (71) hide show
  1. package/README.md +10 -7
  2. package/dist/agent-loop.d.ts +84 -205
  3. package/dist/agent-loop.js +288 -597
  4. package/dist/calibration.d.ts +14 -10
  5. package/dist/calibration.js +30 -23
  6. package/dist/capabilities.d.ts +111 -42
  7. package/dist/capabilities.js +281 -138
  8. package/dist/client.d.ts +104 -35
  9. package/dist/client.js +205 -74
  10. package/dist/compaction.d.ts +103 -60
  11. package/dist/compaction.js +160 -86
  12. package/dist/config.d.ts +48 -12
  13. package/dist/config.js +20 -1
  14. package/dist/continuation.d.ts +24 -16
  15. package/dist/continuation.js +70 -44
  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 +100 -63
  21. package/dist/events.js +250 -120
  22. package/dist/guards.d.ts +35 -4
  23. package/dist/guards.js +38 -5
  24. package/dist/hook-events.d.ts +21 -5
  25. package/dist/hook-events.js +23 -9
  26. package/dist/hooks.d.ts +96 -73
  27. package/dist/hooks.js +202 -116
  28. package/dist/index.d.ts +30 -26
  29. package/dist/index.js +29 -24
  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 +29 -138
  41. package/dist/retry.js +69 -282
  42. package/dist/run-calls.d.ts +123 -0
  43. package/dist/run-calls.js +351 -0
  44. package/dist/run-turn.d.ts +36 -15
  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 +35 -18
  49. package/dist/schema-compat.js +340 -175
  50. package/dist/scope.d.ts +28 -5
  51. package/dist/scope.js +37 -10
  52. package/dist/side-task.d.ts +123 -40
  53. package/dist/side-task.js +181 -63
  54. package/dist/snapshot.d.ts +10 -5
  55. package/dist/snapshot.js +73 -30
  56. package/dist/spec.d.ts +56 -25
  57. package/dist/spec.js +494 -215
  58. package/dist/stream.d.ts +66 -16
  59. package/dist/stream.js +220 -97
  60. package/dist/thinking.d.ts +23 -8
  61. package/dist/thinking.js +92 -44
  62. package/dist/tokens.d.ts +164 -6
  63. package/dist/tokens.js +319 -6
  64. package/dist/tool-calls.d.ts +82 -17
  65. package/dist/tool-calls.js +198 -83
  66. package/dist/tool-loading.d.ts +101 -189
  67. package/dist/tool-loading.js +277 -371
  68. package/dist/wire.d.ts +81 -0
  69. package/dist/wire.js +73 -0
  70. package/llms.txt +76 -39
  71. package/package.json +4 -4
@@ -17,6 +17,7 @@ export declare const THINK_FENCE: Fence;
17
17
  /**
18
18
  * The fences nobody writes by accident, and so the ones `streamTurn` reads by default.
19
19
  *
20
+ * @remarks
20
21
  * `<think>` opening a reply is never meant as output, and the other two are made of tokens that
21
22
  * only a model's template produces: gpt-oss's harmony analysis channel, served raw, and Kimi's.
22
23
  */
@@ -27,9 +28,13 @@ export declare const DEFAULT_FENCES: readonly Fence[];
27
28
  * and on for a side task, whose answer is too short to be quoting anything.
28
29
  */
29
30
  export declare const ALL_FENCES: readonly Fence[];
31
+ /** What a `Split` is called when it was inside a fence. */
32
+ export declare const SPLIT_REASONING: 'reasoning';
33
+ /** What a `Split` is called when it was not. */
34
+ export declare const SPLIT_OUTPUT: 'output';
30
35
  /** A piece of `content`, said to be one or the other. */
31
36
  export interface Split {
32
- kind: "reasoning" | "output";
37
+ kind: typeof SPLIT_REASONING | typeof SPLIT_OUTPUT;
33
38
  text: string;
34
39
  }
35
40
  /** What `FenceSplitter` takes besides its fences. */
@@ -37,6 +42,7 @@ export interface FenceSplitterOptions {
37
42
  /**
38
43
  * The template already opened the first fence in the prompt, so the reply starts inside it.
39
44
  *
45
+ * @remarks
40
46
  * Several chat templates end the prompt with `<think>` rather than leaving the model to write
41
47
  * it. Without this the splitter still catches it once `</think>` arrives, and moves what came
42
48
  * before into `reasoning`, but a watcher will have been shown it as output by then.
@@ -46,6 +52,7 @@ export interface FenceSplitterOptions {
46
52
  /**
47
53
  * A state machine over a stream of `content` that routes fenced text to reasoning.
48
54
  *
55
+ * @remarks
49
56
  * The reference shape is Vercel's `extractReasoningMiddleware`. A tag can be split across chunks,
50
57
  * so the tail of each push that could be the start of one is held until the next push settles
51
58
  * it; `finish` releases it. A reply cut off mid-scratchpad ends with the fence still open and its
@@ -62,24 +69,32 @@ export declare class FenceSplitter {
62
69
  /** Everything that was inside a fence so far. */
63
70
  reasoning: string;
64
71
  /**
65
- * @param fences The fences to read, `DEFAULT_FENCES` unless given; an empty list passes
66
- * everything through as output.
67
- * @param options Whether the reply starts inside the first fence.
72
+ * @param [fences] - The fences to read; an empty list passes everything through as output.
73
+ * @param [options] - Whether the reply starts inside the first fence.
68
74
  */
69
75
  constructor(fences?: readonly Fence[], { startInside }?: FenceSplitterOptions);
70
76
  /**
71
77
  * Reads one more piece of content, returning what it settled, in order.
72
78
  *
73
- * @param text The next delta.
79
+ * @param text - The next delta.
80
+ * @returns The pieces now settled, neighbours of one kind merged, and added to `output` and
81
+ * `reasoning` already. Empty where the delta was all markers or all held back as a possible one.
74
82
  */
75
83
  push(text: string): Split[];
76
- /** Releases whatever was held back as a possible tag, now that no more is coming. */
84
+ /**
85
+ * Releases whatever was held back as a possible tag, now that no more is coming.
86
+ *
87
+ * @returns The held text as one piece — reasoning where the reply ended inside a fence, output
88
+ * otherwise — or nothing where none was held.
89
+ */
77
90
  finish(): Split[];
78
91
  }
79
92
  /**
80
93
  * What is left of a complete reply once every scratchpad is taken out of it.
81
94
  *
82
- * @param text The whole reply.
83
- * @param fences The fences to read, every known one unless given.
95
+ * @param text - The whole reply.
96
+ * @param [fences] - The fences to read.
97
+ * @returns The answer alone, untrimmed. A fence left open takes everything after it, and a close
98
+ * with no open before any other fence takes everything before it.
84
99
  */
85
100
  export declare function stripThinking(text: string, fences?: readonly Fence[]): string;
package/dist/thinking.js CHANGED
@@ -8,17 +8,18 @@
8
8
  * keep doing it. This is the one table of fences both the stream and the side tasks read.
9
9
  */
10
10
  /** DeepSeek, Qwen3, QwQ and most distills. */
11
- export const THINK_FENCE = { open: "<think>", close: "</think>" };
11
+ export const THINK_FENCE = { open: '<think>', close: '</think>' };
12
12
  /**
13
13
  * The fences nobody writes by accident, and so the ones `streamTurn` reads by default.
14
14
  *
15
+ * @remarks
15
16
  * `<think>` opening a reply is never meant as output, and the other two are made of tokens that
16
17
  * only a model's template produces: gpt-oss's harmony analysis channel, served raw, and Kimi's.
17
18
  */
18
19
  export const DEFAULT_FENCES = [
19
20
  THINK_FENCE,
20
- { open: "<|channel|>analysis<|message|>", close: "<|end|>" },
21
- { open: "◁think▷", close: "◁/think▷" },
21
+ { open: '<|channel|>analysis<|message|>', close: '<|end|>' },
22
+ { open: '◁think▷', close: '◁/think▷' },
22
23
  ];
23
24
  /**
24
25
  * Every fence known, including the two plain-word ones some fine-tunes and prompt-instructed
@@ -27,25 +28,27 @@ export const DEFAULT_FENCES = [
27
28
  */
28
29
  export const ALL_FENCES = [
29
30
  ...DEFAULT_FENCES,
30
- { open: "<thinking>", close: "</thinking>" },
31
- { open: "<reasoning>", close: "</reasoning>" },
31
+ { open: '<thinking>', close: '</thinking>' },
32
+ { open: '<reasoning>', close: '</reasoning>' },
32
33
  ];
33
34
  /**
34
35
  * Framing that is neither scratchpad nor answer, dropped wherever it turns up outside a fence.
35
36
  *
37
+ * @remarks
36
38
  * Only harmony has any: after the analysis channel closes, raw gpt-oss output announces the final
37
39
  * channel before the answer and ends with a return token.
38
40
  */
39
41
  const FRAMING = {
40
- "<|channel|>analysis<|message|>": [
41
- "<|start|>assistant",
42
- "<|channel|>final<|message|>",
43
- "<|return|>",
44
- ],
42
+ '<|channel|>analysis<|message|>': ['<|start|>assistant', '<|channel|>final<|message|>', '<|return|>'],
45
43
  };
44
+ /** What a `Split` is called when it was inside a fence. */
45
+ export const SPLIT_REASONING = 'reasoning';
46
+ /** What a `Split` is called when it was not. */
47
+ export const SPLIT_OUTPUT = 'output';
46
48
  /**
47
49
  * A state machine over a stream of `content` that routes fenced text to reasoning.
48
50
  *
51
+ * @remarks
49
52
  * The reference shape is Vercel's `extractReasoningMiddleware`. A tag can be split across chunks,
50
53
  * so the tail of each push that could be the start of one is held until the next push settles
51
54
  * it; `finish` releases it. A reply cut off mid-scratchpad ends with the fence still open and its
@@ -57,86 +60,117 @@ const FRAMING = {
57
60
  */
58
61
  export class FenceSplitter {
59
62
  /** The answer so far, with every fence taken out. */
60
- output = "";
63
+ output = '';
61
64
  /** Everything that was inside a fence so far. */
62
- reasoning = "";
65
+ reasoning = '';
63
66
  #fences;
64
67
  #markers;
65
68
  #inside;
66
69
  #seenFence;
67
- #held = "";
70
+ #held = '';
68
71
  /**
69
- * @param fences The fences to read, `DEFAULT_FENCES` unless given; an empty list passes
70
- * everything through as output.
71
- * @param options Whether the reply starts inside the first fence.
72
+ * @param [fences] - The fences to read; an empty list passes everything through as output.
73
+ * @param [options] - Whether the reply starts inside the first fence.
72
74
  */
73
75
  constructor(fences = DEFAULT_FENCES, { startInside } = {}) {
74
76
  this.#fences = fences;
75
- this.#markers = fences.flatMap((fence) => [
76
- fence.open,
77
- fence.close,
78
- ...(FRAMING[fence.open] ?? []),
79
- ]);
77
+ this.#markers = fences.flatMap((fence) => [fence.open, fence.close, ...(FRAMING[fence.open] ?? [])]);
80
78
  this.#inside = startInside ? fences[0] : undefined;
81
79
  this.#seenFence = this.#inside !== undefined;
82
80
  }
83
81
  /**
84
82
  * Reads one more piece of content, returning what it settled, in order.
85
83
  *
86
- * @param text The next delta.
84
+ * @param text - The next delta.
85
+ * @returns The pieces now settled, neighbours of one kind merged, and added to `output` and
86
+ * `reasoning` already. Empty where the delta was all markers or all held back as a possible one.
87
87
  */
88
88
  push(text) {
89
89
  const parts = [];
90
90
  let rest = this.#held + text;
91
- this.#held = "";
91
+ this.#held = '';
92
92
  while (rest) {
93
93
  const found = this.#next(rest);
94
94
  if (!found) {
95
95
  const keep = this.#partialTail(rest);
96
- this.#emit(parts, rest.slice(0, rest.length - keep));
96
+ this.#emitInto(parts, rest.slice(0, rest.length - keep));
97
97
  this.#held = rest.slice(rest.length - keep);
98
98
  break;
99
99
  }
100
- this.#emit(parts, rest.slice(0, found.at));
100
+ this.#emitInto(parts, rest.slice(0, found.at));
101
101
  rest = rest.slice(found.at + found.marker.length);
102
102
  this.#take(found.marker);
103
103
  }
104
104
  return parts;
105
105
  }
106
- /** Releases whatever was held back as a possible tag, now that no more is coming. */
106
+ /**
107
+ * Releases whatever was held back as a possible tag, now that no more is coming.
108
+ *
109
+ * @returns The held text as one piece — reasoning where the reply ended inside a fence, output
110
+ * otherwise — or nothing where none was held.
111
+ */
107
112
  finish() {
108
113
  const parts = [];
109
- this.#emit(parts, this.#held);
110
- this.#held = "";
114
+ this.#emitInto(parts, this.#held);
115
+ this.#held = '';
111
116
  return parts;
112
117
  }
113
- /** The markers worth looking for: only the close inside a fence, every one outside it. */
118
+ /**
119
+ * The markers worth looking for: only the close inside a fence, every one outside it.
120
+ *
121
+ * @returns Outside a fence, the splitter's own list of opens, closes and framing — not a copy.
122
+ */
114
123
  #candidates() {
115
124
  return this.#inside ? [this.#inside.close] : this.#markers;
116
125
  }
117
- /** The earliest marker that means something in the current state. */
126
+ /**
127
+ * The earliest marker that means something in the current state.
128
+ *
129
+ * @param text - What is left of this push to read.
130
+ * @returns The marker and where in `text` it starts — the longer of two that start at one place —
131
+ * or `undefined` where none is whole in it.
132
+ */
118
133
  #next(text) {
119
134
  let best;
120
135
  for (const marker of this.#candidates()) {
121
136
  const at = text.indexOf(marker);
122
- if (at === -1)
137
+ if (at === -1) {
123
138
  continue;
124
- if (!best || at < best.at || (at === best.at && marker.length > best.marker.length))
139
+ }
140
+ const earlier = !best || at < best.at;
141
+ const longerAtTheSamePlace = best && at === best.at && marker.length > best.marker.length;
142
+ if (earlier || longerAtTheSamePlace) {
125
143
  best = { at, marker };
144
+ }
126
145
  }
127
146
  return best;
128
147
  }
129
- /** How much of the end of `text` could be the beginning of a marker. */
148
+ /**
149
+ * How much of the end of `text` could be the beginning of a marker.
150
+ *
151
+ * @param text - Text with no whole marker in it.
152
+ * @returns The length of the longest such tail over every marker worth looking for, zero where
153
+ * there is none. Always short of a whole marker.
154
+ */
130
155
  #partialTail(text) {
131
156
  let keep = 0;
132
- for (const marker of this.#candidates())
133
- for (let length = Math.min(marker.length - 1, text.length); length > keep; length--)
157
+ for (const marker of this.#candidates()) {
158
+ for (let length = Math.min(marker.length - 1, text.length); length > keep; length--) {
134
159
  if (text.endsWith(marker.slice(0, length))) {
135
160
  keep = length;
136
161
  break;
137
162
  }
163
+ }
164
+ }
138
165
  return keep;
139
166
  }
167
+ /**
168
+ * Moves the state past a marker just read: out of a fence, into one, or nowhere.
169
+ *
170
+ * @param marker - The marker found. Inside a fence any marker leaves it, since only its close is
171
+ * looked for there. Outside, an open enters its fence, framing changes nothing, and a close met
172
+ * before any fence was seen moves all of `output` so far into `reasoning`.
173
+ */
140
174
  #take(marker) {
141
175
  if (this.#inside) {
142
176
  this.#inside = undefined;
@@ -153,28 +187,42 @@ export class FenceSplitter {
153
187
  const closes = this.#fences.some((fence) => fence.close === marker);
154
188
  if (closes && !this.#seenFence) {
155
189
  this.reasoning += this.output;
156
- this.output = "";
190
+ this.output = '';
157
191
  }
158
- if (closes)
192
+ if (closes) {
159
193
  this.#seenFence = true;
194
+ }
160
195
  }
161
- #emit(parts, text) {
162
- if (!text)
196
+ /**
197
+ * Files text under the side the state is on: in `output` or `reasoning`, and in the pieces a call
198
+ * is returning.
199
+ *
200
+ * @param parts - The pieces settled so far. Written to — the text joins the last piece where
201
+ * that is of the same kind, and is pushed as a new one where it is not.
202
+ * @param text - What to file. Empty files nothing.
203
+ */
204
+ #emitInto(parts, text) {
205
+ if (!text) {
163
206
  return;
164
- const kind = this.#inside ? "reasoning" : "output";
207
+ }
208
+ const kind = this.#inside ? SPLIT_REASONING : SPLIT_OUTPUT;
165
209
  this[kind] += text;
166
210
  const last = parts.at(-1);
167
- if (last?.kind === kind)
211
+ if (last?.kind === kind) {
168
212
  last.text += text;
169
- else
213
+ }
214
+ else {
170
215
  parts.push({ kind, text });
216
+ }
171
217
  }
172
218
  }
173
219
  /**
174
220
  * What is left of a complete reply once every scratchpad is taken out of it.
175
221
  *
176
- * @param text The whole reply.
177
- * @param fences The fences to read, every known one unless given.
222
+ * @param text - The whole reply.
223
+ * @param [fences] - The fences to read.
224
+ * @returns The answer alone, untrimmed. A fence left open takes everything after it, and a close
225
+ * with no open before any other fence takes everything before it.
178
226
  */
179
227
  export function stripThinking(text, fences = ALL_FENCES) {
180
228
  const splitter = new FenceSplitter(fences);
package/dist/tokens.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import type OpenAI from 'openai';
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,172 @@ export declare 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, and the module that
19
- * owns it should not be one that also does something. It was extracted from `side-task` to
20
- * break a cycle with `retry`; neither reads it now, but `hooks` does to cap a context block, as
21
- * does any consumer sizing its own prompt, and a leaf with no imports is the right home for it.
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 declare const estimateTokens: (text: string) => number;
32
+ /**
33
+ * A token count as a message shows it: 1234 → "1.2k".
34
+ *
35
+ * @param tokens - The count to render.
36
+ * @returns The count in thousands to one decimal place with a `k`, or the number as it is below a
37
+ * thousand.
38
+ *
39
+ * @remarks
40
+ * The numbers in an overflow message are large and nobody reads the units digit.
41
+ */
42
+ export declare const compact: (tokens: number) => string;
43
+ /** What the two token estimates below take besides what they measure. */
44
+ export interface TokenEstimateOptions {
45
+ /**
46
+ * The divisor — `charsPerTokenFor` for a model whose reported usage has calibrated it. A value
47
+ * that is not a number above zero is ignored.
48
+ *
49
+ * @defaultValue `CHARS_PER_TOKEN`
50
+ */
51
+ charsPerToken?: number;
52
+ }
53
+ /**
54
+ * How many characters one message is worth: its keys, and its content in whichever shape.
55
+ *
56
+ * @param message - The message as it will be sent.
57
+ * @returns The characters of its role, its content — a string, or the text and refusal parts of a
58
+ * list — and its name, call id, reasoning and tool calls, plus a constant for each key and part
59
+ * that carries them. An image or audio part adds nothing.
60
+ */
61
+ export declare function messageChars(message: OpenAI.ChatCompletionMessageParam): number;
62
+ /**
63
+ * How many characters a tool array is worth, measured once per array.
64
+ *
65
+ * @param tools - The tool definitions as they will be sent. An empty array is worth nothing.
66
+ * @returns The length of the array serialised as JSON, as it was the first time this array was
67
+ * asked about — a change made to it since is not seen.
68
+ */
69
+ export declare function toolsChars(tools: OpenAI.ChatCompletionTool[]): number;
70
+ /**
71
+ * How many characters a request is worth: the walk `requestTokens` divides, without the division.
72
+ *
73
+ * @param body - The request as it was sent, tools included.
74
+ * @returns The sum of `messageChars` over the messages. The tools are not in it, whether or not
75
+ * the body carries them; `toolsChars` counts those.
76
+ *
77
+ * @remarks
78
+ * What calibration reads a reported prompt count against, since a ratio is only as good as the
79
+ * character count it was taken over agreeing with the one it is later applied to.
80
+ */
81
+ export declare function requestChars(body: OpenAI.ChatCompletionCreateParamsStreaming): number;
82
+ /**
83
+ * What this request will cost the window, in tokens, near enough.
84
+ *
85
+ * @param body - The request as it will be sent, tools included.
86
+ * @param [options] - The divisor, `CHARS_PER_TOKEN` when none is given.
87
+ * @returns Whole tokens: the messages' and the tools', each rounded up on its own and added.
88
+ *
89
+ * @remarks
90
+ * See `estimateTokens` for why it is characters over four and which way it is wrong on purpose,
91
+ * and `charsPerTokenFor` for the divisor a model's own reported usage has measured instead.
92
+ *
93
+ * Summed by walking the body rather than by serialising it. `JSON.stringify` on the messages
94
+ * built the entire transcript into a string on every call and threw it away having read nothing
95
+ * but its `.length` — against a transcript that grows by a turn each turn, and one the SDK is
96
+ * about to serialise again to send. What the walk misses is JSON's own punctuation and the keys,
97
+ * which the envelope constants put back — one per key and one per content part, rather than one
98
+ * per message, since the keys only some shapes carry and the parts a client appends block by
99
+ * block are most of what a tool-using transcript is made of. What is left is a message's
100
+ * escaping, which is not a constant and is small against an estimate that is already characters
101
+ * over four.
102
+ */
103
+ export declare const requestTokens: (body: OpenAI.ChatCompletionCreateParamsStreaming, { charsPerToken }?: TokenEstimateOptions) => number;
104
+ /**
105
+ * What a request is made of, by the part of it a consumer can actually do something about.
106
+ *
107
+ * @remarks
108
+ * The question an operator asks is not how big the request is — the total already answers that —
109
+ * but what is filling the window, and the only useful answer names a lever: a system prompt to
110
+ * shorten, a tool list to load on demand instead of declaring whole, a transcript to compact,
111
+ * results to prune. So the cut follows the levers rather than the roles: `toolResults` is exactly
112
+ * what `pruneToolResults` can shrink, and `history` is everything `planCompaction` folds, the
113
+ * arguments of the calls in it included.
114
+ */
115
+ export interface ContextBreakdown {
116
+ /** Every system and developer message, wherever it sits in the transcript. */
117
+ system: number;
118
+ /** The declared tool schemas, which a chat template renders ahead of the system prompt. */
119
+ tools: number;
120
+ /** What was said: the user and assistant messages, and the calls the assistant asked for. */
121
+ history: number;
122
+ /** What the tools handed back — the `tool` messages, and nothing else. */
123
+ toolResults: number;
124
+ /** The four above, summed. */
125
+ total: number;
126
+ }
127
+ /**
128
+ * What each part of a request is worth in characters, by the same walk `requestTokens` divides.
129
+ *
130
+ * @param body - The request as it will be sent, tools included.
131
+ * @returns A new breakdown in characters. A `developer` message counts as `system`, and anything
132
+ * that is neither that nor a `tool` message as `history`.
133
+ *
134
+ * @remarks
135
+ * Exact and additive: the parts sum to `total`, which is `requestChars` plus `toolsChars`. The
136
+ * conversion to tokens is `contextTokens`' business, because that is where an estimate and a
137
+ * reported count have to be told apart.
138
+ */
139
+ export declare function contextChars(body: OpenAI.ChatCompletionCreateParamsStreaming): ContextBreakdown;
140
+ /** What `contextTokens` takes besides the request. */
141
+ export interface ContextBreakdownOptions extends TokenEstimateOptions {
142
+ /**
143
+ * The prompt count the endpoint reported for this request, if it has answered. Given one, the
144
+ * parts are shares of it and the breakdown sums to what was actually charged rather than to an
145
+ * estimate; left out, they are shares of `requestTokens`.
146
+ */
147
+ promptTokens?: number;
148
+ }
149
+ /**
150
+ * What each part of a request costs the window, in tokens, adding up to the whole.
151
+ *
152
+ * @param body - The request as it will be sent, tools included.
153
+ * @param [options] - The divisor, and the reported prompt count when there is one.
154
+ * @returns A new breakdown in tokens. Its `total` is the reported count when one above zero was
155
+ * given, and `requestTokens` of the same body otherwise.
156
+ *
157
+ * @remarks
158
+ * Shares rather than four independent estimates, because a readout whose parts do not add up to
159
+ * the total beside them is a readout nobody trusts. Nothing in the round trip reports anything
160
+ * finer than a prompt count — a completion says how many tokens it read and not a word about
161
+ * where they came from — so the proportions are an estimate whatever the total is.
162
+ *
163
+ * Without a reported count the total is `requestTokens`, and the tools are counted the way it and
164
+ * `TurnMetrics.toolSchemaTokens` count them rather than shared out, so the two agree by
165
+ * construction and an operator does not read one number for the tool block in the metrics and a
166
+ * different one here. With a reported count every part is a share of it, the tools included:
167
+ * that number is the server's, and the point of using it is that the parts sum to what was
168
+ * charged.
169
+ */
170
+ export declare function contextTokens(body: OpenAI.ChatCompletionCreateParamsStreaming, { charsPerToken, promptTokens }?: ContextBreakdownOptions): ContextBreakdown;
171
+ /**
172
+ * One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
173
+ *
174
+ * @param message - The message as it will be sent.
175
+ * @param [options] - The divisor, `CHARS_PER_TOKEN` when none is given.
176
+ * @returns `messageChars` over the divisor, rounded up per message, where `requestTokens` rounds
177
+ * once over them all.
178
+ *
179
+ * @remarks
180
+ * For the arithmetic that weighs part of a transcript against a window — `planCompaction`'s kept
181
+ * tail — where `estimateTokens` on the text alone would leave out the calls and the envelope.
182
+ */
183
+ export declare const messageTokens: (message: OpenAI.ChatCompletionMessageParam, { charsPerToken }?: TokenEstimateOptions) => number;