@cubicecho/agent-core 2.22.0 → 2.24.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 +61 -6
- package/dist/agent-loop.d.ts +3 -2
- package/dist/agent-loop.js +385 -303
- package/dist/calibration.js +8 -6
- package/dist/capabilities.d.ts +36 -0
- package/dist/capabilities.js +116 -65
- package/dist/client.d.ts +5 -4
- package/dist/client.js +62 -57
- package/dist/compaction.js +5 -9
- package/dist/continuation.d.ts +4 -4
- package/dist/continuation.js +1 -4
- package/dist/events.d.ts +16 -1
- package/dist/events.js +64 -44
- package/dist/guards.d.ts +22 -0
- package/dist/guards.js +22 -0
- package/dist/hook-events.d.ts +22 -0
- package/dist/hook-events.js +22 -0
- package/dist/hooks.d.ts +15 -16
- package/dist/hooks.js +44 -44
- package/dist/index.d.ts +6 -4
- package/dist/index.js +6 -4
- package/dist/reset.d.ts +6 -2
- package/dist/reset.js +6 -2
- package/dist/retry.d.ts +13 -7
- package/dist/retry.js +14 -7
- package/dist/run-turn.d.ts +8 -1
- package/dist/runtime.d.ts +120 -0
- package/dist/runtime.js +113 -0
- package/dist/schema-compat.d.ts +23 -0
- package/dist/schema-compat.js +76 -59
- package/dist/scope.d.ts +35 -0
- package/dist/scope.js +42 -0
- package/dist/side-task.d.ts +5 -0
- package/dist/side-task.js +24 -52
- package/dist/snapshot.d.ts +2 -2
- package/dist/snapshot.js +27 -48
- package/dist/spec.d.ts +11 -12
- package/dist/spec.js +36 -66
- package/dist/stream.d.ts +7 -0
- package/dist/stream.js +8 -1
- package/dist/thinking.js +6 -4
- package/dist/tokens.d.ts +9 -2
- package/dist/tokens.js +10 -3
- package/dist/tool-calls.d.ts +9 -0
- package/dist/tool-calls.js +11 -6
- package/dist/tool-loading.d.ts +10 -0
- package/dist/tool-loading.js +15 -7
- package/llms.txt +19 -5
- package/package.json +1 -1
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { preselect, runAgentLoop } from "./agent-loop.js";
|
|
2
|
+
import { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.js";
|
|
3
|
+
import { capabilitiesFor, expireCapabilities, resetCapabilities } from "./capabilities.js";
|
|
4
|
+
import { configureClients, contextLimitFor, getClient, listModels, resetClients, servedWindow, } from "./client.js";
|
|
5
|
+
import { compactTranscript, runCompaction, summariser } from "./compaction.js";
|
|
6
|
+
import { continueTurn } from "./continuation.js";
|
|
7
|
+
import { configureEvents, emit, endRun, history, resetEvents, watch, } from "./events.js";
|
|
8
|
+
import { assembleContext, configureHooks, consult, gather, notify, resetHooks, withContext, } from "./hooks.js";
|
|
9
|
+
import { resetAll } from "./reset.js";
|
|
10
|
+
import { runTurn } from "./run-turn.js";
|
|
11
|
+
import { inScope, rootScope } from "./scope.js";
|
|
12
|
+
import { ask, askJson, resetHints, tryAsk } from "./side-task.js";
|
|
13
|
+
import { exportCapabilities, importCapabilities } from "./snapshot.js";
|
|
14
|
+
/**
|
|
15
|
+
* Every exported function that reads or writes what a runtime remembers, itself or through what
|
|
16
|
+
* it calls. The rest of the package is pure, and is the same function whichever runtime it is
|
|
17
|
+
* called beside.
|
|
18
|
+
*/
|
|
19
|
+
const STATEFUL = {
|
|
20
|
+
runAgentLoop,
|
|
21
|
+
preselect,
|
|
22
|
+
runTurn,
|
|
23
|
+
continueTurn,
|
|
24
|
+
ask,
|
|
25
|
+
askJson,
|
|
26
|
+
tryAsk,
|
|
27
|
+
summariser,
|
|
28
|
+
runCompaction,
|
|
29
|
+
compactTranscript,
|
|
30
|
+
getClient,
|
|
31
|
+
listModels,
|
|
32
|
+
contextLimitFor,
|
|
33
|
+
servedWindow,
|
|
34
|
+
configureClients,
|
|
35
|
+
capabilitiesFor,
|
|
36
|
+
expireCapabilities,
|
|
37
|
+
exportCapabilities,
|
|
38
|
+
importCapabilities,
|
|
39
|
+
calibrate,
|
|
40
|
+
charsPerTokenFor,
|
|
41
|
+
emit,
|
|
42
|
+
endRun,
|
|
43
|
+
history,
|
|
44
|
+
watch,
|
|
45
|
+
configureEvents,
|
|
46
|
+
configureHooks,
|
|
47
|
+
gather,
|
|
48
|
+
notify,
|
|
49
|
+
consult,
|
|
50
|
+
assembleContext,
|
|
51
|
+
withContext,
|
|
52
|
+
resetClients,
|
|
53
|
+
resetCapabilities,
|
|
54
|
+
resetCalibration,
|
|
55
|
+
resetHints,
|
|
56
|
+
resetEvents,
|
|
57
|
+
resetHooks,
|
|
58
|
+
resetAll,
|
|
59
|
+
};
|
|
60
|
+
/** Makes the methods of the runtime whose state is `scope`. */
|
|
61
|
+
function bind(scope) {
|
|
62
|
+
const within = (fn) => (...args) => inScope(scope, () => fn(...args));
|
|
63
|
+
const methods = Object.fromEntries(Object.entries(STATEFUL).map(([name, fn]) => [
|
|
64
|
+
name,
|
|
65
|
+
within(fn),
|
|
66
|
+
]));
|
|
67
|
+
return {
|
|
68
|
+
...methods,
|
|
69
|
+
// What it hands back is called later, by `compactTranscript` or by the host, and asks then.
|
|
70
|
+
summariser: (...args) => within(summariser(...args)),
|
|
71
|
+
run: (fn) => inScope(scope, fn),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A runtime of its own: separate clients, listings, capability latches, hints, event bus and hook
|
|
76
|
+
* settings from the process's and from every other runtime's.
|
|
77
|
+
*
|
|
78
|
+
* For a host that must not share them — one that mints a key per tenant, where a shared pool has
|
|
79
|
+
* tenants evicting each other's clients and a shared bus has one tenant's run id readable by
|
|
80
|
+
* another — and for tests, where a runtime per case needs no `resetAll` between them and can run
|
|
81
|
+
* in parallel. A host with one deployment's worth of endpoints does not need one: the top-level
|
|
82
|
+
* functions are the default runtime's methods.
|
|
83
|
+
*
|
|
84
|
+
* The runtime is carried by async context rather than by argument, so `runAgentLoop` called as a
|
|
85
|
+
* method uses this runtime all the way down, and so does a top-level function the host calls from
|
|
86
|
+
* inside it — from `dispatch`, a hook, `onEvent`. Outside a method or `run`, a top-level function
|
|
87
|
+
* is the default runtime's again; a request handler that emits to a run a runtime started calls
|
|
88
|
+
* `runtime.emit`.
|
|
89
|
+
*
|
|
90
|
+
* Nothing needs closing. A runtime is garbage once nothing holds it; its one timer, the event
|
|
91
|
+
* bus's sweep, is unreferenced and stops rescheduling when its last run has been swept.
|
|
92
|
+
*
|
|
93
|
+
* @param options What to configure it with before first use. A part left out keeps the
|
|
94
|
+
* defaults — not the default runtime's settings, which a new runtime does not inherit.
|
|
95
|
+
*/
|
|
96
|
+
export function createRuntime(options = {}) {
|
|
97
|
+
const runtime = bind(new Map());
|
|
98
|
+
if (options.clients)
|
|
99
|
+
runtime.configureClients(options.clients);
|
|
100
|
+
if (options.events)
|
|
101
|
+
runtime.configureEvents(options.events);
|
|
102
|
+
if (options.hooks)
|
|
103
|
+
runtime.configureHooks(options.hooks);
|
|
104
|
+
return runtime;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The runtime the top-level functions use when no other is running.
|
|
108
|
+
*
|
|
109
|
+
* `defaultRuntime.getClient(endpoint)` and `getClient(endpoint)` are the same call outside any
|
|
110
|
+
* runtime. Inside one they differ: the top-level function follows the runtime that is running,
|
|
111
|
+
* and this always reaches the process-wide state.
|
|
112
|
+
*/
|
|
113
|
+
export const defaultRuntime = bind(rootScope);
|
package/dist/schema-compat.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type OpenAI from "openai";
|
|
2
|
+
/**
|
|
3
|
+
* One JSON Schema as a strict server will accept it, for a schema that is not a tool's parameters.
|
|
4
|
+
*
|
|
5
|
+
* What `sanitizeTools` does to each tool, without the tool: a `response_format` schema goes through
|
|
6
|
+
* the same grammar converter a tool's parameters do, and a caller holding only the schema had to
|
|
7
|
+
* wrap it in a definition to get here. Nothing is remembered — the cache is keyed on a tool
|
|
8
|
+
* definition's identity, and a bare schema has no object that stands for it across requests.
|
|
9
|
+
*
|
|
10
|
+
* @param schema The schema as written. Never mutated. Its root is held to an object, as a tool's
|
|
11
|
+
* parameters are, and anything that is not a schema at all comes back as an object with no
|
|
12
|
+
* properties.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sanitizeSchema(schema: unknown): Record<string, unknown>;
|
|
2
15
|
/**
|
|
3
16
|
* Tool definitions a strict server will accept, remembered per definition object.
|
|
4
17
|
*
|
|
@@ -9,6 +22,16 @@ import type OpenAI from "openai";
|
|
|
9
22
|
* changed, a new definition is returned in its place.
|
|
10
23
|
*/
|
|
11
24
|
export declare const sanitizeTools: (tools: OpenAI.ChatCompletionTool[]) => OpenAI.ChatCompletionTool[];
|
|
25
|
+
/**
|
|
26
|
+
* One schema without its `pattern` and `format` keywords, for a schema that is not a tool's.
|
|
27
|
+
*
|
|
28
|
+
* The retry `relaxTools` makes, on a bare schema. Only the keywords go: a property that happens to
|
|
29
|
+
* be called `format` is an argument name and stays.
|
|
30
|
+
*
|
|
31
|
+
* @param schema Already sanitised. Relaxing is the retry, not a substitute for `sanitizeSchema`.
|
|
32
|
+
* Never mutated; anything that is not a schema comes back as an object with no properties.
|
|
33
|
+
*/
|
|
34
|
+
export declare function relaxSchema(schema: unknown): Record<string, unknown>;
|
|
12
35
|
/**
|
|
13
36
|
* The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
|
|
14
37
|
* `pattern` and most `format` values, both of which only ever narrowed a string the tool
|
package/dist/schema-compat.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { isRecord } from "./guards.js";
|
|
2
2
|
/** Lookahead and lookbehind: `(?=`, `(?!`, `(?<=`, `(?<!`. */
|
|
3
3
|
const LOOKAROUND = /\(\?<?[=!]/;
|
|
4
4
|
const PRIMITIVES = new Set(["object", "string", "number", "integer", "boolean", "array", "null"]);
|
|
@@ -20,6 +20,24 @@ const SCHEMA_KEYS = new Set([
|
|
|
20
20
|
]);
|
|
21
21
|
/** Keys whose value is a name -> schema map. */
|
|
22
22
|
const SCHEMA_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions"]);
|
|
23
|
+
/** The two spellings of a schema's own pool of named definitions. */
|
|
24
|
+
const POOL_KEYS = ["definitions", "$defs"];
|
|
25
|
+
/**
|
|
26
|
+
* One keyword's value with `fn` applied wherever it holds a schema, and untouched where it holds
|
|
27
|
+
* data — which is what keeps a walk out of `default`, `enum` and `const`.
|
|
28
|
+
*/
|
|
29
|
+
const mapChildren = (key, value, fn) => {
|
|
30
|
+
if (SCHEMA_KEYS.has(key))
|
|
31
|
+
return Array.isArray(value) ? value.map((sub) => fn(sub)) : fn(value);
|
|
32
|
+
// The keys here are argument names; only the values are schemas.
|
|
33
|
+
if (SCHEMA_MAPS.has(key) && isRecord(value))
|
|
34
|
+
return Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, fn(sub)]));
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
/** The names a schema's `required` lists, without whatever in it is not a name. */
|
|
38
|
+
const requiredOf = (node) => Array.isArray(node.required)
|
|
39
|
+
? node.required.filter((name) => typeof name === "string")
|
|
40
|
+
: [];
|
|
23
41
|
/**
|
|
24
42
|
* Coerces one schema position. Malformed MCP output sometimes puts a bare type name where a
|
|
25
43
|
* whole schema belongs, which the grammar converter reports as `Unrecognized schema: "object"`.
|
|
@@ -29,7 +47,7 @@ function asSchema(node) {
|
|
|
29
47
|
return PRIMITIVES.has(node) && node !== "object" ? { type: node } : EMPTY_OBJECT();
|
|
30
48
|
if (typeof node === "boolean")
|
|
31
49
|
return node;
|
|
32
|
-
if (!
|
|
50
|
+
if (!isRecord(node))
|
|
33
51
|
return EMPTY_OBJECT();
|
|
34
52
|
return normalize(node);
|
|
35
53
|
}
|
|
@@ -50,14 +68,8 @@ function normalize(node) {
|
|
|
50
68
|
else
|
|
51
69
|
out.type = "null";
|
|
52
70
|
}
|
|
53
|
-
else if (SCHEMA_KEYS.has(key)) {
|
|
54
|
-
out[key] = Array.isArray(value) ? value.map(asSchema) : asSchema(value);
|
|
55
|
-
}
|
|
56
|
-
else if (SCHEMA_MAPS.has(key) && isObject(value)) {
|
|
57
|
-
out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, asSchema(sub)]));
|
|
58
|
-
}
|
|
59
71
|
else {
|
|
60
|
-
out[key] = value;
|
|
72
|
+
out[key] = mapChildren(key, value, asSchema);
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
collapseNullableUnion(out);
|
|
@@ -66,7 +78,7 @@ function normalize(node) {
|
|
|
66
78
|
if (typeof out.pattern === "string" && LOOKAROUND.test(out.pattern))
|
|
67
79
|
delete out.pattern;
|
|
68
80
|
// `{"type": "object"}` with no properties produces invalid GBNF.
|
|
69
|
-
if (out.type === "object" && !
|
|
81
|
+
if (out.type === "object" && !isRecord(out.properties))
|
|
70
82
|
out.properties = {};
|
|
71
83
|
// Strict validators reject any sibling of `$ref`, and draft-07 ignores them, so a reference
|
|
72
84
|
// stands alone or not at all. This is not hypothetical tidying: collapsing `anyOf: [{$ref},
|
|
@@ -88,11 +100,11 @@ function collapseNullableUnion(node) {
|
|
|
88
100
|
const variants = node[key];
|
|
89
101
|
if (!Array.isArray(variants))
|
|
90
102
|
continue;
|
|
91
|
-
const concrete = variants.filter((item) => !(
|
|
103
|
+
const concrete = variants.filter((item) => !(isRecord(item) && item.type === "null"));
|
|
92
104
|
if (concrete.length !== 1 || concrete.length === variants.length)
|
|
93
105
|
continue;
|
|
94
106
|
delete node[key];
|
|
95
|
-
Object.assign(node, { nullable: true, ...(
|
|
107
|
+
Object.assign(node, { nullable: true, ...(isRecord(concrete[0]) ? concrete[0] : {}) });
|
|
96
108
|
}
|
|
97
109
|
}
|
|
98
110
|
/** Combinators at the top level of a parameters schema; strict backends reject them outright. */
|
|
@@ -102,8 +114,8 @@ const LOCAL_POINTER = /^#\/(definitions|\$defs)\/([^/]+)$/;
|
|
|
102
114
|
/** The `definitions` and `$defs` that a local pointer in this schema resolves against. */
|
|
103
115
|
function poolsOf(parameters) {
|
|
104
116
|
const defs = {};
|
|
105
|
-
for (const key of
|
|
106
|
-
if (
|
|
117
|
+
for (const key of POOL_KEYS)
|
|
118
|
+
if (isRecord(parameters[key]))
|
|
107
119
|
defs[key] = parameters[key];
|
|
108
120
|
return defs;
|
|
109
121
|
}
|
|
@@ -119,16 +131,16 @@ function poolsOf(parameters) {
|
|
|
119
131
|
function resolveRef(node, defs) {
|
|
120
132
|
const seen = new Set();
|
|
121
133
|
let current = node;
|
|
122
|
-
while (
|
|
134
|
+
while (isRecord(current) && typeof current.$ref === "string") {
|
|
123
135
|
const pointer = current.$ref;
|
|
124
136
|
const target = LOCAL_POINTER.exec(pointer);
|
|
125
137
|
if (!target || seen.has(pointer))
|
|
126
138
|
return undefined;
|
|
127
139
|
seen.add(pointer);
|
|
128
140
|
const pool = defs[target[1]];
|
|
129
|
-
current =
|
|
141
|
+
current = isRecord(pool) ? pool[target[2]] : undefined;
|
|
130
142
|
}
|
|
131
|
-
return
|
|
143
|
+
return isRecord(current) ? current : undefined;
|
|
132
144
|
}
|
|
133
145
|
/**
|
|
134
146
|
* Replaces a root-level `$ref` with what it points at.
|
|
@@ -164,18 +176,16 @@ function mergeRootAllOf(out) {
|
|
|
164
176
|
if (!Array.isArray(branches))
|
|
165
177
|
return;
|
|
166
178
|
const defs = poolsOf(out);
|
|
167
|
-
const properties =
|
|
168
|
-
const required = new Set(
|
|
179
|
+
const properties = isRecord(out.properties) ? { ...out.properties } : {};
|
|
180
|
+
const required = new Set(requiredOf(out));
|
|
169
181
|
for (const raw of branches) {
|
|
170
182
|
const branch = resolveRef(raw, defs);
|
|
171
183
|
if (!branch)
|
|
172
184
|
continue;
|
|
173
|
-
if (
|
|
185
|
+
if (isRecord(branch.properties))
|
|
174
186
|
Object.assign(properties, branch.properties);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (typeof name === "string")
|
|
178
|
-
required.add(name);
|
|
187
|
+
for (const name of requiredOf(branch))
|
|
188
|
+
required.add(name);
|
|
179
189
|
}
|
|
180
190
|
if (!Object.keys(properties).length)
|
|
181
191
|
return;
|
|
@@ -214,22 +224,16 @@ function mergeRootUnion(out) {
|
|
|
214
224
|
shared = new Set();
|
|
215
225
|
continue;
|
|
216
226
|
}
|
|
217
|
-
if (
|
|
227
|
+
if (isRecord(branch.properties))
|
|
218
228
|
Object.assign(properties, branch.properties);
|
|
219
|
-
const names =
|
|
220
|
-
? branch.required.filter((name) => typeof name === "string")
|
|
221
|
-
: [];
|
|
229
|
+
const names = requiredOf(branch);
|
|
222
230
|
shared = previous === null ? new Set(names) : new Set(names.filter((n) => previous.has(n)));
|
|
223
231
|
}
|
|
224
232
|
if (!Object.keys(properties).length)
|
|
225
233
|
continue;
|
|
226
|
-
out.properties = { ...(
|
|
227
|
-
if (shared?.size)
|
|
228
|
-
|
|
229
|
-
? out.required.filter((name) => typeof name === "string")
|
|
230
|
-
: [];
|
|
231
|
-
out.required = [...new Set([...already, ...shared])];
|
|
232
|
-
}
|
|
234
|
+
out.properties = { ...(isRecord(out.properties) ? out.properties : {}), ...properties };
|
|
235
|
+
if (shared?.size)
|
|
236
|
+
out.required = [...new Set([...requiredOf(out), ...shared])];
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
/**
|
|
@@ -246,7 +250,7 @@ function collectRefs(node, into) {
|
|
|
246
250
|
collectRefs(item, into);
|
|
247
251
|
return;
|
|
248
252
|
}
|
|
249
|
-
if (!
|
|
253
|
+
if (!isRecord(node))
|
|
250
254
|
return;
|
|
251
255
|
for (const [key, value] of Object.entries(node)) {
|
|
252
256
|
if (key === "$ref" && typeof value === "string")
|
|
@@ -269,7 +273,7 @@ function collectRefs(node, into) {
|
|
|
269
273
|
* outside it still refers in.
|
|
270
274
|
*/
|
|
271
275
|
function pruneDefs(out) {
|
|
272
|
-
const pools =
|
|
276
|
+
const pools = POOL_KEYS.filter((key) => isRecord(out[key]));
|
|
273
277
|
if (!pools.length)
|
|
274
278
|
return;
|
|
275
279
|
const live = {};
|
|
@@ -282,7 +286,7 @@ function pruneDefs(out) {
|
|
|
282
286
|
continue;
|
|
283
287
|
const [, poolKey, name] = target;
|
|
284
288
|
const pool = out[poolKey];
|
|
285
|
-
if (!
|
|
289
|
+
if (!isRecord(pool) || !(name in pool))
|
|
286
290
|
continue;
|
|
287
291
|
live[poolKey] ??= new Set();
|
|
288
292
|
const names = live[poolKey];
|
|
@@ -317,24 +321,36 @@ function pruneDefs(out) {
|
|
|
317
321
|
function pruneRequired(out) {
|
|
318
322
|
if (!Array.isArray(out.required))
|
|
319
323
|
return;
|
|
320
|
-
const properties =
|
|
324
|
+
const properties = isRecord(out.properties) ? out.properties : {};
|
|
321
325
|
const kept = out.required.filter((name) => typeof name === "string" && name in properties);
|
|
322
326
|
if (kept.length)
|
|
323
327
|
out.required = kept;
|
|
324
328
|
else
|
|
325
329
|
delete out.required;
|
|
326
330
|
}
|
|
327
|
-
|
|
328
|
-
|
|
331
|
+
/**
|
|
332
|
+
* One JSON Schema as a strict server will accept it, for a schema that is not a tool's parameters.
|
|
333
|
+
*
|
|
334
|
+
* What `sanitizeTools` does to each tool, without the tool: a `response_format` schema goes through
|
|
335
|
+
* the same grammar converter a tool's parameters do, and a caller holding only the schema had to
|
|
336
|
+
* wrap it in a definition to get here. Nothing is remembered — the cache is keyed on a tool
|
|
337
|
+
* definition's identity, and a bare schema has no object that stands for it across requests.
|
|
338
|
+
*
|
|
339
|
+
* @param schema The schema as written. Never mutated. Its root is held to an object, as a tool's
|
|
340
|
+
* parameters are, and anything that is not a schema at all comes back as an object with no
|
|
341
|
+
* properties.
|
|
342
|
+
*/
|
|
343
|
+
export function sanitizeSchema(schema) {
|
|
344
|
+
if (!isRecord(schema))
|
|
329
345
|
return EMPTY_OBJECT();
|
|
330
|
-
const out = normalize(inlineRootRef(
|
|
346
|
+
const out = normalize(inlineRootRef(schema));
|
|
331
347
|
mergeRootAllOf(out);
|
|
332
348
|
mergeRootUnion(out);
|
|
333
349
|
for (const key of TOP_LEVEL_COMBINATORS)
|
|
334
350
|
delete out[key];
|
|
335
351
|
if (out.type !== "object")
|
|
336
352
|
out.type = "object";
|
|
337
|
-
if (!
|
|
353
|
+
if (!isRecord(out.properties))
|
|
338
354
|
out.properties = {};
|
|
339
355
|
pruneRequired(out);
|
|
340
356
|
pruneDefs(out);
|
|
@@ -386,7 +402,7 @@ const through = (cache, tools, fn) => tools.map((tool) => {
|
|
|
386
402
|
* @param tools The definitions as the pool hands them over. Never mutated — where a schema
|
|
387
403
|
* changed, a new definition is returned in its place.
|
|
388
404
|
*/
|
|
389
|
-
export const sanitizeTools = (tools) => through(sanitized, tools,
|
|
405
|
+
export const sanitizeTools = (tools) => through(sanitized, tools, sanitizeSchema);
|
|
390
406
|
/**
|
|
391
407
|
* Walked as a schema rather than as arbitrary JSON, because `pattern` and `format` are keyword
|
|
392
408
|
* names and perfectly ordinary argument names at once. Matching on the key alone deleted a
|
|
@@ -394,29 +410,33 @@ export const sanitizeTools = (tools) => through(sanitized, tools, sanitizeParame
|
|
|
394
410
|
* argument that no longer existed — which every strict validator rejects, so the retry produced
|
|
395
411
|
* the failure it was reaching for. The same distinction keeps the walk out of `default`, `enum`
|
|
396
412
|
* and `const`, whose contents are data, not schema.
|
|
397
|
-
*
|
|
398
|
-
* At module scope rather than inside `relaxTools`, so the closure is made once rather than per
|
|
399
|
-
* call — which, on the path this is on, is per request.
|
|
400
413
|
*/
|
|
401
414
|
const strip = (node) => {
|
|
402
415
|
if (Array.isArray(node))
|
|
403
416
|
return node.map(strip);
|
|
404
|
-
if (!
|
|
417
|
+
if (!isRecord(node))
|
|
405
418
|
return node;
|
|
406
419
|
const out = {};
|
|
407
420
|
for (const [key, value] of Object.entries(node)) {
|
|
408
421
|
if (key === "pattern" || key === "format")
|
|
409
422
|
continue;
|
|
410
|
-
|
|
411
|
-
out[key] = Array.isArray(value) ? value.map(strip) : strip(value);
|
|
412
|
-
else if (SCHEMA_MAPS.has(key) && isObject(value))
|
|
413
|
-
// The keys here are argument names; only the values are schemas.
|
|
414
|
-
out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, strip(sub)]));
|
|
415
|
-
else
|
|
416
|
-
out[key] = value;
|
|
423
|
+
out[key] = mapChildren(key, value, strip);
|
|
417
424
|
}
|
|
418
425
|
return out;
|
|
419
426
|
};
|
|
427
|
+
/**
|
|
428
|
+
* One schema without its `pattern` and `format` keywords, for a schema that is not a tool's.
|
|
429
|
+
*
|
|
430
|
+
* The retry `relaxTools` makes, on a bare schema. Only the keywords go: a property that happens to
|
|
431
|
+
* be called `format` is an argument name and stays.
|
|
432
|
+
*
|
|
433
|
+
* @param schema Already sanitised. Relaxing is the retry, not a substitute for `sanitizeSchema`.
|
|
434
|
+
* Never mutated; anything that is not a schema comes back as an object with no properties.
|
|
435
|
+
*/
|
|
436
|
+
export function relaxSchema(schema) {
|
|
437
|
+
const stripped = strip(schema);
|
|
438
|
+
return isRecord(stripped) ? stripped : EMPTY_OBJECT();
|
|
439
|
+
}
|
|
420
440
|
/**
|
|
421
441
|
* The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
|
|
422
442
|
* `pattern` and most `format` values, both of which only ever narrowed a string the tool
|
|
@@ -424,10 +444,7 @@ const strip = (node) => {
|
|
|
424
444
|
*
|
|
425
445
|
* @param tools Already sanitised. Relaxing is the retry, not a substitute for `sanitizeTools`.
|
|
426
446
|
*/
|
|
427
|
-
export const relaxTools = (tools) => through(relaxed, tools,
|
|
428
|
-
const stripped = strip(parameters);
|
|
429
|
-
return isObject(stripped) ? stripped : EMPTY_OBJECT();
|
|
430
|
-
});
|
|
447
|
+
export const relaxTools = (tools) => through(relaxed, tools, relaxSchema);
|
|
431
448
|
/**
|
|
432
449
|
* Qwen chat templates raise this when the transcript has no user turn. Some servers wrap it
|
|
433
450
|
* in the same "unable to generate parser" wording as a real schema failure, and stripping
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything one runtime remembers: each module's state, under a key only that module holds.
|
|
3
|
+
*
|
|
4
|
+
* A map rather than a record of named caches, so a module that grows a cache declares it where
|
|
5
|
+
* it is used and `runtime.ts` does not have to hear about it.
|
|
6
|
+
*/
|
|
7
|
+
export type Scope = Map<symbol, unknown>;
|
|
8
|
+
/** The default runtime's scope: what the top-level functions use when no runtime is running. */
|
|
9
|
+
export declare const rootScope: Scope;
|
|
10
|
+
/** The scope of the runtime whose method is running, or the default one outside any. */
|
|
11
|
+
export declare const currentScope: () => Scope;
|
|
12
|
+
/**
|
|
13
|
+
* Runs `fn` with `scope` as the current one, for it and for everything it awaits or schedules.
|
|
14
|
+
*
|
|
15
|
+
* @param scope The runtime's state. Nested calls replace it for their own duration only.
|
|
16
|
+
* @param fn What to run. Its return value, or what it throws, is handed straight back.
|
|
17
|
+
*/
|
|
18
|
+
export declare const inScope: <T>(scope: Scope, fn: () => T) => T;
|
|
19
|
+
/** One module's state, read from whichever scope is current. See `scoped`. */
|
|
20
|
+
export interface Scoped<T> {
|
|
21
|
+
/** The current scope's copy, made on first use. */
|
|
22
|
+
(): T;
|
|
23
|
+
/** Drops the current scope's copy, so the next read makes a fresh one. */
|
|
24
|
+
reset(): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Declares a piece of module state that every runtime has its own copy of.
|
|
28
|
+
*
|
|
29
|
+
* What used to be `const cache = new Map()` at the top of a module is `scoped(() => new Map())`,
|
|
30
|
+
* and each use reads `cache()`. The copy is made lazily, so a runtime pays only for the modules
|
|
31
|
+
* it touches.
|
|
32
|
+
*
|
|
33
|
+
* @param create Builds a fresh, empty copy. Called once per scope, and again after `reset`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function scoped<T>(create: () => T): Scoped<T>;
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/**
|
|
3
|
+
* Which scope the code running now belongs to, carried across every `await` beneath a runtime's
|
|
4
|
+
* method. Async context rather than a parameter, because the state is read five calls down —
|
|
5
|
+
* `runAgentLoop` to `runTurn` to `negotiate` to `capabilitiesFor` — and threading it would put a
|
|
6
|
+
* runtime argument on every exported function in between.
|
|
7
|
+
*/
|
|
8
|
+
const active = new AsyncLocalStorage();
|
|
9
|
+
/** The default runtime's scope: what the top-level functions use when no runtime is running. */
|
|
10
|
+
export const rootScope = new Map();
|
|
11
|
+
/** The scope of the runtime whose method is running, or the default one outside any. */
|
|
12
|
+
export const currentScope = () => active.getStore() ?? rootScope;
|
|
13
|
+
/**
|
|
14
|
+
* Runs `fn` with `scope` as the current one, for it and for everything it awaits or schedules.
|
|
15
|
+
*
|
|
16
|
+
* @param scope The runtime's state. Nested calls replace it for their own duration only.
|
|
17
|
+
* @param fn What to run. Its return value, or what it throws, is handed straight back.
|
|
18
|
+
*/
|
|
19
|
+
export const inScope = (scope, fn) => active.run(scope, fn);
|
|
20
|
+
/**
|
|
21
|
+
* Declares a piece of module state that every runtime has its own copy of.
|
|
22
|
+
*
|
|
23
|
+
* What used to be `const cache = new Map()` at the top of a module is `scoped(() => new Map())`,
|
|
24
|
+
* and each use reads `cache()`. The copy is made lazily, so a runtime pays only for the modules
|
|
25
|
+
* it touches.
|
|
26
|
+
*
|
|
27
|
+
* @param create Builds a fresh, empty copy. Called once per scope, and again after `reset`.
|
|
28
|
+
*/
|
|
29
|
+
export function scoped(create) {
|
|
30
|
+
const key = Symbol();
|
|
31
|
+
const read = () => {
|
|
32
|
+
const scope = currentScope();
|
|
33
|
+
if (!scope.has(key))
|
|
34
|
+
scope.set(key, create());
|
|
35
|
+
return scope.get(key);
|
|
36
|
+
};
|
|
37
|
+
return Object.assign(read, {
|
|
38
|
+
reset: () => {
|
|
39
|
+
currentScope().delete(key);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
package/dist/side-task.d.ts
CHANGED
|
@@ -126,6 +126,11 @@ export declare function tryAsk<T>(label: string, run: () => Promise<T>, { onNoti
|
|
|
126
126
|
* Models are asked for JSON and often answer with prose around it, or a fenced block. Pull out
|
|
127
127
|
* the first array or object rather than failing the task over a wrapper.
|
|
128
128
|
*
|
|
129
|
+
* What is pulled out is read the way a tool call's arguments are: as written where that parses,
|
|
130
|
+
* and otherwise with the almost-JSON a local model writes repaired — single quotes, `True` and
|
|
131
|
+
* `None`, bare keys, a trailing comma. A small model asked for a list gets these wrong as often in
|
|
132
|
+
* a reply as in a call, and a side task that fails over one costs the run a second request.
|
|
133
|
+
*
|
|
129
134
|
* @param text The reply, fences and prose included. Nothing parseable gives `undefined`.
|
|
130
135
|
*/
|
|
131
136
|
export declare function parseJson<T>(text: string): T | undefined;
|