@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.
- package/README.md +14 -9
- package/dist/agent-loop.d.ts +84 -204
- package/dist/agent-loop.js +416 -643
- package/dist/calibration.d.ts +14 -10
- package/dist/calibration.js +30 -22
- package/dist/capabilities.d.ts +140 -35
- package/dist/capabilities.js +345 -152
- package/dist/client.d.ts +104 -35
- package/dist/client.js +210 -78
- package/dist/compaction.d.ts +103 -60
- package/dist/compaction.js +163 -93
- package/dist/config.d.ts +48 -12
- package/dist/config.js +20 -1
- package/dist/continuation.d.ts +25 -17
- package/dist/continuation.js +70 -47
- package/dist/digest.d.ts +7 -0
- package/dist/digest.js +18 -0
- package/dist/errors.d.ts +24 -15
- package/dist/errors.js +23 -15
- package/dist/events.d.ts +108 -58
- package/dist/events.js +265 -128
- package/dist/guards.d.ts +53 -0
- package/dist/guards.js +55 -0
- package/dist/hook-events.d.ts +38 -0
- package/dist/hook-events.js +36 -0
- package/dist/hooks.d.ts +105 -83
- package/dist/hooks.js +224 -139
- package/dist/index.d.ts +30 -25
- package/dist/index.js +29 -23
- package/dist/ledger.d.ts +62 -30
- package/dist/ledger.js +163 -56
- package/dist/platform.d.ts +13 -0
- package/dist/platform.js +13 -0
- package/dist/preselect.d.ts +222 -0
- package/dist/preselect.js +290 -0
- package/dist/request-body.d.ts +32 -0
- package/dist/request-body.js +47 -0
- package/dist/reset.d.ts +1 -0
- package/dist/reset.js +7 -6
- package/dist/retry.d.ts +40 -143
- package/dist/retry.js +79 -285
- package/dist/run-calls.d.ts +123 -0
- package/dist/run-calls.js +351 -0
- package/dist/run-turn.d.ts +44 -16
- package/dist/run-turn.js +74 -37
- package/dist/runtime.d.ts +23 -17
- package/dist/runtime.js +34 -24
- package/dist/schema-compat.d.ts +51 -11
- package/dist/schema-compat.js +375 -193
- package/dist/scope.d.ts +28 -5
- package/dist/scope.js +37 -10
- package/dist/side-task.d.ts +127 -39
- package/dist/side-task.js +189 -100
- package/dist/snapshot.d.ts +12 -7
- package/dist/snapshot.js +87 -65
- package/dist/spec.d.ts +66 -36
- package/dist/spec.js +514 -265
- package/dist/stream.d.ts +73 -16
- package/dist/stream.js +228 -98
- package/dist/thinking.d.ts +23 -8
- package/dist/thinking.js +96 -46
- package/dist/tokens.d.ts +171 -6
- package/dist/tokens.js +326 -6
- package/dist/tool-calls.d.ts +89 -15
- package/dist/tool-calls.js +205 -85
- package/dist/tool-loading.d.ts +109 -187
- package/dist/tool-loading.js +286 -372
- package/dist/wire.d.ts +81 -0
- package/dist/wire.js +73 -0
- package/llms.txt +87 -43
- package/package.json +4 -4
package/dist/side-task.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import OpenAI from
|
|
2
|
-
import { capabilitiesFor, effortFor, modelCapabilitiesFor, negotiate, } from
|
|
3
|
-
import { endpointId, getClient } from
|
|
4
|
-
import { errorMessage } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { scoped } from
|
|
8
|
-
import { stripThinking } from
|
|
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
|
-
*
|
|
50
|
+
* The pairs that refused the hints, the live set, for `exportCapabilities` and
|
|
51
|
+
* `importCapabilities`.
|
|
52
52
|
*
|
|
53
|
-
* The
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*
|
|
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
|
-
*
|
|
62
|
-
* is
|
|
61
|
+
* @remarks
|
|
62
|
+
* The current scope's set is emptied in place, so a holder of it sees it empty.
|
|
63
63
|
*/
|
|
64
|
-
|
|
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 ||
|
|
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.
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
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:
|
|
113
|
-
{ role:
|
|
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 =
|
|
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) || !
|
|
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
|
-
|
|
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 ===
|
|
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 =
|
|
192
|
-
const
|
|
193
|
-
|
|
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:
|
|
221
|
+
type: JSON_SCHEMA_FORMAT,
|
|
203
222
|
json_schema: {
|
|
204
223
|
name,
|
|
205
224
|
strict,
|
|
206
|
-
schema:
|
|
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
|
-
*
|
|
236
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ export interface ModelSnapshot {
|
|
|
16
16
|
refusedFields: string[];
|
|
17
17
|
structuredOutput: boolean;
|
|
18
18
|
/**
|
|
19
|
-
* Continues a trailing assistant message.
|
|
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
|
|
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;
|