@cubicecho/agent-core 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/side-task.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import OpenAI from "openai";
2
- import { capabilitiesFor, negotiate } from "./capabilities.js";
3
- import { endpointKey, getClient } from "./client.js";
2
+ import { capabilitiesFor, modelCapabilitiesFor, negotiate, } from "./capabilities.js";
3
+ import { endpointId, getClient } from "./client.js";
4
4
  import { errorMessage } from "./errors.js";
5
5
  import { isTransient } from "./retry.js";
6
+ import { relaxTools, sanitizeTools } from "./schema-compat.js";
6
7
  /**
7
8
  * One-shot calls that support a run without being one: picking tools, naming a session,
8
9
  * summarising a transcript, proposing follow-ups. They share a shape — small prompt, short
@@ -25,7 +26,7 @@ const NO_THINKING = { chat_template_kwargs: { enable_thinking: false } };
25
26
  * The models that turned out not to take the hints, by endpoint and model.
26
27
  *
27
28
  * Keyed rather than global for the reason the client cache is keyed, and on the same
28
- * `endpointKey` it is: a refusal is a fact about what is on the other end, not about this
29
+ * endpoint it is, by `endpointId`: a refusal is a fact about what is on the other end, not about this
29
30
  * process. A llama.cpp box and a cloud API are both reachable from one consumer over its
30
31
  * lifetime, and the first one's refusal must not stop the second from ever being asked.
31
32
  *
@@ -38,7 +39,10 @@ const NO_THINKING = { chat_template_kwargs: { enable_thinking: false } };
38
39
  * the same (endpoint, model) pair, where a run on that model can read it too.
39
40
  */
40
41
  const noHints = new Set();
41
- const hintKey = (config, model) => JSON.stringify([endpointKey(config), model]);
42
+ /** An (endpoint, model) pair as `noHints` holds it: `[endpointId, model]`, stringified. */
43
+ export const hintKey = (endpoint, model) => JSON.stringify([endpoint, model]);
44
+ /** The pairs that refused the hints, the live set, for `exportCapabilities` and `importCapabilities`. */
45
+ export const refusedHints = () => noHints;
42
46
  /** Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints. */
43
47
  export const resetHints = () => noHints.clear();
44
48
  /**
@@ -86,48 +90,73 @@ const stripThinking = (text) => text
86
90
  * @param user The input it applies to.
87
91
  * @param options Reply ceiling, temperature, cancellation, notices.
88
92
  */
89
- export async function ask(config, model, system, user, { maxTokens = 512, temperature = 0.3, signal, onNotice } = {}) {
90
- const send = (hints, refused) => getClient(config).chat.completions.create({
91
- model,
92
- // The reasoning models want the ceiling spelled the other way, and they are exactly the
93
- // models a side task most wants to stop deliberating.
94
- ...(refused && !refused.legacyTokenLimit
95
- ? { max_completion_tokens: maxTokens }
96
- : { max_tokens: maxTokens }),
97
- // One that will only run at the temperature it was built with is sent none: a side task
98
- // wants the same answer twice, and 1.0 from that model is as close as it gets.
99
- ...(refused && !refused.chosenTemperature ? {} : { temperature }),
100
- messages: [
101
- { role: "system", content: system },
102
- { role: "user", content: user },
103
- ],
104
- ...(hints ? NO_THINKING : {}),
105
- ...(hints && refused?.reasoningEffort !== false ? { reasoning_effort: "none" } : {}),
106
- }, { signal });
93
+ export function ask(config, model, system, user, options = {}) {
94
+ return complete(config, model, system, user, options);
95
+ }
96
+ /**
97
+ * The request `ask` and `askJson` share, with `format` deciding the extra body fields from what
98
+ * the model and the endpoint have refused, rebuilt on every re-send.
99
+ */
100
+ async function complete(config, model, system, user, { maxTokens = 512, temperature = 0.3, signal, onNotice }, format) {
101
+ // Whether the last request carried an effort, which `negotiate` decides and not this function.
102
+ let sentEffort = false;
103
+ const send = (hints, effort, supports, refused) => {
104
+ sentEffort = effort && refused?.reasoningEffort !== false;
105
+ return getClient(config).chat.completions.create({
106
+ model,
107
+ // The reasoning models want the ceiling spelled the other way, and they are exactly the
108
+ // models a side task most wants to stop deliberating.
109
+ ...(refused && !refused.legacyTokenLimit
110
+ ? { max_completion_tokens: maxTokens }
111
+ : { max_tokens: maxTokens }),
112
+ // One that will only run at the temperature it was built with is sent none: a side task
113
+ // wants the same answer twice, and 1.0 from that model is as close as it gets.
114
+ ...(refused && !refused.chosenTemperature ? {} : { temperature }),
115
+ messages: [
116
+ { role: "system", content: system },
117
+ { role: "user", content: user },
118
+ ],
119
+ ...(hints ? NO_THINKING : {}),
120
+ // Not gated on `hints`: a model that refuses `chat_template_kwargs` may still read the
121
+ // effort, and the two latches would otherwise contradict each other.
122
+ ...(sentEffort ? { reasoning_effort: "none" } : {}),
123
+ ...(format && refused ? format(supports, refused) : {}),
124
+ }, { signal });
125
+ };
107
126
  // The endpoint's own object, not one of this module's: what a model refuses is the same fact
108
127
  // whether a run or a side task found it out, and the point of latching it is that only one of
109
- // them has to pay for it. Nothing here sends tools or `stream_options`, so the two
110
- // endpoint-level flags are not in play the model's three are the whole of what this meets.
128
+ // them has to pay for it. Nothing here sends tools or `stream_options`; the grammar flag is in
129
+ // play only for `askJson`, whose schema a llama.cpp server compiles the way it does a tool's.
111
130
  const supports = capabilitiesFor(config.baseUrl, config.apiKey);
112
- const attempt = (hints) => negotiate(supports, (_supports, _produced, refused) => send(hints, refused), {
131
+ const attempt = (hints, effort) => negotiate(supports, (latched, _produced, refused) => send(hints, effort, latched, refused), {
113
132
  model,
114
133
  onNotice,
115
134
  });
116
- const key = hintKey(config, model);
135
+ const key = hintKey(endpointId(config), model);
117
136
  const hints = !noHints.has(key);
118
137
  let response;
119
138
  try {
120
- response = await attempt(hints);
139
+ response = await attempt(hints, true);
121
140
  }
122
141
  catch (error) {
123
142
  // Whatever is left after `negotiate` has answered everything it knows: on this path that is
124
143
  // the hints it does not, which is `chat_template_kwargs` and an effort the model has but
125
- // does not offer as `none`.
126
- if (!hints || !rejectedTheRequest(error))
144
+ // does not offer as `none`. Or a 400 about something else entirely, which is why the
145
+ // notice says what was tried rather than what was wrong.
146
+ const effort = sentEffort;
147
+ if (!(hints || effort) || !rejectedTheRequest(error))
127
148
  throw error;
128
- onNotice?.(`${model} rejected the no-thinking hints; retrying without them`);
129
- noHints.add(key);
130
- response = await attempt(false);
149
+ onNotice?.(`${model} rejected a request carrying the no-thinking hints; retrying without them`);
150
+ response = await attempt(false, false);
151
+ // Latched on the finding, not the hypothesis: a context overflow is a 400 `negotiate` does
152
+ // not recognise too, and it fails the retry the same way, leaving the next call to try the
153
+ // hints again. When both went out the refusal cannot say which, so the one `negotiate`
154
+ // cannot latch is blamed; if it was the effort after all, the next call is left with only
155
+ // the effort to drop and latches that instead.
156
+ if (hints)
157
+ noHints.add(key);
158
+ else
159
+ modelCapabilitiesFor(supports, model).reasoningEffort = false;
131
160
  }
132
161
  const message = response.choices[0]?.message;
133
162
  const answer = stripThinking(message?.content ?? "").trim();
@@ -138,6 +167,46 @@ export async function ask(config, model, system, user, { maxTokens = 512, temper
138
167
  const reasoning = message?.reasoning_content;
139
168
  return typeof reasoning === "string" ? stripThinking(reasoning).trim() : "";
140
169
  }
170
+ /**
171
+ * A side task whose answer is JSON matching a schema, parsed. Undefined when no JSON came back.
172
+ * Throws like any request.
173
+ *
174
+ * Sends `response_format` with the schema, which a llama.cpp server compiles into a grammar and
175
+ * vLLM, LM Studio, Ollama and OpenAI each hold the reply to, so a small model that wraps JSON in
176
+ * prose cannot. The schema is normalised as a tool's parameters are, and relaxed where the endpoint
177
+ * could not build a grammar, since the same converter reads both. A model that refuses the field
178
+ * latches it off and is asked in words: the schema rides on the system prompt either way, and
179
+ * the reply goes through `parseJson`, which finds the JSON in whatever came back.
180
+ *
181
+ * @param config Where to send it and how long to wait.
182
+ * @param model The model to ask.
183
+ * @param system The instruction. The schema is appended to it.
184
+ * @param user The input it applies to.
185
+ * @param schema The JSON Schema of the answer. Its root is held to an object, as a tool's is.
186
+ * @param options A side task's options, plus the schema's `name` and whether it is `strict`.
187
+ */
188
+ export async function askJson(config, model, system, user, schema, { name = "answer", strict = true, ...options } = {}) {
189
+ const tool = (parameters) => ({
190
+ type: "function",
191
+ function: { name, parameters },
192
+ });
193
+ const [sanitized] = sanitizeTools([tool(schema)]);
194
+ const shapeOf = (definition) => definition?.type === "function" ? (definition.function.parameters ?? {}) : {};
195
+ const instruction = `${system}\n\nReply with JSON alone, matching this JSON Schema:\n${JSON.stringify(shapeOf(sanitized))}`;
196
+ const reply = await complete(config, model, instruction, user, options, (supports, refused) => refused.structuredOutput
197
+ ? {
198
+ response_format: {
199
+ type: "json_schema",
200
+ json_schema: {
201
+ name,
202
+ strict,
203
+ schema: shapeOf(supports.strictSchemas ? sanitized : relaxTools([sanitized])[0]),
204
+ },
205
+ },
206
+ }
207
+ : {});
208
+ return parseJson(reply);
209
+ }
141
210
  /**
142
211
  * A side task is never worth failing the work it supports. Callers that can carry on without
143
212
  * an answer use this and get `undefined` instead of an exception.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What endpoints and models refused, carried across a restart.
3
+ *
4
+ * Every latch here dies with the process, so each restart of a consumer spends one refused request
5
+ * per endpoint and model learning the same facts again, with a notice each time — and on a local
6
+ * reasoning model the first side task after boot is sent the hints, refused and sent again. The
7
+ * package cannot know where a consumer keeps state, so it hands over a blob and takes one back.
8
+ */
9
+ /** The version `importCapabilities` accepts. Raised when what is latched changes shape. */
10
+ export declare const CAPABILITY_SNAPSHOT_VERSION = 1;
11
+ /** What one model on an endpoint refused. `true` is not refused, as on `ModelCapabilities`. */
12
+ export interface ModelSnapshot {
13
+ reasoningEffort: boolean;
14
+ legacyTokenLimit: boolean;
15
+ chosenTemperature: boolean;
16
+ refusedFields: string[];
17
+ structuredOutput: boolean;
18
+ /** Takes the no-thinking hints `ask` sends. */
19
+ thinkingHints: boolean;
20
+ }
21
+ /** What one endpoint refused, and under it what each of its models did. */
22
+ export interface EndpointSnapshot {
23
+ strictSchemas: boolean;
24
+ usageInStream: boolean;
25
+ models: Record<string, ModelSnapshot>;
26
+ }
27
+ /** Every latched refusal in the process, JSON-safe. See `exportCapabilities`. */
28
+ export interface CapabilitySnapshot {
29
+ version: number;
30
+ /** When it was taken, as an ISO string, for the consumer to judge how stale is too stale. */
31
+ savedAt: string;
32
+ /** By `endpointId`: the endpoint's URL and key hashed together, so no key is in the blob. */
33
+ endpoints: Record<string, EndpointSnapshot>;
34
+ }
35
+ /**
36
+ * Every refusal this process has latched, as a JSON-safe blob to store and hand back on boot.
37
+ *
38
+ * Covers what `negotiate` latches on endpoints and models and the models `ask` found refusing the
39
+ * no-thinking hints. Only what was actually refused is in it, so a snapshot of a process that met
40
+ * no refusals has no endpoints. Endpoints are named by digest rather than URL and key, since the
41
+ * blob is meant to be written somewhere and a key must not be written with it.
42
+ */
43
+ export declare function exportCapabilities(): CapabilitySnapshot;
44
+ /**
45
+ * Latches what a stored snapshot says was refused, on top of whatever this process has learned.
46
+ *
47
+ * Refusals only ever latch off, so importing merges rather than replaces: a flag already off stays
48
+ * off whatever the snapshot says, and one the snapshot has off is turned off. A snapshot of another
49
+ * version, or anything that is not one, is ignored — a stale shape costs the refused requests it
50
+ * would have saved, which is what a restart cost before. How old is too old is the consumer's call,
51
+ * made on `savedAt` before importing, since a server behind a URL can be upgraded between boots.
52
+ *
53
+ * @param snapshot What `exportCapabilities` returned, as stored. Read defensively: a field of the
54
+ * wrong type is skipped rather than trusted.
55
+ * @returns Whether the snapshot was of this version and applied.
56
+ */
57
+ export declare function importCapabilities(snapshot: unknown): boolean;
@@ -0,0 +1,123 @@
1
+ import { capabilitiesById, knownCapabilities, modelCapabilitiesFor } from "./capabilities.js";
2
+ import { hintKey, refusedHints } from "./side-task.js";
3
+ /**
4
+ * What endpoints and models refused, carried across a restart.
5
+ *
6
+ * Every latch here dies with the process, so each restart of a consumer spends one refused request
7
+ * per endpoint and model learning the same facts again, with a notice each time — and on a local
8
+ * reasoning model the first side task after boot is sent the hints, refused and sent again. The
9
+ * package cannot know where a consumer keeps state, so it hands over a blob and takes one back.
10
+ */
11
+ /** The version `importCapabilities` accepts. Raised when what is latched changes shape. */
12
+ export const CAPABILITY_SNAPSHOT_VERSION = 1;
13
+ const optimisticModel = () => ({
14
+ reasoningEffort: true,
15
+ legacyTokenLimit: true,
16
+ chosenTemperature: true,
17
+ refusedFields: [],
18
+ structuredOutput: true,
19
+ thinkingHints: true,
20
+ });
21
+ const refusedAnything = (model) => !model.reasoningEffort ||
22
+ !model.legacyTokenLimit ||
23
+ !model.chosenTemperature ||
24
+ !model.thinkingHints ||
25
+ !model.structuredOutput ||
26
+ model.refusedFields.length > 0;
27
+ /**
28
+ * Every refusal this process has latched, as a JSON-safe blob to store and hand back on boot.
29
+ *
30
+ * Covers what `negotiate` latches on endpoints and models and the models `ask` found refusing the
31
+ * no-thinking hints. Only what was actually refused is in it, so a snapshot of a process that met
32
+ * no refusals has no endpoints. Endpoints are named by digest rather than URL and key, since the
33
+ * blob is meant to be written somewhere and a key must not be written with it.
34
+ */
35
+ export function exportCapabilities() {
36
+ const endpoints = {};
37
+ const entry = (id) => {
38
+ endpoints[id] ??= { strictSchemas: true, usageInStream: true, models: {} };
39
+ return endpoints[id];
40
+ };
41
+ for (const [id, supports] of knownCapabilities()) {
42
+ const models = {};
43
+ for (const [name, refused] of supports.models) {
44
+ const model = {
45
+ ...optimisticModel(),
46
+ reasoningEffort: refused.reasoningEffort,
47
+ legacyTokenLimit: refused.legacyTokenLimit,
48
+ chosenTemperature: refused.chosenTemperature,
49
+ refusedFields: [...refused.refusedFields].sort(),
50
+ structuredOutput: refused.structuredOutput,
51
+ };
52
+ if (refusedAnything(model))
53
+ models[name] = model;
54
+ }
55
+ if (!supports.strictSchemas || !supports.usageInStream || Object.keys(models).length) {
56
+ Object.assign(entry(id), {
57
+ strictSchemas: supports.strictSchemas,
58
+ usageInStream: supports.usageInStream,
59
+ models,
60
+ });
61
+ }
62
+ }
63
+ for (const key of refusedHints()) {
64
+ const [id, name] = JSON.parse(key);
65
+ const models = entry(id).models;
66
+ models[name] ??= optimisticModel();
67
+ models[name].thinkingHints = false;
68
+ }
69
+ return { version: CAPABILITY_SNAPSHOT_VERSION, savedAt: new Date().toISOString(), endpoints };
70
+ }
71
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
72
+ /**
73
+ * Latches what a stored snapshot says was refused, on top of whatever this process has learned.
74
+ *
75
+ * Refusals only ever latch off, so importing merges rather than replaces: a flag already off stays
76
+ * off whatever the snapshot says, and one the snapshot has off is turned off. A snapshot of another
77
+ * version, or anything that is not one, is ignored — a stale shape costs the refused requests it
78
+ * would have saved, which is what a restart cost before. How old is too old is the consumer's call,
79
+ * made on `savedAt` before importing, since a server behind a URL can be upgraded between boots.
80
+ *
81
+ * @param snapshot What `exportCapabilities` returned, as stored. Read defensively: a field of the
82
+ * wrong type is skipped rather than trusted.
83
+ * @returns Whether the snapshot was of this version and applied.
84
+ */
85
+ export function importCapabilities(snapshot) {
86
+ if (!isRecord(snapshot) || snapshot.version !== CAPABILITY_SNAPSHOT_VERSION)
87
+ return false;
88
+ if (!isRecord(snapshot.endpoints))
89
+ return false;
90
+ for (const [id, endpoint] of Object.entries(snapshot.endpoints)) {
91
+ if (!isRecord(endpoint))
92
+ continue;
93
+ const supports = capabilitiesById(id);
94
+ if (endpoint.strictSchemas === false)
95
+ supports.strictSchemas = false;
96
+ if (endpoint.usageInStream === false)
97
+ supports.usageInStream = false;
98
+ if (!isRecord(endpoint.models))
99
+ continue;
100
+ for (const [name, model] of Object.entries(endpoint.models)) {
101
+ if (!isRecord(model))
102
+ continue;
103
+ const refused = modelCapabilitiesFor(supports, name);
104
+ if (model.reasoningEffort === false)
105
+ refused.reasoningEffort = false;
106
+ if (model.legacyTokenLimit === false)
107
+ refused.legacyTokenLimit = false;
108
+ if (model.chosenTemperature === false)
109
+ refused.chosenTemperature = false;
110
+ if (model.structuredOutput === false)
111
+ refused.structuredOutput = false;
112
+ if (Array.isArray(model.refusedFields)) {
113
+ for (const field of model.refusedFields) {
114
+ if (typeof field === "string")
115
+ refused.refusedFields.add(field);
116
+ }
117
+ }
118
+ if (model.thinkingHints === false)
119
+ refusedHints().add(hintKey(id, name));
120
+ }
121
+ }
122
+ return true;
123
+ }
package/dist/stream.d.ts CHANGED
@@ -14,6 +14,14 @@ export interface TurnUsage {
14
14
  prompt: number;
15
15
  completion: number;
16
16
  total: number;
17
+ /**
18
+ * How much of `prompt` came from the endpoint's prompt cache — a part of it, not in addition.
19
+ *
20
+ * The only way a caller can tell whether the prefix it is careful to keep still is actually
21
+ * being reused: a prefix that stops hitting the cache otherwise shows up as a bill and nothing
22
+ * else. Zero is also what a server that does not report it sends.
23
+ */
24
+ cached: number;
17
25
  }
18
26
  /** One streamed turn, put back together into the shape a loop and a transcript work with. */
19
27
  export interface Turn {
package/dist/stream.js CHANGED
@@ -50,7 +50,7 @@ export async function streamTurn(client, body, { signal, idleMs, produced, onThi
50
50
  const stream = await client.chat.completions.create(body, { signal: linked });
51
51
  const content = [];
52
52
  const calls = new Map();
53
- const usage = { prompt: 0, completion: 0, total: 0 };
53
+ const usage = { prompt: 0, completion: 0, total: 0, cached: 0 };
54
54
  let finishReason = "";
55
55
  for await (const chunk of stream) {
56
56
  // Rearmed on every chunk, latched below on only some: a priming chunk is the endpoint
@@ -64,6 +64,9 @@ export async function streamTurn(client, body, { signal, idleMs, produced, onThi
64
64
  usage.prompt = chunk.usage.prompt_tokens ?? 0;
65
65
  usage.completion = chunk.usage.completion_tokens ?? 0;
66
66
  usage.total = chunk.usage.total_tokens ?? 0;
67
+ const reported = chunk.usage;
68
+ usage.cached =
69
+ reported.prompt_tokens_details?.cached_tokens ?? reported.prompt_cache_hit_tokens ?? 0;
67
70
  }
68
71
  // One choice, because that is what an agent loop asks for. A body with `n` above one
69
72
  // keeps only the first; nothing here is built to reassemble several at once.
@@ -0,0 +1,67 @@
1
+ import type OpenAI from "openai";
2
+ /**
3
+ * Reading what a model meant by a tool call when it did not write one cleanly.
4
+ *
5
+ * Local models get tool calls wrong in two ways a hosted one rarely does. The arguments come back
6
+ * almost-JSON — single quotes, Python's `True`, a trailing comma, a string holding the JSON
7
+ * rather than the JSON — or cut off at the ceiling. And the call comes back not as a call at all
8
+ * but as text, in the model's own template, because the server's tool-call parser was written for
9
+ * a different one. Both were being handled in every consumer, differently, or not at all.
10
+ */
11
+ /** A tool call as the loop handles it, recovered or streamed. */
12
+ export type ToolCall = OpenAI.ChatCompletionMessageFunctionToolCall;
13
+ /**
14
+ * Tool arguments that could not be read as an object, with why.
15
+ *
16
+ * `truncated` is a call cut off at the reply ceiling, which no repair can finish and the fix for
17
+ * is a larger `maxTokens`; `malformed` is one the model wrote wrongly, which it can be told about
18
+ * and try again.
19
+ */
20
+ export declare class ToolArgumentsError extends Error {
21
+ /** Whether the model ran out of room or wrote something unreadable. */
22
+ readonly kind: "truncated" | "malformed";
23
+ /**
24
+ * @param kind Why the arguments could not be read.
25
+ * @param message What the model is handed back as the tool's result.
26
+ */
27
+ constructor(kind: "truncated" | "malformed", message: string);
28
+ }
29
+ /**
30
+ * A tool call's arguments as the object the tool is handed. Empty is no arguments.
31
+ *
32
+ * Lenient where the model's meaning is plain and strict where it is not: an object already
33
+ * parsed passes through, JSON inside a string is opened, and the almost-JSON local models write —
34
+ * single quotes, `True`, bare keys, a trailing comma — is repaired. What is still not an object
35
+ * throws a `ToolArgumentsError`, and the loop hands its message back to the model as the tool's
36
+ * 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
+ */
43
+ export declare function parseToolArguments(raw: unknown, { finishReason }?: {
44
+ finishReason?: string | null;
45
+ }): Record<string, unknown>;
46
+ /**
47
+ * Tool calls a model wrote into its reply as text, taken out of it and made into calls.
48
+ *
49
+ * A server whose tool-call parser does not match the model's chat template streams the call as
50
+ * content, and the run ends on what reads like a finished answer. The templates' own markers are
51
+ * looked for — `<tool_call>` (Hermes, Qwen, Qwen3-Coder's markup included), `[TOOL_CALLS]`
52
+ * (Mistral, both spellings) and `<|python_tag|>` (Llama 3) — and, failing those, a reply that is
53
+ * nothing but a JSON call, or holds one fenced one, provided every name in it is in `names`. Only
54
+ * text after the last `</think>` is searched, since a model deliberating about a call is not making
55
+ * 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
+ */
62
+ export declare function recoverToolCalls(content: string, { names }?: {
63
+ names?: Iterable<string>;
64
+ }): {
65
+ content: string;
66
+ toolCalls: ToolCall[];
67
+ };