@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/side-task.js CHANGED
@@ -1,11 +1,13 @@
1
- import OpenAI from "openai";
2
- import { capabilitiesFor, effortFor, modelCapabilitiesFor, negotiate, } from "./capabilities.js";
3
- import { endpointId, getClient } from "./client.js";
4
- import { errorMessage } from "./errors.js";
5
- import { isTransient } from "./retry.js";
6
- import { relaxTools, sanitizeTools } from "./schema-compat.js";
7
- import { scoped } from "./scope.js";
8
- import { stripThinking } from "./thinking.js";
1
+ import OpenAI from 'openai';
2
+ import { capabilitiesFor, ceilingAndTemperature, EFFORT_NONE, effortFor, modelCapabilitiesFor, negotiate, } from './capabilities.js';
3
+ import { endpointId, getClient, modelKey, resolveApiKey, sameUrl } from './client.js';
4
+ import { errorMessage } from './errors.js';
5
+ import { refusesRequest } from './retry.js';
6
+ import { relaxSchema, sanitizeSchema } from './schema-compat.js';
7
+ import { scoped } from './scope.js';
8
+ import { stripThinking } from './thinking.js';
9
+ import { looseJson } from './tool-calls.js';
10
+ import { JSON_SCHEMA_FORMAT, Role } from './wire.js';
9
11
  /**
10
12
  * One-shot calls that support a run without being one: picking tools, naming a session,
11
13
  * summarising a transcript, proposing follow-ups. They share a shape — small prompt, short
@@ -18,6 +20,7 @@ import { stripThinking } from "./thinking.js";
18
20
  * disagree about which they take, so send both. One that rejects the unknown fields gets a
19
21
  * single retry without them, and is not offered them again.
20
22
  *
23
+ * @remarks
21
24
  * Only the second half is latched here. `reasoning_effort` is a field `negotiate` already knows
22
25
  * how to be refused, so it is sent under `ModelCapabilities.reasoningEffort` instead — which
23
26
  * both narrows the fallback below to the field it is really about, and shares the answer with
@@ -27,6 +30,7 @@ const NO_THINKING = { chat_template_kwargs: { enable_thinking: false } };
27
30
  /**
28
31
  * The models that turned out not to take the hints, by endpoint and model.
29
32
  *
33
+ * @remarks
30
34
  * Keyed rather than global for the reason the client cache is keyed, and on the same
31
35
  * endpoint it is, by `endpointId`: a refusal is a fact about what is on the other end, not about this
32
36
  * process. A llama.cpp box and a cloud API are both reachable from one consumer over its
@@ -38,42 +42,36 @@ const NO_THINKING = { chat_template_kwargs: { enable_thinking: false } };
38
42
  * Keyed on the host alone, the first model to refuse spoke for every model on it.
39
43
  *
40
44
  * Only the `chat_template_kwargs` half is here. `reasoning_effort` is `negotiate`'s to latch, on
41
- * the same (endpoint, model) pair, where a run on that model can read it too.
45
+ * the same (endpoint, model) pair, where a run on that model can read it too. A pair is held as
46
+ * `modelKey(endpointId, model)`, which is what `importCapabilities` adds to it by.
42
47
  */
43
48
  const noHints = scoped(() => new Set());
44
- /** An (endpoint, model) pair as `noHints` holds it: `[endpointId, model]`, stringified. */
45
- export const hintKey = (endpoint, model) => JSON.stringify([endpoint, model]);
46
- /** The pairs that refused the hints, the live set, for `exportCapabilities` and `importCapabilities`. */
47
- export const refusedHints = () => noHints();
48
- /** Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints. */
49
- export const resetHints = () => noHints().clear();
50
49
  /**
51
- * Whether a failure is the server complaining about the request, rather than failing to answer.
50
+ * The pairs that refused the hints, the live set, for `exportCapabilities` and
51
+ * `importCapabilities`.
52
52
  *
53
- * The retry below used to catch everything, so an aborted first call — or a connection that
54
- * never landed — latched the hints off for the life of the process and every later side task
55
- * paid for it by burning a whole budget on deliberation. Narrowing that to any 4xx was still
56
- * too wide: 401, 404 and 429 are all 4xx and none of them is about the fields. A 429 was the
57
- * worst of them, because the retry then re-sent the whole request immediately — doubling the
58
- * rate against a server that had just asked for less of it — and `isTransient` accepts exactly
59
- * that status, so the two halves of this package disagreed about one error.
53
+ * @returns The current scope's set itself and not a copy, each pair a
54
+ * `modelKey(endpointId, model)`. Adding to it stops the hints being sent to that pair from the
55
+ * next call.
56
+ */
57
+ export const refusedHints = () => noHints();
58
+ /**
59
+ * Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints.
60
60
  *
61
- * 400 and 422 are what a server says when it read the body and disliked it. Everything else
62
- * is left to the caller's own retry.
61
+ * @remarks
62
+ * The current scope's set is emptied in place, so a holder of it sees it empty.
63
63
  */
64
- function rejectedTheRequest(error) {
65
- if (!(error instanceof OpenAI.APIError) || isTransient(error))
66
- return false;
67
- return error.status === 400 || error.status === 422;
68
- }
64
+ export const resetHints = () => noHints().clear();
69
65
  /**
70
66
  * Runs a side task and returns the reply text, thinking stripped. Throws like any request.
71
67
  *
72
- * @param config Where to send it and how long to wait.
73
- * @param model The model to ask, usually smaller than the one running the work.
74
- * @param system The instruction.
75
- * @param user The input it applies to. Content parts where the model is being shown an image.
76
- * @param options Reply ceiling, temperature, a reasoning effort, cancellation, notices.
68
+ * @param config - Where to send it and how long to wait.
69
+ * @param model - The model to ask, usually smaller than the one running the work.
70
+ * @param system - The instruction.
71
+ * @param user - The input it applies to. Content parts where the model is being shown an image.
72
+ * @param [options] - Reply ceiling, temperature, a reasoning effort, cancellation, notices.
73
+ * @returns The reply's content, trimmed. Where that is empty once the thinking is stripped, a
74
+ * `reasoning_content` the server sent beside it, stripped the same way, or an empty string.
77
75
  */
78
76
  export function ask(config, model, system, user, options = {}) {
79
77
  return complete(config, model, system, user, options);
@@ -81,6 +79,19 @@ export function ask(config, model, system, user, options = {}) {
81
79
  /**
82
80
  * The request `ask` and `askJson` share, with `format` deciding the extra body fields from what
83
81
  * the model and the endpoint have refused, rebuilt on every re-send.
82
+ *
83
+ * @param config - Where to send it. Its `baseUrl` and key also pick the capabilities the request is
84
+ * negotiated against, and with `model` the pair a refusal of the hints is latched on.
85
+ * @param model - The model to ask.
86
+ * @param system - The instruction, sent as the system message.
87
+ * @param user - The input, sent as the user message as it was given.
88
+ * @param options - A side task's options. A `reasoningEffort` that is a level leaves the
89
+ * no-thinking hints out; absent, `""` or `"off"` sends them unless this pair has refused them.
90
+ * @param [format] - Builds the extra body fields from the endpoint's capabilities and the model's.
91
+ * Called on every send, so a re-send is built from what has been latched since. Absent adds none.
92
+ * @returns The first choice's message as `answerOf` reads it, empty when there was no answer.
93
+ * Throws what the request throws, except that a 400 or 422 `negotiate` did not answer, on a
94
+ * request that carried the hints or an effort, is retried once without either.
84
95
  */
85
96
  async function complete(config, model, system, user, { maxTokens = 512, temperature = 0.3, reasoningEffort, signal, onNotice }, format) {
86
97
  // The effort the caller chose, or empty where it chose none: `""` and `"off"` read as absent,
@@ -93,24 +104,18 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
93
104
  // models refuse it by value and list `minimal` as their floor. `effortFor` answers with the
94
105
  // cheapest rung this one takes, which `negotiate` has been stepping up as it was refused.
95
106
  // A level the caller chose goes through the same ladder, which only ever steps up from it.
96
- const asked = effort ? effortFor(refused, level || "none") : "";
97
- sentEffort = asked !== "";
107
+ const asked = effort ? effortFor(refused, level || EFFORT_NONE) : '';
108
+ sentEffort = asked !== '';
98
109
  return getClient(config).chat.completions.create({
99
110
  model,
100
111
  // The reasoning models want the ceiling spelled the other way, and they are exactly the
101
- // models a side task most wants to stop deliberating. Zero is no ceiling, as it is on the
102
- // main model: a spec's `maxTokens: 0` sent as `max_tokens: 0` asks for an empty reply.
103
- ...(maxTokens > 0
104
- ? refused && !refused.legacyTokenLimit
105
- ? { max_completion_tokens: maxTokens }
106
- : { max_tokens: maxTokens }
107
- : {}),
108
- // One that will only run at the temperature it was built with is sent none: a side task
109
- // wants the same answer twice, and 1.0 from that model is as close as it gets.
110
- ...(refused && !refused.chosenTemperature ? {} : { temperature }),
112
+ // models a side task most wants to stop deliberating. One that will only run at its own
113
+ // temperature is sent none: a side task wants the same answer twice, and 1.0 from that
114
+ // model is as close as it gets.
115
+ ...ceilingAndTemperature(refused, maxTokens, temperature),
111
116
  messages: [
112
- { role: "system", content: system },
113
- { role: "user", content: user },
117
+ { role: Role.System, content: system },
118
+ { role: Role.User, content: user },
114
119
  ],
115
120
  ...(hints ? NO_THINKING : {}),
116
121
  // Not gated on `hints`: a model that refuses `chat_template_kwargs` may still read the
@@ -128,7 +133,7 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
128
133
  model,
129
134
  onNotice,
130
135
  });
131
- const key = hintKey(endpointId(config), model);
136
+ const key = modelKey(endpointId(config), model);
132
137
  // A chosen effort replaces the template hint rather than riding beside it: `enable_thinking:
133
138
  // false` next to `reasoning_effort: "high"` is a request that contradicts itself.
134
139
  const hints = !level && !noHints().has(key);
@@ -142,8 +147,9 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
142
147
  // does not offer as `none`. Or a 400 about something else entirely, which is why the
143
148
  // notice says what was tried rather than what was wrong.
144
149
  const effort = sentEffort;
145
- if (!(hints || effort) || !rejectedTheRequest(error))
150
+ if (!(hints || effort) || !refusesRequest(error)) {
146
151
  throw error;
152
+ }
147
153
  onNotice?.(level
148
154
  ? `${model} rejected a request carrying a reasoning effort; retrying without it`
149
155
  : `${model} rejected a request carrying the no-thinking hints; retrying without them`);
@@ -153,57 +159,70 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
153
159
  // hints again. When both went out the refusal cannot say which, so the one `negotiate`
154
160
  // cannot latch is blamed; if it was the effort after all, the next call is left with only
155
161
  // the effort to drop and latches that instead.
156
- if (hints)
162
+ if (hints) {
157
163
  noHints().add(key);
158
- else
164
+ }
165
+ else {
159
166
  modelCapabilitiesFor(supports, model).reasoningEffort = false;
167
+ }
168
+ }
169
+ return answerOf(response.choices[0]?.message);
170
+ }
171
+ /**
172
+ * A reply's answer, thinking stripped, or its scratchpad where the answer is nothing else.
173
+ *
174
+ * @param message - The first choice's message. Absent, as when the reply held no choices, reads as
175
+ * an empty one.
176
+ * @returns The content, trimmed, with its thinking fences stripped. Where that leaves nothing, a
177
+ * string `reasoning_content` read the same way, and an empty string where there is none.
178
+ *
179
+ * @remarks
180
+ * Reasoning models that ignore the hints still fence their scratchpad. A side task answers under
181
+ * a small ceiling, so the fence often never closes, and a template that opened it in the prompt
182
+ * leaves only the close; either way the deliberation used to come back as the answer.
183
+ */
184
+ function answerOf(message) {
185
+ const answer = stripThinking(message?.content ?? '').trim();
186
+ if (answer) {
187
+ return answer;
160
188
  }
161
- const message = response.choices[0]?.message;
162
- // Reasoning models that ignore the hints still fence their scratchpad. A side task answers
163
- // under a small ceiling, so the fence often never closes, and a template that opened it in
164
- // the prompt leaves only the close; either way the deliberation used to come back as the answer.
165
- const answer = stripThinking(message?.content ?? "").trim();
166
189
  // Nothing but scratchpad. Some servers put the deliberation in its own field and leave the
167
190
  // content genuinely empty, in which case there is no answer to find anywhere else.
168
- if (answer)
169
- return answer;
170
191
  const reasoning = message?.reasoning_content;
171
- return typeof reasoning === "string" ? stripThinking(reasoning).trim() : "";
192
+ return typeof reasoning === 'string' ? stripThinking(reasoning).trim() : '';
172
193
  }
173
194
  /**
174
195
  * A side task whose answer is JSON matching a schema, parsed. Undefined when no JSON came back.
175
196
  * Throws like any request.
176
197
  *
198
+ * @param config - Where to send it and how long to wait.
199
+ * @param model - The model to ask.
200
+ * @param system - The instruction. The schema is appended to it.
201
+ * @param user - The input it applies to. Content parts where the model is being shown an image.
202
+ * @param schema - The JSON Schema of the answer. Its root is held to an object, as a tool's is.
203
+ * @param [options] - A side task's options, plus the schema's `name` and whether it is `strict`.
204
+ * @returns What the reply parsed to, unchecked against the schema: `T` is the caller's claim.
205
+ * `undefined` when `parseJson` found nothing in it.
206
+ *
207
+ * @remarks
177
208
  * Sends `response_format` with the schema, which a llama.cpp server compiles into a grammar and
178
209
  * vLLM, LM Studio, Ollama and OpenAI each hold the reply to, so a small model that wraps JSON in
179
210
  * prose cannot. The schema is normalised as a tool's parameters are, and relaxed where the endpoint
180
211
  * could not build a grammar, since the same converter reads both. A model that refuses the field
181
212
  * latches it off and is asked in words: the schema rides on the system prompt either way, and
182
213
  * the reply goes through `parseJson`, which finds the JSON in whatever came back.
183
- *
184
- * @param config Where to send it and how long to wait.
185
- * @param model The model to ask.
186
- * @param system The instruction. The schema is appended to it.
187
- * @param user The input it applies to. Content parts where the model is being shown an image.
188
- * @param schema The JSON Schema of the answer. Its root is held to an object, as a tool's is.
189
- * @param options A side task's options, plus the schema's `name` and whether it is `strict`.
190
214
  */
191
- export async function askJson(config, model, system, user, schema, { name = "answer", strict = true, ...options } = {}) {
192
- const tool = (parameters) => ({
193
- type: "function",
194
- function: { name, parameters },
195
- });
196
- const [sanitized] = sanitizeTools([tool(schema)]);
197
- const shapeOf = (definition) => definition?.type === "function" ? (definition.function.parameters ?? {}) : {};
198
- const instruction = `${system}\n\nReply with JSON alone, matching this JSON Schema:\n${JSON.stringify(shapeOf(sanitized))}`;
215
+ export async function askJson(config, model, system, user, schema, { name = 'answer', strict = true, ...options } = {}) {
216
+ const sanitized = sanitizeSchema(schema);
217
+ const instruction = `${system}\n\nReply with JSON alone, matching this JSON Schema:\n${JSON.stringify(sanitized)}`;
199
218
  const reply = await complete(config, model, instruction, user, options, (supports, refused) => refused.structuredOutput
200
219
  ? {
201
220
  response_format: {
202
- type: "json_schema",
221
+ type: JSON_SCHEMA_FORMAT,
203
222
  json_schema: {
204
223
  name,
205
224
  strict,
206
- schema: shapeOf(supports.strictSchemas ? sanitized : relaxTools([sanitized])[0]),
225
+ schema: supports.strictSchemas ? sanitized : relaxSchema(sanitized),
207
226
  },
208
227
  },
209
228
  }
@@ -211,12 +230,17 @@ export async function askJson(config, model, system, user, schema, { name = "ans
211
230
  return parseJson(reply);
212
231
  }
213
232
  /**
233
+ * Runs a side task and answers `undefined` where it fails, so the work it supports carries on.
234
+ *
235
+ * @param label - Names the task in the notice when it fails.
236
+ * @param run - The call to attempt. Anything it throws becomes `undefined`, an abort excepted.
237
+ * @param [options] - `onNotice`, told what was given up on.
238
+ * @returns What `run` resolved with, or `undefined` when it threw. An `APIUserAbortError` is thrown
239
+ * on and raises no notice.
240
+ *
241
+ * @remarks
214
242
  * A side task is never worth failing the work it supports. Callers that can carry on without
215
243
  * an answer use this and get `undefined` instead of an exception.
216
- *
217
- * @param label Names the task in the notice when it fails.
218
- * @param run The call to attempt. Anything it throws becomes `undefined`, an abort excepted.
219
- * @param options `onNotice`, told what was given up on.
220
244
  */
221
245
  export async function tryAsk(label, run, { onNotice } = {}) {
222
246
  try {
@@ -225,55 +249,120 @@ export async function tryAsk(label, run, { onNotice } = {}) {
225
249
  catch (error) {
226
250
  // A cancelled run is not a failed side task. Swallowing the abort made the two
227
251
  // indistinguishable and left the cancellation with nowhere to go.
228
- if (error instanceof OpenAI.APIUserAbortError)
252
+ if (error instanceof OpenAI.APIUserAbortError) {
229
253
  throw error;
254
+ }
230
255
  onNotice?.(`${label}: ${errorMessage(error)}`);
231
256
  return undefined;
232
257
  }
233
258
  }
234
259
  /**
235
- * Models are asked for JSON and often answer with prose around it, or a fenced block. Pull out
236
- * the first array or object rather than failing the task over a wrapper.
260
+ * The first array or object in a model's reply, parsed, whatever prose or fence surrounds it.
261
+ *
262
+ * @param text - The reply, fences and prose included. Nothing parseable gives `undefined`.
263
+ * @returns What lies between the first `[` or `{` and the last `]` or `}`, parsed — inside the
264
+ * first fenced block where there is one. Unchecked: `T` is the caller's claim. `undefined` when
265
+ * there is no such stretch or it will not parse even repaired.
266
+ *
267
+ * @remarks
268
+ * Models are asked for JSON and often answer with prose around it, or a fenced block, and a
269
+ * wrapper is not worth failing the task over.
237
270
  *
238
- * @param text The reply, fences and prose included. Nothing parseable gives `undefined`.
271
+ * What is pulled out is read the way a tool call's arguments are: as written where that parses,
272
+ * and otherwise with the almost-JSON a local model writes repaired — single quotes, `True` and
273
+ * `None`, bare keys, a trailing comma. A small model asked for a list gets these wrong as often in
274
+ * a reply as in a call, and a side task that fails over one costs the run a second request.
239
275
  */
240
276
  export function parseJson(text) {
241
277
  const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
242
278
  const body = (fenced?.[1] ?? text).trim();
243
279
  const start = body.search(/[[{]/);
244
- if (start < 0)
280
+ if (start < 0) {
245
281
  return undefined;
246
- const end = Math.max(body.lastIndexOf("]"), body.lastIndexOf("}"));
247
- if (end <= start)
248
- return undefined;
249
- try {
250
- return JSON.parse(body.slice(start, end + 1));
251
282
  }
252
- catch {
283
+ const end = Math.max(body.lastIndexOf(']'), body.lastIndexOf('}'));
284
+ if (end <= start) {
253
285
  return undefined;
254
286
  }
287
+ return looseJson(body.slice(start, end + 1));
255
288
  }
256
289
  /**
257
290
  * Strips the quoting and list punctuation models decorate short answers with.
258
291
  *
259
- * @param line One line of a reply.
292
+ * @param line - One line of a reply.
293
+ * @returns The line trimmed, without one leading bullet or number, any quotes or backticks at
294
+ * either end, or trailing full stops. Empty when nothing else was there.
260
295
  */
261
296
  export const clean = (line) => line
262
297
  .trim()
263
- .replace(/^(?:[-*•]|\d+[.)])\s*/, "")
264
- .replace(/^["'`]+|["'`.]+$/g, "")
298
+ .replace(/^(?:[-*•]|\d+[.)])\s*/, '')
299
+ .replace(/^["'`]+|["'`.]+$/g, '')
265
300
  .trim();
266
301
  /**
267
302
  * A list-shaped reply, one item per line, cleaned of the bullets and quotes models decorate
268
303
  * them with. Overlong items are dropped rather than truncated — a suggestion that has to be
269
304
  * squinted at is worse than one fewer suggestion.
270
305
  *
271
- * @param text The reply, one item per line.
272
- * @param max How many items to keep.
273
- * @param maxChars Longest item kept. Longer ones are dropped, not truncated.
306
+ * @param text - The reply, one item per line.
307
+ * @param max - How many items to keep.
308
+ * @param maxChars - Longest item kept. Longer ones are dropped, not truncated.
309
+ * @returns The first `max` items that survive, in reply order. Lines that clean to nothing are not
310
+ * items, and an overlong one does not count towards `max`.
274
311
  */
275
312
  export const listLines = (text, max, maxChars) => text
276
- .split("\n")
313
+ .split('\n')
277
314
  .map(clean)
278
315
  .filter((line) => line.length > 0 && line.length <= maxChars)
279
316
  .slice(0, max);
317
+ /**
318
+ * A side task's settings as the call that honours them: its endpoint, its model, and the options
319
+ * `ask`, `askJson`, `summariser` and `preselect` take last.
320
+ *
321
+ * @param task - The task, ordinarily `resolved.tasks.<key>`.
322
+ * @param [options] - What the host adds — cancellation, notices, `keywords` — and its own defaults
323
+ * for the settings a task may leave out.
324
+ * @param [agent] - The endpoint the main turn uses, key included. Absent sends the task's endpoint
325
+ * as it stands.
326
+ * @returns The three arguments a side-task entry point takes. `options` is a new object, and
327
+ * `endpoint` is the task's own object when no `agent` was given, a copy otherwise.
328
+ *
329
+ * @remarks
330
+ * A helper rather than a second signature on each of the four, so a setting added to a task later
331
+ * arrives through `options` without a host's call changing. What the task states wins over the
332
+ * same field in `options`: the host's code is written once and the task is what an operator
333
+ * configured over it, so a host passes its own ceiling as the value for a task that names none.
334
+ * A setting the task leaves out and `options` does too is absent from the result, which is what
335
+ * leaves it to the entry point's own default — 0.3, not the agent's temperature.
336
+ *
337
+ * A resolved spec carries no key, so a task on the agent's own endpoint would otherwise go out
338
+ * unkeyed, and be remembered under a second capability entry beside the agent's. Given `agent`,
339
+ * such a task is sent the agent's `baseUrl` as the agent spells it and, having no key of its own,
340
+ * the agent's key, so `capabilitiesFor` answers both with one object; a task on another endpoint
341
+ * is sent only a key of its own, as `resolveApiKey` has it. The environment is not read: the
342
+ * agent's key is whatever the main turn is sent.
343
+ */
344
+ export function taskCall(task, options, agent) {
345
+ const own = task.endpoint;
346
+ const shared = agent && (!own.baseUrl.trim() || sameUrl(own.baseUrl, agent.baseUrl));
347
+ const stated = {};
348
+ if (task.maxTokens !== undefined) {
349
+ stated.maxTokens = task.maxTokens;
350
+ }
351
+ if (task.temperature !== undefined) {
352
+ stated.temperature = task.temperature;
353
+ }
354
+ if (task.reasoningEffort !== undefined) {
355
+ stated.reasoningEffort = task.reasoningEffort;
356
+ }
357
+ return {
358
+ endpoint: agent
359
+ ? {
360
+ ...own,
361
+ ...(shared ? { baseUrl: agent.baseUrl } : {}),
362
+ apiKey: resolveApiKey(own, agent, {}),
363
+ }
364
+ : own,
365
+ model: task.model,
366
+ options: { ...options, ...stated },
367
+ };
368
+ }
@@ -16,8 +16,8 @@ export interface ModelSnapshot {
16
16
  refusedFields: string[];
17
17
  structuredOutput: boolean;
18
18
  /**
19
- * Continues a trailing assistant message. Absent in a snapshot taken before it was latched, which
20
- * reads as not refused.
19
+ * Continues a trailing assistant message. Always written; a stored snapshot from before it was
20
+ * latched lacks it, which `importCapabilities` reads as not refused.
21
21
  */
22
22
  assistantPrefill: boolean;
23
23
  /** Takes the no-thinking hints `ask` sends. */
@@ -28,7 +28,7 @@ export interface ModelSnapshot {
28
28
  * none refused.
29
29
  */
30
30
  refusedEfforts?: string[];
31
- /** The efforts a refusal published as this model's, in ladder order. Absent is none published. */
31
+ /** The efforts a refusal published as this model's, in the order it gave them. Absent is none published. */
32
32
  supportedEfforts?: string[];
33
33
  }
34
34
  /** What one endpoint refused, and under it what each of its models did. */
@@ -55,6 +55,10 @@ export interface CapabilitySnapshot {
55
55
  /**
56
56
  * Every refusal this process has latched, as a JSON-safe blob to store and hand back on boot.
57
57
  *
58
+ * @returns A fresh object that shares nothing with the live latches, its lists copied and the two
59
+ * refused ones sorted.
60
+ *
61
+ * @remarks
58
62
  * Covers what `negotiate` latches on endpoints and models and the models `ask` found refusing the
59
63
  * no-thinking hints. Only what was actually refused is in it, so a snapshot of a process that met
60
64
  * no refusals has no endpoints. Endpoints are named by digest rather than URL and key, since the
@@ -65,15 +69,16 @@ export declare function exportCapabilities(): CapabilitySnapshot;
65
69
  /**
66
70
  * Latches what a stored snapshot says was refused, on top of whatever this process has learned.
67
71
  *
72
+ * @param snapshot - What `exportCapabilities` returned, as stored. Read defensively: a field of the
73
+ * wrong type is skipped rather than trusted.
74
+ * @returns Whether the snapshot was of this version and applied.
75
+ *
76
+ * @remarks
68
77
  * Refusals only ever latch off, so importing merges rather than replaces: a flag already off stays
69
78
  * off whatever the snapshot says, and one the snapshot has off is turned off. A snapshot of another
70
79
  * version, or anything that is not one, is ignored — a stale shape costs the refused requests it
71
80
  * would have saved, which is what a restart cost before. How old is too old is the consumer's call,
72
81
  * made on `savedAt` before importing, or afterwards per endpoint with `expireCapabilities`, since a
73
82
  * server behind a URL can be upgraded between boots.
74
- *
75
- * @param snapshot What `exportCapabilities` returned, as stored. Read defensively: a field of the
76
- * wrong type is skipped rather than trusted.
77
- * @returns Whether the snapshot was of this version and applied.
78
83
  */
79
84
  export declare function importCapabilities(snapshot: unknown): boolean;