@cubicecho/agent-core 4.2.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 +27 -2
- package/dist/endpoint/capabilities.d.ts +15 -1
- package/dist/endpoint/capabilities.js +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run/request-body.d.ts +1 -1
- package/dist/run/request-body.js +2 -4
- package/dist/spec/spec.d.ts +5 -1
- package/dist/spec/spec.js +36 -26
- package/dist/tools/preselect.d.ts +5 -3
- package/dist/tools/preselect.js +4 -3
- package/dist/turn/side-task.d.ts +44 -4
- package/dist/turn/side-task.js +41 -12
- 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:
|
|
@@ -588,7 +606,7 @@ one just over it means the ranking chose arbitrarily, which is the case a model
|
|
|
588
606
|
on. Matching nothing is not confident either, since the words cannot tell a request that needs no
|
|
589
607
|
tools from one whose words are not in the catalogue. An empty `toolSelectModel` still means no
|
|
590
608
|
preselection at all, words included. `preselect` also takes a `temperature` and a
|
|
591
|
-
`reasoningEffort`, read as `ask` reads them, and `runAgentLoop({ preselect: true })` makes the call
|
|
609
|
+
`reasoningEffort` and an `extraBody`, read as `ask` reads them, and `runAgentLoop({ preselect: true })` makes the call
|
|
592
610
|
itself — see "Its side tasks" below.
|
|
593
611
|
|
|
594
612
|
A turn cut off at `maxTokens` is said so as a notice, and with `maxContinuations` above zero it is
|
|
@@ -1196,7 +1214,7 @@ comes from.
|
|
|
1196
1214
|
### Its side tasks
|
|
1197
1215
|
|
|
1198
1216
|
`tasks.<key>` configures a side task apart from the main model — its own model, endpoint, ceiling,
|
|
1199
|
-
temperature
|
|
1217
|
+
temperature, reasoning effort and `extraBody` — and resolution hands each back as a `ResolvedTask` holding only
|
|
1200
1218
|
what a layer stated. `taskCall` turns one into the arguments `ask`, `askJson`, `summariser` and
|
|
1201
1219
|
`preselect` already take, so a setting added to a task later arrives without the host's call
|
|
1202
1220
|
changing:
|
|
@@ -1217,6 +1235,13 @@ no-thinking hints by default, and absent or `"off"` keeps them; a level — `"lo
|
|
|
1217
1235
|
sent as `reasoning_effort` in their place, for a compaction model an operator pointed at a model
|
|
1218
1236
|
that should deliberate.
|
|
1219
1237
|
|
|
1238
|
+
`extraBody` carries the sampler fields a model card asks for, as it does on the main model: `ask`,
|
|
1239
|
+
`askJson`, `summariser` and `preselect` all take one in their options, merge it in after the
|
|
1240
|
+
temperature, and offer its keys to `negotiate` as `droppable`, so a field the endpoint refuses by
|
|
1241
|
+
name is latched off for that model and the request is sent again without it. The agent's own
|
|
1242
|
+
`model.extraBody` is never sent to a side task, and `taskCall` lays a task's over the host's key by
|
|
1243
|
+
key.
|
|
1244
|
+
|
|
1220
1245
|
The third argument is the endpoint the main turn uses. Given it, a task on the same endpoint is
|
|
1221
1246
|
sent the agent's spelling of the URL and, having no key of its own, the agent's key, so the two
|
|
1222
1247
|
share one entry in `capabilitiesFor` and a refusal learned by either is known to both; a task on
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EndpointIdentity } from '../core/config.ts';
|
|
2
2
|
import type { Produced } from '../wire/stream.ts';
|
|
3
3
|
/**
|
|
4
4
|
* What an endpoint turned out not to support, and answering it when it says so.
|
|
@@ -307,6 +307,20 @@ export declare const ceilingAndTemperature: (refused: ModelCapabilities | undefi
|
|
|
307
307
|
} | {
|
|
308
308
|
temperature?: number | undefined;
|
|
309
309
|
};
|
|
310
|
+
/**
|
|
311
|
+
* The request fields an `extraBody` adds as this model takes them, to spread into a request body.
|
|
312
|
+
*
|
|
313
|
+
* @param refused - What the model has refused, as `negotiate` hands it over. Absent is a model that
|
|
314
|
+
* has refused nothing.
|
|
315
|
+
* @param [extraBody] - The fields to add. Absent adds none.
|
|
316
|
+
* @returns A fresh object of the fields, less the `RESERVED_BODY_FIELDS` and those the model has
|
|
317
|
+
* refused by name.
|
|
318
|
+
*
|
|
319
|
+
* @remarks
|
|
320
|
+
* The keys are what a caller hands `negotiate` as `droppable`, so a field the endpoint has never
|
|
321
|
+
* heard of is latched into `refusedFields` and left out of the next send.
|
|
322
|
+
*/
|
|
323
|
+
export declare const extraFields: (refused: ModelCapabilities | undefined, extraBody?: Record<string, unknown>) => Record<string, unknown>;
|
|
310
324
|
/** What `negotiate` takes besides the request. All optional. */
|
|
311
325
|
export interface NegotiateOptions {
|
|
312
326
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RESERVED_BODY_FIELDS } from '../core/config.js';
|
|
1
2
|
import { getOrCreate } from '../core/guards.js';
|
|
2
3
|
import { scoped } from '../core/scope.js';
|
|
3
4
|
import { errorMessage } from '../wire/errors.js';
|
|
@@ -353,6 +354,20 @@ export const ceilingAndTemperature = (refused, maxTokens, temperature) => {
|
|
|
353
354
|
...(refused?.chosenTemperature === false ? {} : { temperature }),
|
|
354
355
|
};
|
|
355
356
|
};
|
|
357
|
+
/**
|
|
358
|
+
* The request fields an `extraBody` adds as this model takes them, to spread into a request body.
|
|
359
|
+
*
|
|
360
|
+
* @param refused - What the model has refused, as `negotiate` hands it over. Absent is a model that
|
|
361
|
+
* has refused nothing.
|
|
362
|
+
* @param [extraBody] - The fields to add. Absent adds none.
|
|
363
|
+
* @returns A fresh object of the fields, less the `RESERVED_BODY_FIELDS` and those the model has
|
|
364
|
+
* refused by name.
|
|
365
|
+
*
|
|
366
|
+
* @remarks
|
|
367
|
+
* The keys are what a caller hands `negotiate` as `droppable`, so a field the endpoint has never
|
|
368
|
+
* heard of is latched into `refusedFields` and left out of the next send.
|
|
369
|
+
*/
|
|
370
|
+
export const extraFields = (refused, extraBody = {}) => Object.fromEntries(Object.entries(extraBody).filter(([field]) => !RESERVED_BODY_FIELDS.includes(field) && !refused?.refusedFields.has(field)));
|
|
356
371
|
/**
|
|
357
372
|
* How to answer a refused effort *value*, or `undefined` when there is nothing new to learn or
|
|
358
373
|
* nowhere left to go — in which case the refusal is the caller's, as it was before this existed.
|
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';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
|
-
import {
|
|
2
|
+
import type { ModelParams } from '../core/config.ts';
|
|
3
3
|
import { type Capabilities, type ModelCapabilities } from '../endpoint/capabilities.ts';
|
|
4
4
|
import { type ToolOrder } from '../tools/tool-loading.ts';
|
|
5
5
|
/**
|
package/dist/run/request-body.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ceilingAndTemperature, effortFor, } from '../endpoint/capabilities.js';
|
|
1
|
+
import { ceilingAndTemperature, effortFor, extraFields, } from '../endpoint/capabilities.js';
|
|
3
2
|
import { relaxTools, sanitizeTools } from '../endpoint/schema-compat.js';
|
|
4
3
|
import { orderTools } from '../tools/tool-loading.js';
|
|
5
4
|
/**
|
|
@@ -33,12 +32,11 @@ export function buildBody(config, supports, refused, messages, tools = [], order
|
|
|
33
32
|
const sorted = orderTools(tools, order);
|
|
34
33
|
const declared = supports.strictSchemas ? sanitizeTools(sorted) : relaxTools(sanitizeTools(sorted));
|
|
35
34
|
const effort = effortFor(refused, config.reasoningEffort);
|
|
36
|
-
const extra = Object.entries(config.extraBody ?? {}).filter(([field]) => !RESERVED_BODY_FIELDS.includes(field) && !refused?.refusedFields.has(field));
|
|
37
35
|
return {
|
|
38
36
|
...ceilingAndTemperature(refused, config.maxTokens, config.temperature),
|
|
39
37
|
...(effort ? { reasoning_effort: effort } : {}),
|
|
40
38
|
...(supports.usageInStream ? { stream_options: { include_usage: true } } : {}),
|
|
41
|
-
...
|
|
39
|
+
...extraFields(refused, config.extraBody),
|
|
42
40
|
model: config.model,
|
|
43
41
|
messages,
|
|
44
42
|
stream: true,
|
package/dist/spec/spec.d.ts
CHANGED
|
@@ -151,6 +151,8 @@ export interface TaskSpec {
|
|
|
151
151
|
* side task is sent by default; see `SideTaskOptions.reasoningEffort`.
|
|
152
152
|
*/
|
|
153
153
|
reasoningEffort?: string;
|
|
154
|
+
/** Request fields the interface cannot spell, as `model.extraBody` but for this task alone. */
|
|
155
|
+
extraBody?: Record<string, unknown>;
|
|
154
156
|
}
|
|
155
157
|
/** A tool call bound to a lifecycle event, run by the host rather than the model. */
|
|
156
158
|
export interface AgentHook {
|
|
@@ -198,7 +200,7 @@ export interface SpecBundle {
|
|
|
198
200
|
* One side task, resolved: a model, and the endpoint it is reached through.
|
|
199
201
|
*
|
|
200
202
|
* @remarks
|
|
201
|
-
* The
|
|
203
|
+
* The settings are present only where a layer stated them, and nothing is filled in from
|
|
202
204
|
* the agent's own model — `taskCall` turns one into a call, and what is absent there is left to
|
|
203
205
|
* the side task's default.
|
|
204
206
|
*/
|
|
@@ -209,6 +211,8 @@ export interface ResolvedTask {
|
|
|
209
211
|
maxTokens?: number;
|
|
210
212
|
temperature?: number;
|
|
211
213
|
reasoningEffort?: string;
|
|
214
|
+
/** Merged key by key across the layers. `model.extraBody` is the agent's and never reaches it. */
|
|
215
|
+
extraBody?: Record<string, unknown>;
|
|
212
216
|
}
|
|
213
217
|
/**
|
|
214
218
|
* What layered documents resolve to: the flat shape the loop already takes, plus the parts of an
|
package/dist/spec/spec.js
CHANGED
|
@@ -241,10 +241,6 @@ const parseSampling = (report, path, value) => ({
|
|
|
241
241
|
* @param value - What the document held under `model`.
|
|
242
242
|
* @returns The fields that survived — an empty object where none did — or `undefined` where the
|
|
243
243
|
* section is absent or is not an object.
|
|
244
|
-
*
|
|
245
|
-
* @remarks
|
|
246
|
-
* `extraBody` comes back as a new object without the `RESERVED_BODY_FIELDS`, each of which is
|
|
247
|
-
* warned about. An empty one survives as an empty one.
|
|
248
244
|
*/
|
|
249
245
|
function parseModel(report, value) {
|
|
250
246
|
if (absent(value)) {
|
|
@@ -253,31 +249,41 @@ function parseModel(report, value) {
|
|
|
253
249
|
if (!isRecord(value)) {
|
|
254
250
|
return void report.drop('model', 'must be an object');
|
|
255
251
|
}
|
|
256
|
-
let extraBody;
|
|
257
|
-
if (!absent(value.extraBody)) {
|
|
258
|
-
if (!isRecord(value.extraBody)) {
|
|
259
|
-
report.drop('model.extraBody', 'must be an object');
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
extraBody = {};
|
|
263
|
-
for (const [key, held] of Object.entries(value.extraBody)) {
|
|
264
|
-
// `buildBody` would overwrite them anyway; better to say so on import.
|
|
265
|
-
if (RESERVED_BODY_FIELDS.includes(key)) {
|
|
266
|
-
report.drop(`model.extraBody.${key}`, "is the loop's to set");
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
extraBody[key] = held;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
252
|
return kept({
|
|
275
253
|
model: report.string('model.model', value.model),
|
|
276
254
|
...parseSampling(report, 'model', value),
|
|
277
255
|
contextLength: report.number('model.contextLength', value.contextLength, 0),
|
|
278
|
-
extraBody,
|
|
256
|
+
extraBody: parseExtraBody(report, 'model.extraBody', value.extraBody),
|
|
279
257
|
});
|
|
280
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Reads an `extraBody`, the agent's model's or a side task's.
|
|
261
|
+
*
|
|
262
|
+
* @param report - Where what is dropped is written.
|
|
263
|
+
* @param path - Where it sits in the document: `model.extraBody`, or `tasks.title.extraBody`.
|
|
264
|
+
* @param value - What the document held there.
|
|
265
|
+
* @returns A new object without the `RESERVED_BODY_FIELDS`, each of which is warned about, or
|
|
266
|
+
* `undefined` where it is absent or is not an object. An empty one survives as an empty one.
|
|
267
|
+
*/
|
|
268
|
+
function parseExtraBody(report, path, value) {
|
|
269
|
+
if (absent(value)) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
if (!isRecord(value)) {
|
|
273
|
+
return void report.drop(path, 'must be an object');
|
|
274
|
+
}
|
|
275
|
+
const extraBody = {};
|
|
276
|
+
for (const [key, held] of Object.entries(value)) {
|
|
277
|
+
// The request is built around them and they would be overwritten anyway; better to say so on import.
|
|
278
|
+
if (RESERVED_BODY_FIELDS.includes(key)) {
|
|
279
|
+
report.drop(`${path}.${key}`, "is the loop's to set");
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
extraBody[key] = held;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return extraBody;
|
|
286
|
+
}
|
|
281
287
|
/**
|
|
282
288
|
* Reads the `prompt` array, part by part.
|
|
283
289
|
*
|
|
@@ -429,6 +435,7 @@ function parseTasks(report, value) {
|
|
|
429
435
|
model: report.string(`${path}.model`, held.model),
|
|
430
436
|
endpoint: parseEndpoint(report, `${path}.endpoint`, held.endpoint),
|
|
431
437
|
...parseSampling(report, path, held),
|
|
438
|
+
extraBody: parseExtraBody(report, `${path}.extraBody`, held.extraBody),
|
|
432
439
|
});
|
|
433
440
|
}
|
|
434
441
|
return tasks;
|
|
@@ -767,16 +774,18 @@ function mergeById(items, idOf) {
|
|
|
767
774
|
* @param layers - The documents, weakest first. None is written to.
|
|
768
775
|
* @param endpoint - The agent's merged endpoint. Read, not written to.
|
|
769
776
|
* @returns The tasks whose merged `model` is not empty, keyed as the documents keyed them — one a
|
|
770
|
-
* layer declined with `model: ""` is not among them.
|
|
777
|
+
* layer declined with `model: ""` is not among them. A task's `extraBody` merges by key.
|
|
771
778
|
*/
|
|
772
779
|
function resolveTasks(layers, endpoint) {
|
|
773
780
|
const merged = new Map();
|
|
774
781
|
for (const layer of layers) {
|
|
775
782
|
for (const [key, task] of Object.entries(layer.tasks ?? {})) {
|
|
783
|
+
const below = merged.get(key);
|
|
776
784
|
merged.set(key, {
|
|
777
|
-
...
|
|
785
|
+
...below,
|
|
778
786
|
...task,
|
|
779
|
-
endpoint: mergeLayers([
|
|
787
|
+
endpoint: mergeLayers([below?.endpoint, task.endpoint]),
|
|
788
|
+
extraBody: mergeLayers([below?.extraBody, task.extraBody]),
|
|
780
789
|
});
|
|
781
790
|
}
|
|
782
791
|
}
|
|
@@ -793,6 +802,7 @@ function resolveTasks(layers, endpoint) {
|
|
|
793
802
|
maxTokens: task.maxTokens,
|
|
794
803
|
temperature: task.temperature,
|
|
795
804
|
reasoningEffort: task.reasoningEffort,
|
|
805
|
+
extraBody: task.extraBody && said(task.extraBody) ? task.extraBody : undefined,
|
|
796
806
|
});
|
|
797
807
|
}
|
|
798
808
|
return tasks;
|
|
@@ -158,6 +158,8 @@ export interface PreselectOptions {
|
|
|
158
158
|
temperature?: number;
|
|
159
159
|
/** As `SideTaskOptions.reasoningEffort`: absent keeps the no-thinking hints. */
|
|
160
160
|
reasoningEffort?: string;
|
|
161
|
+
/** As `SideTaskOptions.extraBody`: sampler fields the options cannot spell. */
|
|
162
|
+
extraBody?: Record<string, unknown>;
|
|
161
163
|
/**
|
|
162
164
|
* The most tools the choice may name.
|
|
163
165
|
*
|
|
@@ -181,8 +183,8 @@ export interface PreselectOptions {
|
|
|
181
183
|
* `toolSelectModel: settings.toolSelectModel || settings.model`.
|
|
182
184
|
* @param catalog - The servers to choose from.
|
|
183
185
|
* @param prompt - The request being planned for. Only its head is read; see `preselectInput`.
|
|
184
|
-
* @param [options] - Cancellation, notices, the reply ceiling, the temperature
|
|
185
|
-
* as `ask` reads them, the cap the choice is held to, and whether to try the words first.
|
|
186
|
+
* @param [options] - Cancellation, notices, the reply ceiling, the temperature, reasoning effort and
|
|
187
|
+
* extra fields as `ask` reads them, the cap the choice is held to, and whether to try the words first.
|
|
186
188
|
* @returns Names as the catalogue spells them, never more than `maxPerLoad`. Empty where `model` is
|
|
187
189
|
* empty, the catalogue has no tools, the call failed, or its reply named nothing in the catalogue.
|
|
188
190
|
*
|
|
@@ -198,4 +200,4 @@ export interface PreselectOptions {
|
|
|
198
200
|
* starting now and starting in a few seconds. The words have to be clear about it; see
|
|
199
201
|
* `preselectByKeywords` for what that means.
|
|
200
202
|
*/
|
|
201
|
-
export declare function preselect(config: Endpoint, model: string, catalog: CatalogServer[], prompt: string, { signal, onNotice, maxTokens, temperature, reasoningEffort, maxPerLoad, keywords, }?: PreselectOptions): Promise<string[]>;
|
|
203
|
+
export declare function preselect(config: Endpoint, model: string, catalog: CatalogServer[], prompt: string, { signal, onNotice, maxTokens, temperature, reasoningEffort, extraBody, maxPerLoad, keywords, }?: PreselectOptions): Promise<string[]>;
|
package/dist/tools/preselect.js
CHANGED
|
@@ -248,8 +248,8 @@ export function preselectByKeywords(catalog, prompt, { maxPerLoad = MAX_PER_LOAD
|
|
|
248
248
|
* `toolSelectModel: settings.toolSelectModel || settings.model`.
|
|
249
249
|
* @param catalog - The servers to choose from.
|
|
250
250
|
* @param prompt - The request being planned for. Only its head is read; see `preselectInput`.
|
|
251
|
-
* @param [options] - Cancellation, notices, the reply ceiling, the temperature
|
|
252
|
-
* as `ask` reads them, the cap the choice is held to, and whether to try the words first.
|
|
251
|
+
* @param [options] - Cancellation, notices, the reply ceiling, the temperature, reasoning effort and
|
|
252
|
+
* extra fields as `ask` reads them, the cap the choice is held to, and whether to try the words first.
|
|
253
253
|
* @returns Names as the catalogue spells them, never more than `maxPerLoad`. Empty where `model` is
|
|
254
254
|
* empty, the catalogue has no tools, the call failed, or its reply named nothing in the catalogue.
|
|
255
255
|
*
|
|
@@ -265,7 +265,7 @@ export function preselectByKeywords(catalog, prompt, { maxPerLoad = MAX_PER_LOAD
|
|
|
265
265
|
* starting now and starting in a few seconds. The words have to be clear about it; see
|
|
266
266
|
* `preselectByKeywords` for what that means.
|
|
267
267
|
*/
|
|
268
|
-
export async function preselect(config, model, catalog, prompt, { signal, onNotice, maxTokens = 256, temperature, reasoningEffort, maxPerLoad = MAX_PER_LOAD, keywords, } = {}) {
|
|
268
|
+
export async function preselect(config, model, catalog, prompt, { signal, onNotice, maxTokens = 256, temperature, reasoningEffort, extraBody, maxPerLoad = MAX_PER_LOAD, keywords, } = {}) {
|
|
269
269
|
if (!model || !catalog.some((server) => server.tools.length > 0)) {
|
|
270
270
|
return [];
|
|
271
271
|
}
|
|
@@ -285,6 +285,7 @@ export async function preselect(config, model, catalog, prompt, { signal, onNoti
|
|
|
285
285
|
maxTokens,
|
|
286
286
|
temperature,
|
|
287
287
|
reasoningEffort,
|
|
288
|
+
extraBody,
|
|
288
289
|
signal,
|
|
289
290
|
onNotice,
|
|
290
291
|
}), { onNotice, signal });
|
package/dist/turn/side-task.d.ts
CHANGED
|
@@ -55,6 +55,16 @@ export interface SideTaskOptions {
|
|
|
55
55
|
* refused it by value, and `chat_template_kwargs` is left out of the request altogether.
|
|
56
56
|
*/
|
|
57
57
|
reasoningEffort?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Request fields these options cannot spell, as `ModelParams.extraBody` is for the main model:
|
|
60
|
+
* `top_p`, `top_k`, `min_p` and the penalties a model card asks for.
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* Merged in after the ceiling, temperature and thinking fields, so it may override them, and
|
|
64
|
+
* before `askJson`'s `response_format`. `model`, `messages`, `stream` and `tools` are dropped. A
|
|
65
|
+
* field the endpoint refuses by name is latched for the model and the request sent again without it.
|
|
66
|
+
*/
|
|
67
|
+
extraBody?: Record<string, unknown>;
|
|
58
68
|
/** Abandons the call, usually because the run it supports has gone away. */
|
|
59
69
|
signal?: AbortSignal;
|
|
60
70
|
/**
|
|
@@ -97,6 +107,8 @@ export interface SideTask {
|
|
|
97
107
|
temperature?: number;
|
|
98
108
|
/** As `SideTaskOptions.reasoningEffort`: a level replaces the no-thinking hints. */
|
|
99
109
|
reasoningEffort?: string;
|
|
110
|
+
/** As `SideTaskOptions.extraBody`: the sampler fields a model card asks for, sent as given. */
|
|
111
|
+
extraBody?: Record<string, unknown>;
|
|
100
112
|
}
|
|
101
113
|
/**
|
|
102
114
|
* Runs a side task and returns the reply text, thinking stripped. Throws like any request.
|
|
@@ -105,11 +117,38 @@ export interface SideTask {
|
|
|
105
117
|
* @param model - The model to ask, usually smaller than the one running the work.
|
|
106
118
|
* @param system - The instruction.
|
|
107
119
|
* @param user - The input it applies to. Content parts where the model is being shown an image.
|
|
108
|
-
* @param [options] - Reply ceiling, temperature, a reasoning effort, cancellation, notices.
|
|
120
|
+
* @param [options] - Reply ceiling, temperature, a reasoning effort, extra fields, cancellation, notices.
|
|
109
121
|
* @returns The reply's content, trimmed. Where that is empty once the thinking is stripped, a
|
|
110
122
|
* `reasoning_content` the server sent beside it, stripped the same way, or an empty string.
|
|
111
123
|
*/
|
|
112
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>;
|
|
113
152
|
/** What `askJson` takes besides a side task's options. */
|
|
114
153
|
export interface AskJsonOptions extends SideTaskOptions {
|
|
115
154
|
/**
|
|
@@ -138,7 +177,7 @@ export interface AskJsonOptions extends SideTaskOptions {
|
|
|
138
177
|
* @param schema - The JSON Schema of the answer. Its root is held to an object, as a tool's is.
|
|
139
178
|
* @param [options] - A side task's options, plus the schema's `name` and whether it is `strict`.
|
|
140
179
|
* @returns What the reply parsed to, unchecked against the schema: `T` is the caller's claim.
|
|
141
|
-
* `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.
|
|
142
181
|
*
|
|
143
182
|
* @remarks
|
|
144
183
|
* Sends `response_format` with the schema, which a llama.cpp server compiles into a grammar and
|
|
@@ -204,7 +243,7 @@ export declare const clean: (line: string) => string;
|
|
|
204
243
|
export declare const listLines: (text: string, max: number, maxChars: number) => string[];
|
|
205
244
|
/**
|
|
206
245
|
* A side task's settings as the call that honours them: its endpoint, its model, and the options
|
|
207
|
-
* `ask`, `askJson`, `summariser` and `preselect` take last.
|
|
246
|
+
* `ask`, `askReply`, `askJson`, `summariser` and `preselect` take last.
|
|
208
247
|
*
|
|
209
248
|
* @param task - The task, ordinarily `resolved.tasks.<key>`.
|
|
210
249
|
* @param [options] - What the host adds — cancellation, notices, `keywords` — and its own defaults
|
|
@@ -215,10 +254,11 @@ export declare const listLines: (text: string, max: number, maxChars: number) =>
|
|
|
215
254
|
* `endpoint` is the task's own object when no `agent` was given, a copy otherwise.
|
|
216
255
|
*
|
|
217
256
|
* @remarks
|
|
218
|
-
* 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
|
|
219
258
|
* arrives through `options` without a host's call changing. What the task states wins over the
|
|
220
259
|
* same field in `options`: the host's code is written once and the task is what an operator
|
|
221
260
|
* configured over it, so a host passes its own ceiling as the value for a task that names none.
|
|
261
|
+
* `extraBody` is merged key by key, the task's keys winning, as layers merge it.
|
|
222
262
|
* A setting the task leaves out and `options` does too is absent from the result, which is what
|
|
223
263
|
* leaves it to the entry point's own default — 0.3, not the agent's temperature.
|
|
224
264
|
*
|
package/dist/turn/side-task.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { scoped } from '../core/scope.js';
|
|
2
|
-
import { capabilitiesFor, ceilingAndTemperature, EFFORT_NONE, effortFor, modelCapabilitiesFor, negotiate, } from '../endpoint/capabilities.js';
|
|
2
|
+
import { capabilitiesFor, ceilingAndTemperature, EFFORT_NONE, effortFor, extraFields, modelCapabilitiesFor, negotiate, } from '../endpoint/capabilities.js';
|
|
3
3
|
import { endpointId, getClient, modelKey, resolveApiKey, sameUrl } from '../endpoint/client.js';
|
|
4
4
|
import { relaxSchema, sanitizeSchema } from '../endpoint/schema-compat.js';
|
|
5
5
|
import { errorMessage } from '../wire/errors.js';
|
|
@@ -68,11 +68,31 @@ export const resetHints = () => noHints().clear();
|
|
|
68
68
|
* @param model - The model to ask, usually smaller than the one running the work.
|
|
69
69
|
* @param system - The instruction.
|
|
70
70
|
* @param user - The input it applies to. Content parts where the model is being shown an image.
|
|
71
|
-
* @param [options] - Reply ceiling, temperature, a reasoning effort, cancellation, notices.
|
|
71
|
+
* @param [options] - Reply ceiling, temperature, a reasoning effort, extra fields, cancellation, notices.
|
|
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
|
/**
|
|
@@ -86,13 +106,14 @@ export function ask(config, model, system, user, options = {}) {
|
|
|
86
106
|
* @param user - The input, sent as the user message as it was given.
|
|
87
107
|
* @param options - A side task's options. A `reasoningEffort` that is a level leaves the
|
|
88
108
|
* no-thinking hints out; absent, `""` or `"off"` sends them unless this pair has refused them.
|
|
109
|
+
* An `extraBody` goes out less the fields this model has refused by name.
|
|
89
110
|
* @param [format] - Builds the extra body fields from the endpoint's capabilities and the model's.
|
|
90
111
|
* Called on every send, so a re-send is built from what has been latched since. Absent adds none.
|
|
91
|
-
* @returns The first choice's message as `answerOf` reads it, empty when there was no answer
|
|
92
|
-
* 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
|
|
93
114
|
* request that carried the hints or an effort, is retried once without either.
|
|
94
115
|
*/
|
|
95
|
-
async function complete(config, model, system, user, { maxTokens = 512, temperature = 0.3, reasoningEffort, signal, onNotice }, format) {
|
|
116
|
+
async function complete(config, model, system, user, { maxTokens = 512, temperature = 0.3, reasoningEffort, extraBody, signal, onNotice }, format) {
|
|
96
117
|
// The effort the caller chose, or empty where it chose none: `""` and `"off"` read as absent,
|
|
97
118
|
// since off is what the hints below already ask for.
|
|
98
119
|
const level = effortFor(undefined, reasoningEffort);
|
|
@@ -120,6 +141,7 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
|
|
|
120
141
|
// Not gated on `hints`: a model that refuses `chat_template_kwargs` may still read the
|
|
121
142
|
// effort, and the two latches would otherwise contradict each other.
|
|
122
143
|
...(sentEffort ? { reasoning_effort: asked } : {}),
|
|
144
|
+
...extraFields(refused, extraBody),
|
|
123
145
|
...(format && refused ? format(supports, refused) : {}),
|
|
124
146
|
}, { signal });
|
|
125
147
|
};
|
|
@@ -131,6 +153,7 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
|
|
|
131
153
|
const attempt = (hints, effort) => negotiate(supports, (latched, _produced, refused) => send(hints, effort, latched, refused), {
|
|
132
154
|
model,
|
|
133
155
|
onNotice,
|
|
156
|
+
droppable: Object.keys(extraBody ?? {}),
|
|
134
157
|
});
|
|
135
158
|
const key = modelKey(endpointId(config), model);
|
|
136
159
|
// A chosen effort replaces the template hint rather than riding beside it: `enable_thinking:
|
|
@@ -165,7 +188,9 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
|
|
|
165
188
|
modelCapabilitiesFor(supports, model).reasoningEffort = false;
|
|
166
189
|
}
|
|
167
190
|
}
|
|
168
|
-
|
|
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 };
|
|
169
194
|
}
|
|
170
195
|
/**
|
|
171
196
|
* A reply's answer, thinking stripped, or its scratchpad where the answer is nothing else.
|
|
@@ -201,7 +226,7 @@ function answerOf(message) {
|
|
|
201
226
|
* @param schema - The JSON Schema of the answer. Its root is held to an object, as a tool's is.
|
|
202
227
|
* @param [options] - A side task's options, plus the schema's `name` and whether it is `strict`.
|
|
203
228
|
* @returns What the reply parsed to, unchecked against the schema: `T` is the caller's claim.
|
|
204
|
-
* `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.
|
|
205
230
|
*
|
|
206
231
|
* @remarks
|
|
207
232
|
* Sends `response_format` with the schema, which a llama.cpp server compiles into a grammar and
|
|
@@ -214,7 +239,7 @@ function answerOf(message) {
|
|
|
214
239
|
export async function askJson(config, model, system, user, schema, { name = 'answer', strict = true, ...options } = {}) {
|
|
215
240
|
const sanitized = sanitizeSchema(schema);
|
|
216
241
|
const instruction = `${system}\n\nReply with JSON alone, matching this JSON Schema:\n${JSON.stringify(sanitized)}`;
|
|
217
|
-
const
|
|
242
|
+
const { text } = await complete(config, model, instruction, user, options, (supports, refused) => refused.structuredOutput
|
|
218
243
|
? {
|
|
219
244
|
response_format: {
|
|
220
245
|
type: JSON_SCHEMA_FORMAT,
|
|
@@ -226,7 +251,7 @@ export async function askJson(config, model, system, user, schema, { name = 'ans
|
|
|
226
251
|
},
|
|
227
252
|
}
|
|
228
253
|
: {});
|
|
229
|
-
return parseJson(
|
|
254
|
+
return parseJson(text);
|
|
230
255
|
}
|
|
231
256
|
/**
|
|
232
257
|
* Runs a side task and answers `undefined` where it fails, so the work it supports carries on.
|
|
@@ -318,7 +343,7 @@ export const listLines = (text, max, maxChars) => text
|
|
|
318
343
|
.slice(0, max);
|
|
319
344
|
/**
|
|
320
345
|
* A side task's settings as the call that honours them: its endpoint, its model, and the options
|
|
321
|
-
* `ask`, `askJson`, `summariser` and `preselect` take last.
|
|
346
|
+
* `ask`, `askReply`, `askJson`, `summariser` and `preselect` take last.
|
|
322
347
|
*
|
|
323
348
|
* @param task - The task, ordinarily `resolved.tasks.<key>`.
|
|
324
349
|
* @param [options] - What the host adds — cancellation, notices, `keywords` — and its own defaults
|
|
@@ -329,10 +354,11 @@ export const listLines = (text, max, maxChars) => text
|
|
|
329
354
|
* `endpoint` is the task's own object when no `agent` was given, a copy otherwise.
|
|
330
355
|
*
|
|
331
356
|
* @remarks
|
|
332
|
-
* 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
|
|
333
358
|
* arrives through `options` without a host's call changing. What the task states wins over the
|
|
334
359
|
* same field in `options`: the host's code is written once and the task is what an operator
|
|
335
360
|
* configured over it, so a host passes its own ceiling as the value for a task that names none.
|
|
361
|
+
* `extraBody` is merged key by key, the task's keys winning, as layers merge it.
|
|
336
362
|
* A setting the task leaves out and `options` does too is absent from the result, which is what
|
|
337
363
|
* leaves it to the entry point's own default — 0.3, not the agent's temperature.
|
|
338
364
|
*
|
|
@@ -356,6 +382,9 @@ export function taskCall(task, options, agent) {
|
|
|
356
382
|
if (task.reasoningEffort !== undefined) {
|
|
357
383
|
stated.reasoningEffort = task.reasoningEffort;
|
|
358
384
|
}
|
|
385
|
+
if (task.extraBody !== undefined) {
|
|
386
|
+
stated.extraBody = { ...options?.extraBody, ...task.extraBody };
|
|
387
|
+
}
|
|
359
388
|
return {
|
|
360
389
|
endpoint: agent
|
|
361
390
|
? {
|
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",
|