@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 +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/turn/side-task.d.ts +30 -3
- package/dist/turn/side-task.js +31 -9
- package/llms.txt +3 -1
- package/package.json +1 -1
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';
|
package/dist/turn/side-task.d.ts
CHANGED
|
@@ -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
|
|
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.
|
package/dist/turn/side-task.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
- `
|
|
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
|
+
"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",
|