@cubicecho/agent-core 4.3.0 → 4.4.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 CHANGED
@@ -268,6 +268,24 @@ const page: SideTaskInput = [
268
268
  const text = await ask(config, visionModel, "You are an OCR engine.", page);
269
269
  ```
270
270
 
271
+ ### Telling a finished reply from a cut-off one
272
+
273
+ `ask` returns the text alone, so a reply that ran into `maxTokens` reads like one that finished.
274
+ Where the reply is the product, `askReply` takes the same arguments and also returns the choice's
275
+ `finish_reason` as the server sent it, or `undefined` where it sent none. A model looping on a page
276
+ writes until the ceiling stops it, and a transcript saved from that is worse than none:
277
+
278
+ ```ts
279
+ const { text, finishReason } = await askReply(config, visionModel, "You are an OCR engine.", page);
280
+ if (finishReason === FinishReason.Length) {
281
+ // cut off: retry with a higher ceiling, or keep the OCR text already there
282
+ }
283
+ ```
284
+
285
+ Nothing is thrown for a cut-off reply, since whether one is still worth keeping is the caller's
286
+ call. `askJson` needs no such variant: a reply cut off before its JSON closed does not parse, so it
287
+ comes back `undefined`, as a reply with no JSON in it does.
288
+
271
289
  ## Sizing a request before sending it
272
290
 
273
291
  `runTurn` will refuse a request that cannot fit rather than spending a round trip finding out:
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export { type KeywordPreselection, type KeywordPreselectOptions, PRESELECT_SCHEM
31
31
  export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, orderTools, PROXY_TOOLS, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, type ToolOrder, } from './tools/tool-loading.ts';
32
32
  export { type ContinueTurnOptions, continueTurn, } from './turn/continuation.ts';
33
33
  export { type RunTurnOptions, runTurn } from './turn/run-turn.ts';
34
- export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTask, type SideTaskInput, type SideTaskOptions, taskCall, tryAsk, } from './turn/side-task.ts';
34
+ export { type AskJsonOptions, ask, askJson, askReply, clean, listLines, parseJson, resetHints, type SideTask, type SideTaskInput, type SideTaskOptions, type SideTaskReply, taskCall, tryAsk, } from './turn/side-task.ts';
35
35
  export { AgentLoopError, type AgentLoopFailure, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from './wire/errors.ts';
36
36
  export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow, isTransient, SMALLEST_LIKELY_WINDOW, } from './wire/retry.ts';
37
37
  export { CacheBreakReason, type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from './wire/stream.ts';
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ export { PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselect, preselectByKeywords, pre
30
30
  export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, orderTools, PROXY_TOOLS, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, } from './tools/tool-loading.js';
31
31
  export { continueTurn, } from './turn/continuation.js';
32
32
  export { runTurn } from './turn/run-turn.js';
33
- export { ask, askJson, clean, listLines, parseJson, resetHints, taskCall, tryAsk, } from './turn/side-task.js';
33
+ export { ask, askJson, askReply, clean, listLines, parseJson, resetHints, taskCall, tryAsk, } from './turn/side-task.js';
34
34
  export { AgentLoopError, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from './wire/errors.js';
35
35
  export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow, isTransient, SMALLEST_LIKELY_WINDOW, } from './wire/retry.js';
36
36
  export { CacheBreakReason, streamTurn, } from './wire/stream.js';
@@ -122,6 +122,33 @@ export interface SideTask {
122
122
  * `reasoning_content` the server sent beside it, stripped the same way, or an empty string.
123
123
  */
124
124
  export declare function ask(config: Endpoint, model: string, system: string, user: SideTaskInput, options?: SideTaskOptions): Promise<string>;
125
+ /** A side task's reply, with why the model stopped writing it. */
126
+ export interface SideTaskReply {
127
+ /** The reply's content as `ask` returns it: trimmed, thinking stripped. */
128
+ text: string;
129
+ /**
130
+ * The first choice's `finish_reason` as the server sent it, compared with `FinishReason`.
131
+ * `FinishReason.Length` means `text` was cut off at the ceiling. Undefined when none was sent.
132
+ */
133
+ finishReason: string | undefined;
134
+ }
135
+ /**
136
+ * Runs a side task as `ask` does, and says why the reply ended as well as what it said.
137
+ *
138
+ * @param config - Where to send it and how long to wait.
139
+ * @param model - The model to ask.
140
+ * @param system - The instruction.
141
+ * @param user - The input it applies to. Content parts where the model is being shown an image.
142
+ * @param [options] - Reply ceiling, temperature, a reasoning effort, extra fields, cancellation, notices.
143
+ * @returns The text `ask` would return, and the `finish_reason` it came with.
144
+ *
145
+ * @remarks
146
+ * For a caller whose reply is the product, such as a page's transcript: one that ran into the
147
+ * ceiling, often because the model was looping, reads like a finished one in its text alone.
148
+ * Nothing is thrown for it, because whether a cut-off reply is still worth keeping is the
149
+ * caller's call.
150
+ */
151
+ export declare function askReply(config: Endpoint, model: string, system: string, user: SideTaskInput, options?: SideTaskOptions): Promise<SideTaskReply>;
125
152
  /** What `askJson` takes besides a side task's options. */
126
153
  export interface AskJsonOptions extends SideTaskOptions {
127
154
  /**
@@ -150,7 +177,7 @@ export interface AskJsonOptions extends SideTaskOptions {
150
177
  * @param schema - The JSON Schema of the answer. Its root is held to an object, as a tool's is.
151
178
  * @param [options] - A side task's options, plus the schema's `name` and whether it is `strict`.
152
179
  * @returns What the reply parsed to, unchecked against the schema: `T` is the caller's claim.
153
- * `undefined` when `parseJson` found nothing in it.
180
+ * `undefined` when `parseJson` found nothing in it, as in a reply cut off before its JSON closed.
154
181
  *
155
182
  * @remarks
156
183
  * Sends `response_format` with the schema, which a llama.cpp server compiles into a grammar and
@@ -216,7 +243,7 @@ export declare const clean: (line: string) => string;
216
243
  export declare const listLines: (text: string, max: number, maxChars: number) => string[];
217
244
  /**
218
245
  * A side task's settings as the call that honours them: its endpoint, its model, and the options
219
- * `ask`, `askJson`, `summariser` and `preselect` take last.
246
+ * `ask`, `askReply`, `askJson`, `summariser` and `preselect` take last.
220
247
  *
221
248
  * @param task - The task, ordinarily `resolved.tasks.<key>`.
222
249
  * @param [options] - What the host adds — cancellation, notices, `keywords` — and its own defaults
@@ -227,7 +254,7 @@ export declare const listLines: (text: string, max: number, maxChars: number) =>
227
254
  * `endpoint` is the task's own object when no `agent` was given, a copy otherwise.
228
255
  *
229
256
  * @remarks
230
- * A helper rather than a second signature on each of the four, so a setting added to a task later
257
+ * A helper rather than a second signature on each entry point, so a setting added to a task later
231
258
  * arrives through `options` without a host's call changing. What the task states wins over the
232
259
  * same field in `options`: the host's code is written once and the task is what an operator
233
260
  * configured over it, so a host passes its own ceiling as the value for a task that names none.
@@ -72,7 +72,27 @@ export const resetHints = () => noHints().clear();
72
72
  * @returns The reply's content, trimmed. Where that is empty once the thinking is stripped, a
73
73
  * `reasoning_content` the server sent beside it, stripped the same way, or an empty string.
74
74
  */
75
- export function ask(config, model, system, user, options = {}) {
75
+ export async function ask(config, model, system, user, options = {}) {
76
+ const { text } = await complete(config, model, system, user, options);
77
+ return text;
78
+ }
79
+ /**
80
+ * Runs a side task as `ask` does, and says why the reply ended as well as what it said.
81
+ *
82
+ * @param config - Where to send it and how long to wait.
83
+ * @param model - The model to ask.
84
+ * @param system - The instruction.
85
+ * @param user - The input it applies to. Content parts where the model is being shown an image.
86
+ * @param [options] - Reply ceiling, temperature, a reasoning effort, extra fields, cancellation, notices.
87
+ * @returns The text `ask` would return, and the `finish_reason` it came with.
88
+ *
89
+ * @remarks
90
+ * For a caller whose reply is the product, such as a page's transcript: one that ran into the
91
+ * ceiling, often because the model was looping, reads like a finished one in its text alone.
92
+ * Nothing is thrown for it, because whether a cut-off reply is still worth keeping is the
93
+ * caller's call.
94
+ */
95
+ export function askReply(config, model, system, user, options = {}) {
76
96
  return complete(config, model, system, user, options);
77
97
  }
78
98
  /**
@@ -89,8 +109,8 @@ export function ask(config, model, system, user, options = {}) {
89
109
  * An `extraBody` goes out less the fields this model has refused by name.
90
110
  * @param [format] - Builds the extra body fields from the endpoint's capabilities and the model's.
91
111
  * 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
112
+ * @returns The first choice's message as `answerOf` reads it, empty when there was no answer, and
113
+ * its `finish_reason`. Throws what the request throws, except that a 400 or 422 `negotiate` did not answer, on a
94
114
  * request that carried the hints or an effort, is retried once without either.
95
115
  */
96
116
  async function complete(config, model, system, user, { maxTokens = 512, temperature = 0.3, reasoningEffort, extraBody, signal, onNotice }, format) {
@@ -168,7 +188,9 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
168
188
  modelCapabilitiesFor(supports, model).reasoningEffort = false;
169
189
  }
170
190
  }
171
- return answerOf(response.choices[0]?.message);
191
+ const [choice] = response.choices;
192
+ // Typed as always present, and sent as `null` or not at all by servers that have none to give.
193
+ return { text: answerOf(choice?.message), finishReason: choice?.finish_reason ?? undefined };
172
194
  }
173
195
  /**
174
196
  * A reply's answer, thinking stripped, or its scratchpad where the answer is nothing else.
@@ -204,7 +226,7 @@ function answerOf(message) {
204
226
  * @param schema - The JSON Schema of the answer. Its root is held to an object, as a tool's is.
205
227
  * @param [options] - A side task's options, plus the schema's `name` and whether it is `strict`.
206
228
  * @returns What the reply parsed to, unchecked against the schema: `T` is the caller's claim.
207
- * `undefined` when `parseJson` found nothing in it.
229
+ * `undefined` when `parseJson` found nothing in it, as in a reply cut off before its JSON closed.
208
230
  *
209
231
  * @remarks
210
232
  * Sends `response_format` with the schema, which a llama.cpp server compiles into a grammar and
@@ -217,7 +239,7 @@ function answerOf(message) {
217
239
  export async function askJson(config, model, system, user, schema, { name = 'answer', strict = true, ...options } = {}) {
218
240
  const sanitized = sanitizeSchema(schema);
219
241
  const instruction = `${system}\n\nReply with JSON alone, matching this JSON Schema:\n${JSON.stringify(sanitized)}`;
220
- const reply = await complete(config, model, instruction, user, options, (supports, refused) => refused.structuredOutput
242
+ const { text } = await complete(config, model, instruction, user, options, (supports, refused) => refused.structuredOutput
221
243
  ? {
222
244
  response_format: {
223
245
  type: JSON_SCHEMA_FORMAT,
@@ -229,7 +251,7 @@ export async function askJson(config, model, system, user, schema, { name = 'ans
229
251
  },
230
252
  }
231
253
  : {});
232
- return parseJson(reply);
254
+ return parseJson(text);
233
255
  }
234
256
  /**
235
257
  * Runs a side task and answers `undefined` where it fails, so the work it supports carries on.
@@ -321,7 +343,7 @@ export const listLines = (text, max, maxChars) => text
321
343
  .slice(0, max);
322
344
  /**
323
345
  * A side task's settings as the call that honours them: its endpoint, its model, and the options
324
- * `ask`, `askJson`, `summariser` and `preselect` take last.
346
+ * `ask`, `askReply`, `askJson`, `summariser` and `preselect` take last.
325
347
  *
326
348
  * @param task - The task, ordinarily `resolved.tasks.<key>`.
327
349
  * @param [options] - What the host adds — cancellation, notices, `keywords` — and its own defaults
@@ -332,7 +354,7 @@ export const listLines = (text, max, maxChars) => text
332
354
  * `endpoint` is the task's own object when no `agent` was given, a copy otherwise.
333
355
  *
334
356
  * @remarks
335
- * A helper rather than a second signature on each of the four, so a setting added to a task later
357
+ * A helper rather than a second signature on each entry point, so a setting added to a task later
336
358
  * arrives through `options` without a host's call changing. What the task states wins over the
337
359
  * same field in `options`: the host's code is written once and the task is what an operator
338
360
  * configured over it, so a host passes its own ceiling as the value for a task that names none.
package/llms.txt CHANGED
@@ -305,6 +305,7 @@ One-shot calls that support a run without being one: picking tools, naming a ses
305
305
  - `AskJsonOptions` (type) — What `askJson` takes besides a side task's options.
306
306
  - `ask` — Runs a side task and returns the reply text, thinking stripped.
307
307
  - `askJson` — A side task whose answer is JSON matching a schema, parsed.
308
+ - `askReply` — Runs a side task as `ask` does, and says why the reply ended as well as what it said.
308
309
  - `clean` — Strips the quoting and list punctuation models decorate short answers with.
309
310
  - `listLines` — A list-shaped reply, one item per line, cleaned of the bullets and quotes models decorate them with.
310
311
  - `parseJson` — The first array or object in a model's reply, parsed, whatever prose or fence surrounds it.
@@ -312,7 +313,8 @@ One-shot calls that support a run without being one: picking tools, naming a ses
312
313
  - `SideTask` (type) — One side task's own settings, as a resolved agent spec carries them under `tasks.<key>`.
313
314
  - `SideTaskInput` (type) — The input a side task applies its instruction to: text, or the content parts a vision model reads.
314
315
  - `SideTaskOptions` (type) — What a side task may be given.
315
- - `taskCall` — A side task's settings as the call that honours them: its endpoint, its model, and the options `ask`, `askJson`, `summariser` and `preselect` take last.
316
+ - `SideTaskReply` (type) — A side task's reply, with why the model stopped writing it.
317
+ - `taskCall` — A side task's settings as the call that honours them: its endpoint, its model, and the options `ask`, `askReply`, `askJson`, `summariser` and `preselect` take last.
316
318
  - `tryAsk` — Runs a side task and answers `undefined` where it fails, so the work it supports carries on.
317
319
 
318
320
  ### errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
5
5
  "keywords": [
6
6
  "openai",