@cubicecho/agent-core 2.22.0 → 2.23.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 +57 -4
- package/dist/calibration.js +6 -5
- package/dist/capabilities.js +10 -9
- package/dist/client.d.ts +5 -4
- package/dist/client.js +51 -47
- package/dist/events.d.ts +3 -1
- package/dist/events.js +36 -23
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +10 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reset.d.ts +6 -2
- package/dist/reset.js +6 -2
- package/dist/runtime.d.ts +120 -0
- package/dist/runtime.js +113 -0
- package/dist/scope.d.ts +35 -0
- package/dist/scope.js +42 -0
- package/dist/side-task.js +6 -5
- package/llms.txt +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ only, Node >=22.
|
|
|
43
43
|
| `snapshot` | `exportCapabilities` and `importCapabilities`: the latched refusals as a JSON blob a consumer stores, so a restart need not learn them again. |
|
|
44
44
|
| `spec` | `parseSpec`, `resolveAgentSpec` and `exportSpec`: an agent as a versioned JSON document, layered into the flat config the loop takes. Imports nothing but types, and is published separately at `@cubicecho/agent-core/spec`. |
|
|
45
45
|
| `reset` | `resetAll`: drops every cache and latch in one call, so a teardown cannot forget one. |
|
|
46
|
+
| `runtime` | `createRuntime`: a second set of those caches, with the functions that use them as methods — for a host whose tenants must not share a client pool, a latch or an event bus. `defaultRuntime` is the one the top-level functions use. |
|
|
46
47
|
| `tokens` | `estimateTokens`: characters over four, deliberately low, for everything here that has to guess at a window. |
|
|
47
48
|
| `errors` | `errorMessage`: a caught `unknown` turned into something a run row can hold. And what `runAgentLoop` throws, with the run as it stood: `AgentLoopError`, `ToolIterationLimit`, `AgentLoopOverflow`, and `failedRun` to read it off any of them. |
|
|
48
49
|
| `catalog` | `CatalogServer`: the name-only shape `tool-loading` reads a connected server as. |
|
|
@@ -1197,8 +1198,9 @@ const result = await runAgentLoop({ config, preselect: { keywords: true }, compa
|
|
|
1197
1198
|
|
|
1198
1199
|
Four caches outlive any one run: the `OpenAI` clients, the model listings, the latched
|
|
1199
1200
|
capabilities, and `side-task`'s no-thinking hints. A fifth, the characters per token each model was
|
|
1200
|
-
measured at, is keyed on the endpoint's capabilities object, so it goes with them. All four are
|
|
1201
|
-
|
|
1201
|
+
measured at, is keyed on the endpoint's capabilities object, so it goes with them. All four are
|
|
1202
|
+
held once per runtime — once per process, unless you make another, [below](#a-runtime-of-its-own)
|
|
1203
|
+
— and keyed on the same notion of an endpoint — its base URL and its API key — and the clients' key carries the request
|
|
1202
1204
|
timeout as well, since that changes how a request is sent.
|
|
1203
1205
|
|
|
1204
1206
|
**They are keyed per deployment, not per request.** What belongs in them is an endpoint an
|
|
@@ -1209,8 +1211,8 @@ bounding them assumes that, and a consumer that mints an API key per *user* brea
|
|
|
1209
1211
|
|
|
1210
1212
|
That is bounded rather than unbounded: the client pool is a 32-entry LRU, and an evicted client
|
|
1211
1213
|
costs its connection pool and nothing else, since the next request through that endpoint builds
|
|
1212
|
-
another. But churning connection pools is not sharing them, and at that point a
|
|
1213
|
-
|
|
1214
|
+
another. But churning connection pools is not sharing them, and at that point a runtime per
|
|
1215
|
+
tenant, [below](#a-runtime-of-its-own), is the better answer than this.
|
|
1214
1216
|
|
|
1215
1217
|
The listings cache also remembers, for half a minute, that an endpoint did not name a model — the
|
|
1216
1218
|
case where a configured name never matches anything the server serves (a llama.cpp `-a` alias, an
|
|
@@ -1284,6 +1286,57 @@ carries it in the snapshot so an imported latch keeps its real age instead of be
|
|
|
1284
1286
|
every boot. Importing takes the older of the two ages, and a snapshot written before this field
|
|
1285
1287
|
existed reads as met now.
|
|
1286
1288
|
|
|
1289
|
+
### A runtime of its own
|
|
1290
|
+
|
|
1291
|
+
Everything above is the *default runtime*: one set of caches, shared by every caller in the
|
|
1292
|
+
process. `createRuntime` makes another, with its own clients, listings, latches, hints, measured
|
|
1293
|
+
ratios, event bus and hook settings, and the functions that use them as methods of the same names
|
|
1294
|
+
and signatures:
|
|
1295
|
+
|
|
1296
|
+
```ts
|
|
1297
|
+
import { createRuntime } from "@cubicecho/agent-core";
|
|
1298
|
+
|
|
1299
|
+
const runtime = createRuntime({ clients: { maxClients: 8 }, events: { maxEvents: 200 } });
|
|
1300
|
+
|
|
1301
|
+
const run = await runtime.runAgentLoop({ config, messages, dispatch });
|
|
1302
|
+
for await (const event of runtime.watch(runId)) send(event);
|
|
1303
|
+
runtime.resetAll(); // this runtime's caches, and nobody else's
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
It is for the host the section above warns off — one that mints a key per tenant, where a shared
|
|
1307
|
+
pool has tenants evicting each other's clients and a shared bus makes one tenant's run id readable
|
|
1308
|
+
by another — and for tests, where a runtime per case needs no `resetAll` between cases and lets
|
|
1309
|
+
them run in parallel. A host with one deployment's worth of endpoints needs none: the top-level
|
|
1310
|
+
functions are the default runtime's methods, and nothing about them has changed.
|
|
1311
|
+
|
|
1312
|
+
The runtime travels by async context (`AsyncLocalStorage`), not by argument. A method runs its
|
|
1313
|
+
function with the runtime current, and it stays current across every `await` beneath it — so
|
|
1314
|
+
`runtime.runAgentLoop` uses the runtime's clients and latches all the way down, and so does a
|
|
1315
|
+
top-level function your code calls from inside it: an `emit` in `onEvent`, an `ask` in `dispatch`,
|
|
1316
|
+
a `gather` in a hook. `runtime.run(fn)` does the same for code that calls the top-level functions
|
|
1317
|
+
itself:
|
|
1318
|
+
|
|
1319
|
+
```ts
|
|
1320
|
+
await runtime.run(async () => {
|
|
1321
|
+
const limit = await contextLimitFor(config); // the runtime's listing cache
|
|
1322
|
+
emit(runId, { kind: "output", text: "…" }); // the runtime's bus
|
|
1323
|
+
});
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
Outside a method or `run`, a top-level function is the default runtime's again. That is the one
|
|
1327
|
+
thing to keep straight: a request handler that watches or emits to a run a runtime started is not
|
|
1328
|
+
inside it, and calls `runtime.watch` or `runtime.emit`. What a method hands back keeps its runtime
|
|
1329
|
+
— the iterator from `runtime.watch`, the function from `runtime.summariser` — wherever it is read.
|
|
1330
|
+
|
|
1331
|
+
A new runtime starts from the package's defaults plus what `createRuntime` was given; it does not
|
|
1332
|
+
inherit what `configureClients` and the others set on the default one. Nothing needs closing: a
|
|
1333
|
+
runtime is garbage once nothing holds it. `defaultRuntime` is the process's own as an object, for
|
|
1334
|
+
code that takes a `Runtime` and should be handed the shared one.
|
|
1335
|
+
|
|
1336
|
+
The two schema caches (`sanitizeTools`, `relaxTools`) and `requestTokens`' tool-length cache stay
|
|
1337
|
+
process-wide. They are memoised pure functions keyed on the caller's own objects, so there is
|
|
1338
|
+
nothing in them for one tenant to learn about another.
|
|
1339
|
+
|
|
1287
1340
|
## Where the merged behaviour came from
|
|
1288
1341
|
|
|
1289
1342
|
- `schema-compat` — `kanban_server`/`task_server`'s version, which strips **every** sibling of a
|
package/dist/calibration.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CHARS_PER_TOKEN, requestChars, toolsChars } from "./retry.js";
|
|
2
|
+
import { scoped } from "./scope.js";
|
|
2
3
|
/**
|
|
3
4
|
* How many characters a token is worth on one model, learned from what its endpoint reports.
|
|
4
5
|
*
|
|
@@ -27,7 +28,7 @@ const PLAUSIBLE = { least: 1, most: 8 };
|
|
|
27
28
|
* `resetCapabilities` takes its readings with it. Replaced rather than cleared by `resetCalibration`,
|
|
28
29
|
* since a `WeakMap` has no `clear`.
|
|
29
30
|
*/
|
|
30
|
-
|
|
31
|
+
const readings = scoped(() => new WeakMap());
|
|
31
32
|
/**
|
|
32
33
|
* The characters per token to size a request to this model with, `CHARS_PER_TOKEN` until a turn
|
|
33
34
|
* has reported one.
|
|
@@ -43,7 +44,7 @@ let readings = new WeakMap();
|
|
|
43
44
|
* @param model The name the endpoint knows the model as, as it goes in the body.
|
|
44
45
|
*/
|
|
45
46
|
export function charsPerTokenFor(supports, model) {
|
|
46
|
-
const known = readings.get(supports)?.get(model);
|
|
47
|
+
const known = readings().get(supports)?.get(model);
|
|
47
48
|
return known?.length ? Math.max(...known) : CHARS_PER_TOKEN;
|
|
48
49
|
}
|
|
49
50
|
/** Whether any message carries a part whose tokens its characters do not count. */
|
|
@@ -69,10 +70,10 @@ export function calibrate(supports, body, promptTokens) {
|
|
|
69
70
|
if (ratio < PLAUSIBLE.least || ratio > PLAUSIBLE.most) {
|
|
70
71
|
return charsPerTokenFor(supports, body.model);
|
|
71
72
|
}
|
|
72
|
-
let models = readings.get(supports);
|
|
73
|
+
let models = readings().get(supports);
|
|
73
74
|
if (!models) {
|
|
74
75
|
models = new Map();
|
|
75
|
-
readings.set(supports, models);
|
|
76
|
+
readings().set(supports, models);
|
|
76
77
|
}
|
|
77
78
|
const known = models.get(body.model) ?? [];
|
|
78
79
|
known.push(ratio);
|
|
@@ -83,5 +84,5 @@ export function calibrate(supports, body, promptTokens) {
|
|
|
83
84
|
}
|
|
84
85
|
/** Forgets every reading, so the next request is sized at `CHARS_PER_TOKEN` again. */
|
|
85
86
|
export function resetCalibration() {
|
|
86
|
-
readings
|
|
87
|
+
readings.reset();
|
|
87
88
|
}
|
package/dist/capabilities.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { endpointId } from "./client.js";
|
|
2
2
|
import { errorMessage } from "./errors.js";
|
|
3
3
|
import { isGrammarError } from "./schema-compat.js";
|
|
4
|
+
import { scoped } from "./scope.js";
|
|
4
5
|
/**
|
|
5
6
|
* What each endpoint cannot do, remembered for the life of the process.
|
|
6
7
|
*
|
|
@@ -19,7 +20,7 @@ import { isGrammarError } from "./schema-compat.js";
|
|
|
19
20
|
* behind it, including the `models` map underneath, which is the level where two keys through
|
|
20
21
|
* one host are most likely to differ at all.
|
|
21
22
|
*/
|
|
22
|
-
const capabilities = new Map();
|
|
23
|
+
const capabilities = scoped(() => new Map());
|
|
23
24
|
/**
|
|
24
25
|
* What this endpoint is known not to support. The same object every time, so what `negotiate`
|
|
25
26
|
* latches off stays off.
|
|
@@ -64,16 +65,16 @@ export function modelCapabilitiesFor(supports, model) {
|
|
|
64
65
|
return known;
|
|
65
66
|
}
|
|
66
67
|
/** Every endpoint's capabilities by `endpointId`, the live objects, for `exportCapabilities`. */
|
|
67
|
-
export const knownCapabilities = () => capabilities;
|
|
68
|
+
export const knownCapabilities = () => capabilities();
|
|
68
69
|
/**
|
|
69
70
|
* The capabilities of the endpoint with this `endpointId`, created optimistic if unseen — the way
|
|
70
71
|
* `importCapabilities` reaches an endpoint it has only a digest for.
|
|
71
72
|
*/
|
|
72
73
|
export function capabilitiesById(id) {
|
|
73
|
-
let known = capabilities.get(id);
|
|
74
|
+
let known = capabilities().get(id);
|
|
74
75
|
if (!known) {
|
|
75
76
|
known = { strictSchemas: true, usageInStream: true, models: new Map(), since: Date.now() };
|
|
76
|
-
capabilities.set(id, known);
|
|
77
|
+
capabilities().set(id, known);
|
|
77
78
|
}
|
|
78
79
|
return known;
|
|
79
80
|
}
|
|
@@ -92,11 +93,11 @@ export function capabilitiesById(id) {
|
|
|
92
93
|
*/
|
|
93
94
|
export function resetCapabilities(endpoint) {
|
|
94
95
|
if (!endpoint) {
|
|
95
|
-
const held = capabilities.size > 0;
|
|
96
|
-
capabilities.clear();
|
|
96
|
+
const held = capabilities().size > 0;
|
|
97
|
+
capabilities().clear();
|
|
97
98
|
return held;
|
|
98
99
|
}
|
|
99
|
-
return capabilities.delete(endpointId(endpoint));
|
|
100
|
+
return capabilities().delete(endpointId(endpoint));
|
|
100
101
|
}
|
|
101
102
|
/**
|
|
102
103
|
* Forgets every endpoint whose entry is older than this, so the next request finds out again.
|
|
@@ -117,10 +118,10 @@ export function resetCapabilities(endpoint) {
|
|
|
117
118
|
*/
|
|
118
119
|
export function expireCapabilities(maxAgeMs, now = Date.now()) {
|
|
119
120
|
let dropped = 0;
|
|
120
|
-
for (const [id, known] of capabilities) {
|
|
121
|
+
for (const [id, known] of capabilities()) {
|
|
121
122
|
if (now - known.since < maxAgeMs)
|
|
122
123
|
continue;
|
|
123
|
-
capabilities.delete(id);
|
|
124
|
+
capabilities().delete(id);
|
|
124
125
|
dropped++;
|
|
125
126
|
}
|
|
126
127
|
return dropped;
|
package/dist/client.d.ts
CHANGED
|
@@ -27,10 +27,11 @@ export interface ClientPoolOptions {
|
|
|
27
27
|
* Changes what the client pool is held to, for a process whose endpoints are not shaped like the
|
|
28
28
|
* deployments these defaults were chosen for.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
* its size is a deployment's setting, said once at startup. A multi-tenant host
|
|
32
|
-
* per user raises `maxClients` so its tenants stop evicting each other's
|
|
33
|
-
*
|
|
30
|
+
* One setting per runtime for the same reason `configureEvents` is: the pool is one thing per
|
|
31
|
+
* runtime, and its size is a deployment's setting, said once at startup. A multi-tenant host
|
|
32
|
+
* keying on a key per user raises `maxClients` so its tenants stop evicting each other's
|
|
33
|
+
* connection pools, or gives each tenant a `createRuntime` of its own; a dev box shortens
|
|
34
|
+
* `listingMissMs` so a model it has just pulled is seen sooner.
|
|
34
35
|
*
|
|
35
36
|
* A `maxClients` below the pool's current size evicts down to it at once, least recently asked
|
|
36
37
|
* for first, as the next `getClient` would have. A shorter `listingMissMs` applies to misses
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import OpenAI from "openai";
|
|
3
|
+
import { scoped } from "./scope.js";
|
|
3
4
|
/**
|
|
4
5
|
* The SDK insists on a non-empty key even where the server will not look at it. This is what it
|
|
5
6
|
* gets. It also reads as "this endpoint has no key" at a call site, which an empty string does
|
|
@@ -31,7 +32,7 @@ export const timeoutMs = (config) => {
|
|
|
31
32
|
* Bounded, because a cache is only a cache while its keys are. Insertion order is the whole of
|
|
32
33
|
* the eviction: a `Map` iterates oldest-first, and `getClient` re-inserts on a hit.
|
|
33
34
|
*/
|
|
34
|
-
const clients = new Map();
|
|
35
|
+
const clients = scoped(() => new Map());
|
|
35
36
|
/**
|
|
36
37
|
* How many endpoints' clients are kept at once, until `configureClients` moves it.
|
|
37
38
|
*
|
|
@@ -55,7 +56,7 @@ const CLIENT_DEFAULTS = {
|
|
|
55
56
|
listingMissMs: LISTING_MISS_MS,
|
|
56
57
|
};
|
|
57
58
|
/** What is in force now. Read where it is used, so a change applies from the next call. */
|
|
58
|
-
|
|
59
|
+
const poolLimits = scoped(() => ({ ...CLIENT_DEFAULTS }));
|
|
59
60
|
/**
|
|
60
61
|
* Drops the least recently asked-for clients until the pool fits. Nothing is closed on the way
|
|
61
62
|
* out: the SDK holds no handle a caller can release, and an evicted client is garbage once
|
|
@@ -63,20 +64,21 @@ let poolLimits = { ...CLIENT_DEFAULTS };
|
|
|
63
64
|
* of the eviction.
|
|
64
65
|
*/
|
|
65
66
|
const evict = () => {
|
|
66
|
-
for (const oldest of clients.keys()) {
|
|
67
|
-
if (clients.size <= poolLimits.maxClients)
|
|
67
|
+
for (const oldest of clients().keys()) {
|
|
68
|
+
if (clients().size <= poolLimits().maxClients)
|
|
68
69
|
break;
|
|
69
|
-
clients.delete(oldest);
|
|
70
|
+
clients().delete(oldest);
|
|
70
71
|
}
|
|
71
72
|
};
|
|
72
73
|
/**
|
|
73
74
|
* Changes what the client pool is held to, for a process whose endpoints are not shaped like the
|
|
74
75
|
* deployments these defaults were chosen for.
|
|
75
76
|
*
|
|
76
|
-
*
|
|
77
|
-
* its size is a deployment's setting, said once at startup. A multi-tenant host
|
|
78
|
-
* per user raises `maxClients` so its tenants stop evicting each other's
|
|
79
|
-
*
|
|
77
|
+
* One setting per runtime for the same reason `configureEvents` is: the pool is one thing per
|
|
78
|
+
* runtime, and its size is a deployment's setting, said once at startup. A multi-tenant host
|
|
79
|
+
* keying on a key per user raises `maxClients` so its tenants stop evicting each other's
|
|
80
|
+
* connection pools, or gives each tenant a `createRuntime` of its own; a dev box shortens
|
|
81
|
+
* `listingMissMs` so a model it has just pulled is seen sooner.
|
|
80
82
|
*
|
|
81
83
|
* A `maxClients` below the pool's current size evicts down to it at once, least recently asked
|
|
82
84
|
* for first, as the next `getClient` would have. A shorter `listingMissMs` applies to misses
|
|
@@ -91,11 +93,11 @@ const evict = () => {
|
|
|
91
93
|
export function configureClients(options = {}) {
|
|
92
94
|
for (const [name, value] of Object.entries(options)) {
|
|
93
95
|
if (typeof value === "number" && value > 0) {
|
|
94
|
-
poolLimits[name] = value;
|
|
96
|
+
poolLimits()[name] = value;
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
evict();
|
|
98
|
-
return { ...poolLimits };
|
|
100
|
+
return { ...poolLimits() };
|
|
99
101
|
}
|
|
100
102
|
/** How many idle windows the first chunk gets when `firstTokenSeconds` is not given. */
|
|
101
103
|
export const FIRST_TOKEN_FACTOR = 5;
|
|
@@ -132,16 +134,16 @@ export function getClient(config) {
|
|
|
132
134
|
// Stringified rather than joined on a separator: no character is impossible in a URL or a
|
|
133
135
|
// key, and two different endpoints must never resolve to the same cached client.
|
|
134
136
|
const key = JSON.stringify([config.baseUrl, apiKey, timeout]);
|
|
135
|
-
const existing = clients.get(key);
|
|
137
|
+
const existing = clients().get(key);
|
|
136
138
|
if (existing) {
|
|
137
139
|
// Re-inserted so it counts as the youngest. A `Map` keeps insertion order and hands the
|
|
138
140
|
// oldest key over first, which is the whole of the LRU below.
|
|
139
|
-
clients.delete(key);
|
|
140
|
-
clients.set(key, existing);
|
|
141
|
+
clients().delete(key);
|
|
142
|
+
clients().set(key, existing);
|
|
141
143
|
return existing;
|
|
142
144
|
}
|
|
143
145
|
const client = new OpenAI({ baseURL: config.baseUrl, apiKey, timeout, maxRetries: 0 });
|
|
144
|
-
clients.set(key, client);
|
|
146
|
+
clients().set(key, client);
|
|
145
147
|
evict();
|
|
146
148
|
return client;
|
|
147
149
|
}
|
|
@@ -197,14 +199,14 @@ function contextLengthOf(model) {
|
|
|
197
199
|
* It is only ever a cache of something asked for anyway, and a listing that fails leaves
|
|
198
200
|
* whatever was there rather than emptying it.
|
|
199
201
|
*/
|
|
200
|
-
const listings = new Map();
|
|
202
|
+
const listings = scoped(() => new Map());
|
|
201
203
|
/**
|
|
202
204
|
* The models in each endpoint's last listing whose window is only the trained one, by id.
|
|
203
205
|
*
|
|
204
206
|
* Kept beside `listings` rather than on `ModelInfo`, so what `listModels` hands back is the shape
|
|
205
207
|
* it has always been. Replaced with each listing that lands, and left alone by one that fails.
|
|
206
208
|
*/
|
|
207
|
-
const trainedOnly = new Map();
|
|
209
|
+
const trainedOnly = scoped(() => new Map());
|
|
208
210
|
/**
|
|
209
211
|
* When an endpoint was last asked about a model it did not name, keyed on the two together.
|
|
210
212
|
*
|
|
@@ -219,7 +221,7 @@ const trainedOnly = new Map();
|
|
|
219
221
|
* picked up within the minute instead of at the next restart. Bounded by the (endpoint, model)
|
|
220
222
|
* pairs actually asked about, which is the bound the listings themselves have.
|
|
221
223
|
*/
|
|
222
|
-
const misses = new Map();
|
|
224
|
+
const misses = scoped(() => new Map());
|
|
223
225
|
/**
|
|
224
226
|
* What counts as one endpoint, everywhere in this package that has to remember something about
|
|
225
227
|
* one — the model listings, `capabilities`, and the no-thinking hints in `side-task`.
|
|
@@ -257,9 +259,9 @@ export const endpointId = (config) => createHash("sha256").update(endpointKey(co
|
|
|
257
259
|
* A model's entry stays until `resetClients`, like a listing; a server that answered without one
|
|
258
260
|
* is asked again after `listingMissMs`, like a listing that did not name the model.
|
|
259
261
|
*/
|
|
260
|
-
const served = new Map();
|
|
262
|
+
const served = scoped(() => new Map());
|
|
261
263
|
/** Endpoints with neither native route, which are not asked for either again. */
|
|
262
|
-
const unserved = new Set();
|
|
264
|
+
const unserved = scoped(() => new Set());
|
|
263
265
|
/**
|
|
264
266
|
* Endpoints whose `/api/v0/models` answered with an error, and when.
|
|
265
267
|
*
|
|
@@ -271,11 +273,11 @@ const unserved = new Set();
|
|
|
271
273
|
* Per endpoint and on its own, whatever `/props` said: a server whose `/props` answers without a
|
|
272
274
|
* window is never `unserved`, and without this its missing route was asked again with every miss.
|
|
273
275
|
*/
|
|
274
|
-
const refusals = new Map();
|
|
276
|
+
const refusals = scoped(() => new Map());
|
|
275
277
|
/** Whether `/api/v0/models` is to be left alone on this endpoint for now. */
|
|
276
278
|
const refused = (endpoint) => {
|
|
277
|
-
const refusal = refusals.get(endpoint);
|
|
278
|
-
return (refusal !== undefined && (refusal.gone || Date.now() - refusal.at < poolLimits.listingMissMs));
|
|
279
|
+
const refusal = refusals().get(endpoint);
|
|
280
|
+
return (refusal !== undefined && (refusal.gone || Date.now() - refusal.at < poolLimits().listingMissMs));
|
|
279
281
|
};
|
|
280
282
|
/** How long a probe may take before the window is taken from the listing instead. */
|
|
281
283
|
const PROBE_TIMEOUT_MS = 10_000;
|
|
@@ -324,11 +326,11 @@ async function probe(config, path) {
|
|
|
324
326
|
*/
|
|
325
327
|
export async function servedWindow(config) {
|
|
326
328
|
const endpoint = endpointKey(config);
|
|
327
|
-
if (unserved.has(endpoint))
|
|
329
|
+
if (unserved().has(endpoint))
|
|
328
330
|
return 0;
|
|
329
331
|
const key = JSON.stringify([endpoint, config.model]);
|
|
330
|
-
const known = served.get(key);
|
|
331
|
-
if (known && (known.window > 0 || Date.now() - known.at < poolLimits.listingMissMs))
|
|
332
|
+
const known = served().get(key);
|
|
333
|
+
if (known && (known.window > 0 || Date.now() - known.at < poolLimits().listingMissMs))
|
|
332
334
|
return known.window;
|
|
333
335
|
let window = 0;
|
|
334
336
|
try {
|
|
@@ -341,17 +343,17 @@ export async function servedWindow(config) {
|
|
|
341
343
|
if (!refused(endpoint)) {
|
|
342
344
|
const lmstudio = await probe(config, "/api/v0/models");
|
|
343
345
|
if (lmstudio.failed)
|
|
344
|
-
refusals.set(endpoint, { at: Date.now(), gone: lmstudio.missing });
|
|
346
|
+
refusals().set(endpoint, { at: Date.now(), gone: lmstudio.missing });
|
|
345
347
|
else
|
|
346
|
-
refusals.delete(endpoint);
|
|
348
|
+
refusals().delete(endpoint);
|
|
347
349
|
const { data } = (lmstudio.body ?? {});
|
|
348
350
|
const entry = Array.isArray(data)
|
|
349
351
|
? data.find((model) => model?.id === config.model)
|
|
350
352
|
: undefined;
|
|
351
353
|
window = positive(entry?.loaded_context_length);
|
|
352
354
|
}
|
|
353
|
-
if (props.missing && refusals.get(endpoint)?.gone) {
|
|
354
|
-
unserved.add(endpoint);
|
|
355
|
+
if (props.missing && refusals().get(endpoint)?.gone) {
|
|
356
|
+
unserved().add(endpoint);
|
|
355
357
|
return 0;
|
|
356
358
|
}
|
|
357
359
|
}
|
|
@@ -359,7 +361,7 @@ export async function servedWindow(config) {
|
|
|
359
361
|
catch {
|
|
360
362
|
return 0;
|
|
361
363
|
}
|
|
362
|
-
served.set(key, { window, at: Date.now() });
|
|
364
|
+
served().set(key, { window, at: Date.now() });
|
|
363
365
|
return window;
|
|
364
366
|
}
|
|
365
367
|
/**
|
|
@@ -379,8 +381,8 @@ export async function listModels(config) {
|
|
|
379
381
|
})
|
|
380
382
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
381
383
|
const key = endpointKey(config);
|
|
382
|
-
listings.set(key, models);
|
|
383
|
-
trainedOnly.set(key, trained);
|
|
384
|
+
listings().set(key, models);
|
|
385
|
+
trainedOnly().set(key, trained);
|
|
384
386
|
return models;
|
|
385
387
|
}
|
|
386
388
|
/**
|
|
@@ -411,7 +413,9 @@ export async function contextLimitFor(config, declared = 0) {
|
|
|
411
413
|
if (declared > 0)
|
|
412
414
|
return declared;
|
|
413
415
|
const key = endpointKey(config);
|
|
414
|
-
const listed = () => listings
|
|
416
|
+
const listed = () => listings()
|
|
417
|
+
.get(key)
|
|
418
|
+
?.find((model) => model.id === config.model);
|
|
415
419
|
// The listing is asked for again when it does not name this model, rather than only when
|
|
416
420
|
// there is no listing at all. Models arrive after a process starts — an `ollama pull` on a
|
|
417
421
|
// box that has been up a week, a worker added to a router, a name the operator has only just
|
|
@@ -421,11 +425,11 @@ export async function contextLimitFor(config, declared = 0) {
|
|
|
421
425
|
// the window instead of under it and fails against the endpoint's own refusal.
|
|
422
426
|
if (!listed()) {
|
|
423
427
|
const missKey = JSON.stringify([key, config.model]);
|
|
424
|
-
const asked = misses.get(missKey);
|
|
428
|
+
const asked = misses().get(missKey);
|
|
425
429
|
// Asked again, but not on every call. A model that is never coming answers the same zero
|
|
426
430
|
// however often the endpoint is asked, and a caller sizing a window per turn pays a round
|
|
427
431
|
// trip for each of them; `listingMissMs` is how long that answer is allowed to stand.
|
|
428
|
-
if (asked === undefined || Date.now() - asked >= poolLimits.listingMissMs) {
|
|
432
|
+
if (asked === undefined || Date.now() - asked >= poolLimits().listingMissMs) {
|
|
429
433
|
// A failure is not remembered: an endpoint that was down when the last run started is not
|
|
430
434
|
// an endpoint with no models, and a window nobody could ask about is not a failed run.
|
|
431
435
|
const answered = await listModels(config).then(() => true, () => false);
|
|
@@ -435,9 +439,9 @@ export async function contextLimitFor(config, declared = 0) {
|
|
|
435
439
|
// half a minute.
|
|
436
440
|
if (answered) {
|
|
437
441
|
if (listed())
|
|
438
|
-
misses.delete(missKey);
|
|
442
|
+
misses().delete(missKey);
|
|
439
443
|
else
|
|
440
|
-
misses.set(missKey, Date.now());
|
|
444
|
+
misses().set(missKey, Date.now());
|
|
441
445
|
}
|
|
442
446
|
}
|
|
443
447
|
}
|
|
@@ -445,7 +449,7 @@ export async function contextLimitFor(config, declared = 0) {
|
|
|
445
449
|
const fromListing = entry?.contextLength ?? 0;
|
|
446
450
|
// A top-level key settles it. A trained window does not: llama.cpp lists the one the model was
|
|
447
451
|
// built with, and taking it over what `/props` says lets an overflow through the guard.
|
|
448
|
-
if (fromListing > 0 && !trainedOnly.get(key)?.has(config.model))
|
|
452
|
+
if (fromListing > 0 && !trainedOnly().get(key)?.has(config.model))
|
|
449
453
|
return fromListing;
|
|
450
454
|
const window = await servedWindow(config);
|
|
451
455
|
return window > 0 ? window : fromListing;
|
|
@@ -455,12 +459,12 @@ export async function contextLimitFor(config, declared = 0) {
|
|
|
455
459
|
* tests, and for a settings change under test.
|
|
456
460
|
*/
|
|
457
461
|
export function resetClients() {
|
|
458
|
-
poolLimits
|
|
459
|
-
clients.clear();
|
|
460
|
-
listings.clear();
|
|
461
|
-
misses.clear();
|
|
462
|
-
trainedOnly.clear();
|
|
463
|
-
served.clear();
|
|
464
|
-
unserved.clear();
|
|
465
|
-
refusals.clear();
|
|
462
|
+
poolLimits.reset();
|
|
463
|
+
clients().clear();
|
|
464
|
+
listings().clear();
|
|
465
|
+
misses().clear();
|
|
466
|
+
trainedOnly().clear();
|
|
467
|
+
served().clear();
|
|
468
|
+
unserved().clear();
|
|
469
|
+
refusals().clear();
|
|
466
470
|
}
|
package/dist/events.d.ts
CHANGED
|
@@ -50,8 +50,10 @@ export interface EventBusOptions {
|
|
|
50
50
|
* Changes what the bus keeps, for a process whose runs are not shaped like the ones these
|
|
51
51
|
* defaults were chosen for.
|
|
52
52
|
*
|
|
53
|
-
* The bus is one
|
|
53
|
+
* The bus is one thing per runtime rather than an object a caller holds, so this is too: it is
|
|
54
54
|
* a deployment's setting, said once at startup, and not something to move around under a run.
|
|
55
|
+
* Called at the top level that is the process's bus; a runtime made by `createRuntime` has its
|
|
56
|
+
* own, set through its options or its own `configureEvents`.
|
|
55
57
|
* What it costs is memory against how much of a run a late or slow watcher can still read —
|
|
56
58
|
* a server with hundreds of concurrent runs wants a smaller backlog, and one whose tool calls
|
|
57
59
|
* take an hour wants a longer `retainUnendedMs` than the thirty minutes assumed here.
|
package/dist/events.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scoped } from "./scope.js";
|
|
1
2
|
import { LOAD_TOOLS } from "./tool-loading.js";
|
|
2
3
|
/** The numbers a run of the shape this bus was written for wants. */
|
|
3
4
|
const DEFAULTS = {
|
|
@@ -6,14 +7,15 @@ const DEFAULTS = {
|
|
|
6
7
|
retainMs: 60_000,
|
|
7
8
|
retainUnendedMs: 30 * 60_000,
|
|
8
9
|
};
|
|
9
|
-
|
|
10
|
-
let limits = { ...DEFAULTS };
|
|
10
|
+
const bus = scoped(() => ({ limits: { ...DEFAULTS }, streams: new Map(), sweeping: null }));
|
|
11
11
|
/**
|
|
12
12
|
* Changes what the bus keeps, for a process whose runs are not shaped like the ones these
|
|
13
13
|
* defaults were chosen for.
|
|
14
14
|
*
|
|
15
|
-
* The bus is one
|
|
15
|
+
* The bus is one thing per runtime rather than an object a caller holds, so this is too: it is
|
|
16
16
|
* a deployment's setting, said once at startup, and not something to move around under a run.
|
|
17
|
+
* Called at the top level that is the process's bus; a runtime made by `createRuntime` has its
|
|
18
|
+
* own, set through its options or its own `configureEvents`.
|
|
17
19
|
* What it costs is memory against how much of a run a late or slow watcher can still read —
|
|
18
20
|
* a server with hundreds of concurrent runs wants a smaller backlog, and one whose tool calls
|
|
19
21
|
* take an hour wants a longer `retainUnendedMs` than the thirty minutes assumed here.
|
|
@@ -27,14 +29,14 @@ let limits = { ...DEFAULTS };
|
|
|
27
29
|
* @returns Everything in force afterwards, including what this call did not change.
|
|
28
30
|
*/
|
|
29
31
|
export function configureEvents(options = {}) {
|
|
32
|
+
const { limits } = bus();
|
|
30
33
|
for (const [name, value] of Object.entries(options)) {
|
|
31
34
|
if (typeof value === "number" && value > 0)
|
|
32
35
|
limits[name] = value;
|
|
33
36
|
}
|
|
34
37
|
return { ...limits };
|
|
35
38
|
}
|
|
36
|
-
const
|
|
37
|
-
const streamFor = (runId) => {
|
|
39
|
+
const streamFor = ({ streams }, runId) => {
|
|
38
40
|
const existing = streams.get(runId);
|
|
39
41
|
if (existing)
|
|
40
42
|
return existing;
|
|
@@ -60,9 +62,9 @@ const streamFor = (runId) => {
|
|
|
60
62
|
*
|
|
61
63
|
* One timer for the whole map, rescheduled only while there is something in it to expire.
|
|
62
64
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
sweeping = null;
|
|
65
|
+
function sweep(held) {
|
|
66
|
+
const { limits, streams } = held;
|
|
67
|
+
held.sweeping = null;
|
|
66
68
|
const now = Date.now();
|
|
67
69
|
for (const [runId, stream] of streams) {
|
|
68
70
|
if (stream.listeners.size)
|
|
@@ -70,13 +72,13 @@ function sweep() {
|
|
|
70
72
|
if (stream.touched <= now - (stream.ended ? limits.retainMs : limits.retainUnendedMs))
|
|
71
73
|
streams.delete(runId);
|
|
72
74
|
}
|
|
73
|
-
scheduleSweep();
|
|
75
|
+
scheduleSweep(held);
|
|
74
76
|
}
|
|
75
|
-
function scheduleSweep() {
|
|
76
|
-
if (sweeping || streams.size === 0)
|
|
77
|
+
function scheduleSweep(held) {
|
|
78
|
+
if (held.sweeping || held.streams.size === 0)
|
|
77
79
|
return;
|
|
78
|
-
sweeping = setTimeout(sweep, limits.retainMs);
|
|
79
|
-
sweeping.unref?.();
|
|
80
|
+
held.sweeping = setTimeout(() => sweep(held), held.limits.retainMs);
|
|
81
|
+
held.sweeping.unref?.();
|
|
80
82
|
}
|
|
81
83
|
/**
|
|
82
84
|
* Forgets a run that will not be emitting `done` — one whose process is tearing down, or whose
|
|
@@ -86,6 +88,7 @@ function scheduleSweep() {
|
|
|
86
88
|
* @param runId The run to forget. An id nothing was emitted under is ignored.
|
|
87
89
|
*/
|
|
88
90
|
export function endRun(runId) {
|
|
91
|
+
const { streams } = bus();
|
|
89
92
|
const stream = streams.get(runId);
|
|
90
93
|
if (!stream)
|
|
91
94
|
return;
|
|
@@ -106,7 +109,9 @@ export function endRun(runId) {
|
|
|
106
109
|
* @param input The event. `kind` is required; `runId` and `seq` are not a caller's to set.
|
|
107
110
|
*/
|
|
108
111
|
export function emit(runId, input) {
|
|
109
|
-
const
|
|
112
|
+
const held = bus();
|
|
113
|
+
const { limits } = held;
|
|
114
|
+
const stream = streamFor(held, runId);
|
|
110
115
|
// One clock read, used for both the event and the sweep's bookkeeping.
|
|
111
116
|
const at = Date.now();
|
|
112
117
|
const event = {
|
|
@@ -130,7 +135,7 @@ export function emit(runId, input) {
|
|
|
130
135
|
// then dropped: a finished run's record is the row, not this.
|
|
131
136
|
if (event.kind === "done")
|
|
132
137
|
stream.ended = true;
|
|
133
|
-
scheduleSweep();
|
|
138
|
+
scheduleSweep(held);
|
|
134
139
|
for (const listener of stream.listeners) {
|
|
135
140
|
// The guarantee above is the point of this: a listener that throws must not take out the
|
|
136
141
|
// emitter, the listeners after it, or the bookkeeping already done above.
|
|
@@ -155,8 +160,14 @@ export function emit(runId, input) {
|
|
|
155
160
|
* function it is suspended at an `await` rather than at a `yield`, and a `return()` there is
|
|
156
161
|
* queued behind a promise only the next event can settle. An abort resolves that promise itself.
|
|
157
162
|
*/
|
|
158
|
-
export
|
|
159
|
-
|
|
163
|
+
export function watch(runId, signal) {
|
|
164
|
+
// Read here, not in the generator: its body runs when the first event is asked for, and by
|
|
165
|
+
// then the caller may be outside the runtime whose run this is.
|
|
166
|
+
return watching(bus(), runId, signal);
|
|
167
|
+
}
|
|
168
|
+
async function* watching(held, runId, signal) {
|
|
169
|
+
const { limits, streams } = held;
|
|
170
|
+
const stream = streamFor(held, runId);
|
|
160
171
|
// A cursor rather than `shift()`. Draining a backlog an event at a time off the front of an
|
|
161
172
|
// array is a copy of the whole array per event, which on the ten-thousand-delta run this bus
|
|
162
173
|
// is built for is the one quadratic left in the file. The prefix behind the cursor is dropped
|
|
@@ -272,7 +283,7 @@ export async function* watch(runId, signal) {
|
|
|
272
283
|
*
|
|
273
284
|
* @param runId The run to read. An unknown or already-swept run gives an empty array.
|
|
274
285
|
*/
|
|
275
|
-
export const history = (runId) => [...(streams.get(runId)?.events ?? [])];
|
|
286
|
+
export const history = (runId) => [...(bus().streams.get(runId)?.events ?? [])];
|
|
276
287
|
/**
|
|
277
288
|
* Test seam: forget every run, so one test's events cannot be read by the next.
|
|
278
289
|
*
|
|
@@ -286,11 +297,13 @@ export const history = (runId) => [...(streams.get(runId)?.events ?? [])];
|
|
|
286
297
|
* teardown configures again, which is the same line it already wrote once.
|
|
287
298
|
*/
|
|
288
299
|
export const resetEvents = () => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
const held = bus();
|
|
301
|
+
held.streams.clear();
|
|
302
|
+
if (held.sweeping)
|
|
303
|
+
clearTimeout(held.sweeping);
|
|
304
|
+
held.sweeping = null;
|
|
305
|
+
// In place, so a watcher still open reads the defaults too and not the caps it began under.
|
|
306
|
+
Object.assign(held.limits, DEFAULTS);
|
|
294
307
|
};
|
|
295
308
|
/**
|
|
296
309
|
* Consecutive tokens of one kind are one thing being said, not hundreds of things.
|
package/dist/hooks.d.ts
CHANGED
|
@@ -160,8 +160,8 @@ export interface HookOptions {
|
|
|
160
160
|
* Changes what hooks are held to, for a process whose windows are not the size these defaults
|
|
161
161
|
* were chosen for, or whose host wants its own name above the context.
|
|
162
162
|
*
|
|
163
|
-
*
|
|
164
|
-
* settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
|
|
163
|
+
* One setting per runtime for the same reason `configureEvents` is: a budget and a preface are a
|
|
164
|
+
* deployment's settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
|
|
165
165
|
* window can afford more recall than an 8k one — passes `maxTokens` to `gather` instead, and a
|
|
166
166
|
* `preface` passed to `withContext` or `runAgentLoop`'s hooks wins over this one the same way.
|
|
167
167
|
*
|
package/dist/hooks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { errorMessage } from "./errors.js";
|
|
3
|
+
import { scoped } from "./scope.js";
|
|
3
4
|
import { estimateTokens } from "./tokens.js";
|
|
4
5
|
/** Every event a hook can be bound to, in the order a session meets them. */
|
|
5
6
|
export const HOOK_EVENTS = [
|
|
@@ -36,13 +37,13 @@ const HOOK_DEFAULTS = {
|
|
|
36
37
|
preface: HOOK_PREFACE,
|
|
37
38
|
};
|
|
38
39
|
/** What is in force now. Read where it is used, so a change applies from the next request. */
|
|
39
|
-
|
|
40
|
+
const hookSettings = scoped(() => ({ ...HOOK_DEFAULTS }));
|
|
40
41
|
/**
|
|
41
42
|
* Changes what hooks are held to, for a process whose windows are not the size these defaults
|
|
42
43
|
* were chosen for, or whose host wants its own name above the context.
|
|
43
44
|
*
|
|
44
|
-
*
|
|
45
|
-
* settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
|
|
45
|
+
* One setting per runtime for the same reason `configureEvents` is: a budget and a preface are a
|
|
46
|
+
* deployment's settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
|
|
46
47
|
* window can afford more recall than an 8k one — passes `maxTokens` to `gather` instead, and a
|
|
47
48
|
* `preface` passed to `withContext` or `runAgentLoop`'s hooks wins over this one the same way.
|
|
48
49
|
*
|
|
@@ -55,11 +56,11 @@ let hookSettings = { ...HOOK_DEFAULTS };
|
|
|
55
56
|
export function configureHooks(options = {}) {
|
|
56
57
|
const { contextTokens, preface } = options;
|
|
57
58
|
if (typeof contextTokens === "number" && contextTokens > 0) {
|
|
58
|
-
hookSettings.contextTokens = contextTokens;
|
|
59
|
+
hookSettings().contextTokens = contextTokens;
|
|
59
60
|
}
|
|
60
61
|
if (typeof preface === "string")
|
|
61
|
-
hookSettings.preface = preface;
|
|
62
|
-
return { ...hookSettings };
|
|
62
|
+
hookSettings().preface = preface;
|
|
63
|
+
return { ...hookSettings() };
|
|
63
64
|
}
|
|
64
65
|
/**
|
|
65
66
|
* Test seam: puts `configureHooks` back to the defaults, so one test's budget or preface is not the
|
|
@@ -67,14 +68,14 @@ export function configureHooks(options = {}) {
|
|
|
67
68
|
* `resetAll` calls it.
|
|
68
69
|
*/
|
|
69
70
|
export const resetHooks = () => {
|
|
70
|
-
hookSettings
|
|
71
|
+
hookSettings.reset();
|
|
71
72
|
};
|
|
72
73
|
/**
|
|
73
74
|
* The budget a call is held to: its own when it gave a usable one, the process's otherwise. The
|
|
74
75
|
* same rule `configureHooks` applies, so a `0` threaded through for "no opinion" does not quietly
|
|
75
76
|
* turn every hook's context off.
|
|
76
77
|
*/
|
|
77
|
-
const budget = (given) => typeof given === "number" && given > 0 ? given : hookSettings.contextTokens;
|
|
78
|
+
const budget = (given) => typeof given === "number" && given > 0 ? given : hookSettings().contextTokens;
|
|
78
79
|
const attribute = (text) => text.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
79
80
|
/**
|
|
80
81
|
* Builds the context a set of outcomes adds and the notes that go with it.
|
|
@@ -143,7 +144,7 @@ export function assembleContext(outcomes, maxTokens) {
|
|
|
143
144
|
* unless something moved it. Empty says nothing, rather than leaving a blank line where it was.
|
|
144
145
|
* @returns `history` when there was nothing to add or nowhere to add it, otherwise a new array.
|
|
145
146
|
*/
|
|
146
|
-
export function withContext(history, index, context, preface = hookSettings.preface) {
|
|
147
|
+
export function withContext(history, index, context, preface = hookSettings().preface) {
|
|
147
148
|
const message = history[index];
|
|
148
149
|
if (!context || message?.role !== "user")
|
|
149
150
|
return history;
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export { estimateFrom, type LedgerEntry, type LedgerEstimateOptions, type Ledger
|
|
|
24
24
|
export { resetAll } from "./reset.ts";
|
|
25
25
|
export { backoffMs, CHARS_PER_TOKEN, type ContextBreakdown, type ContextBreakdownOptions, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, type TokenEstimateOptions, toolsChars, } from "./retry.ts";
|
|
26
26
|
export { type RunTurnOptions, runTurn } from "./run-turn.ts";
|
|
27
|
+
export { createRuntime, defaultRuntime, type Runtime, type RuntimeOptions } from "./runtime.ts";
|
|
27
28
|
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.ts";
|
|
28
29
|
export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTask, type SideTaskInput, type SideTaskOptions, tryAsk, } from "./side-task.ts";
|
|
29
30
|
export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from "./snapshot.ts";
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ export { estimateFrom, rebaseLedger, recordRequest, tokensBetween, } from "./led
|
|
|
22
22
|
export { resetAll } from "./reset.js";
|
|
23
23
|
export { backoffMs, CHARS_PER_TOKEN, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, toolsChars, } from "./retry.js";
|
|
24
24
|
export { runTurn } from "./run-turn.js";
|
|
25
|
+
export { createRuntime, defaultRuntime } from "./runtime.js";
|
|
25
26
|
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.js";
|
|
26
27
|
export { ask, askJson, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
|
|
27
28
|
export { CAPABILITY_SNAPSHOT_VERSION, exportCapabilities, importCapabilities, } from "./snapshot.js";
|
package/dist/reset.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Forgets everything this package remembers between calls.
|
|
3
3
|
*
|
|
4
|
-
* Six modules here keep state for the life of the
|
|
5
|
-
*
|
|
4
|
+
* Six modules here keep state for the life of the runtime — the process's, unless this is called
|
|
5
|
+
* as a method of one `createRuntime` made, which clears that one alone — each for a good reason
|
|
6
|
+
* and each with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
6
7
|
* out not to take `stream_options` or a grammar, each model's measured characters per token,
|
|
7
8
|
* the models that refused the no-thinking hints, the event bus, and the hooks' configured
|
|
8
9
|
* budget and preface. `resetClients`, `resetCapabilities`, `resetCalibration`, `resetHints`,
|
|
@@ -18,5 +19,8 @@
|
|
|
18
19
|
*
|
|
19
20
|
* It is also the seam that does not need finding again. A seventh module with a cache is a seventh
|
|
20
21
|
* line here, rather than an edit to the teardown of three consumers who will not all notice.
|
|
22
|
+
*
|
|
23
|
+
* A test that builds a `createRuntime` per case has nothing to tear down, and is the better
|
|
24
|
+
* answer where cases run in parallel: this clears state every concurrent caller is sharing.
|
|
21
25
|
*/
|
|
22
26
|
export declare function resetAll(): void;
|
package/dist/reset.js
CHANGED
|
@@ -7,8 +7,9 @@ import { resetHints } from "./side-task.js";
|
|
|
7
7
|
/**
|
|
8
8
|
* Forgets everything this package remembers between calls.
|
|
9
9
|
*
|
|
10
|
-
* Six modules here keep state for the life of the
|
|
11
|
-
*
|
|
10
|
+
* Six modules here keep state for the life of the runtime — the process's, unless this is called
|
|
11
|
+
* as a method of one `createRuntime` made, which clears that one alone — each for a good reason
|
|
12
|
+
* and each with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
12
13
|
* out not to take `stream_options` or a grammar, each model's measured characters per token,
|
|
13
14
|
* the models that refused the no-thinking hints, the event bus, and the hooks' configured
|
|
14
15
|
* budget and preface. `resetClients`, `resetCapabilities`, `resetCalibration`, `resetHints`,
|
|
@@ -24,6 +25,9 @@ import { resetHints } from "./side-task.js";
|
|
|
24
25
|
*
|
|
25
26
|
* It is also the seam that does not need finding again. A seventh module with a cache is a seventh
|
|
26
27
|
* line here, rather than an edit to the teardown of three consumers who will not all notice.
|
|
28
|
+
*
|
|
29
|
+
* A test that builds a `createRuntime` per case has nothing to tear down, and is the better
|
|
30
|
+
* answer where cases run in parallel: this clears state every concurrent caller is sharing.
|
|
27
31
|
*/
|
|
28
32
|
export function resetAll() {
|
|
29
33
|
resetClients();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { preselect, runAgentLoop } from "./agent-loop.ts";
|
|
2
|
+
import { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.ts";
|
|
3
|
+
import { capabilitiesFor, expireCapabilities, resetCapabilities } from "./capabilities.ts";
|
|
4
|
+
import { type ClientPoolOptions, configureClients, contextLimitFor, getClient, listModels, resetClients, servedWindow } from "./client.ts";
|
|
5
|
+
import { compactTranscript, runCompaction, summariser } from "./compaction.ts";
|
|
6
|
+
import { continueTurn } from "./continuation.ts";
|
|
7
|
+
import { configureEvents, type EventBusOptions, emit, endRun, history, resetEvents, watch } from "./events.ts";
|
|
8
|
+
import { assembleContext, configureHooks, consult, gather, type HookOptions, notify, resetHooks, withContext } from "./hooks.ts";
|
|
9
|
+
import { resetAll } from "./reset.ts";
|
|
10
|
+
import { runTurn } from "./run-turn.ts";
|
|
11
|
+
import { ask, askJson, resetHints, tryAsk } from "./side-task.ts";
|
|
12
|
+
import { exportCapabilities, importCapabilities } from "./snapshot.ts";
|
|
13
|
+
/**
|
|
14
|
+
* Every exported function that reads or writes what a runtime remembers, itself or through what
|
|
15
|
+
* it calls. The rest of the package is pure, and is the same function whichever runtime it is
|
|
16
|
+
* called beside.
|
|
17
|
+
*/
|
|
18
|
+
declare const STATEFUL: {
|
|
19
|
+
runAgentLoop: typeof runAgentLoop;
|
|
20
|
+
preselect: typeof preselect;
|
|
21
|
+
runTurn: typeof runTurn;
|
|
22
|
+
continueTurn: typeof continueTurn;
|
|
23
|
+
ask: typeof ask;
|
|
24
|
+
askJson: typeof askJson;
|
|
25
|
+
tryAsk: typeof tryAsk;
|
|
26
|
+
summariser: typeof summariser;
|
|
27
|
+
runCompaction: typeof runCompaction;
|
|
28
|
+
compactTranscript: typeof compactTranscript;
|
|
29
|
+
getClient: typeof getClient;
|
|
30
|
+
listModels: typeof listModels;
|
|
31
|
+
contextLimitFor: typeof contextLimitFor;
|
|
32
|
+
servedWindow: typeof servedWindow;
|
|
33
|
+
configureClients: typeof configureClients;
|
|
34
|
+
capabilitiesFor: typeof capabilitiesFor;
|
|
35
|
+
expireCapabilities: typeof expireCapabilities;
|
|
36
|
+
exportCapabilities: typeof exportCapabilities;
|
|
37
|
+
importCapabilities: typeof importCapabilities;
|
|
38
|
+
calibrate: typeof calibrate;
|
|
39
|
+
charsPerTokenFor: typeof charsPerTokenFor;
|
|
40
|
+
emit: typeof emit;
|
|
41
|
+
endRun: typeof endRun;
|
|
42
|
+
history: typeof history;
|
|
43
|
+
watch: typeof watch;
|
|
44
|
+
configureEvents: typeof configureEvents;
|
|
45
|
+
configureHooks: typeof configureHooks;
|
|
46
|
+
gather: typeof gather;
|
|
47
|
+
notify: typeof notify;
|
|
48
|
+
consult: typeof consult;
|
|
49
|
+
assembleContext: typeof assembleContext;
|
|
50
|
+
withContext: typeof withContext;
|
|
51
|
+
resetClients: typeof resetClients;
|
|
52
|
+
resetCapabilities: typeof resetCapabilities;
|
|
53
|
+
resetCalibration: typeof resetCalibration;
|
|
54
|
+
resetHints: typeof resetHints;
|
|
55
|
+
resetEvents: typeof resetEvents;
|
|
56
|
+
resetHooks: typeof resetHooks;
|
|
57
|
+
resetAll: typeof resetAll;
|
|
58
|
+
};
|
|
59
|
+
/** What a runtime starts out configured with. Every field optional; see `createRuntime`. */
|
|
60
|
+
export interface RuntimeOptions {
|
|
61
|
+
/** The client pool's bounds, as `configureClients` takes them. */
|
|
62
|
+
clients?: ClientPoolOptions;
|
|
63
|
+
/** The event bus's bounds, as `configureEvents` takes them. */
|
|
64
|
+
events?: EventBusOptions;
|
|
65
|
+
/** The hooks' budget and preface, as `configureHooks` takes them. */
|
|
66
|
+
hooks?: HookOptions;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One set of the package's caches, with the functions that use them as methods.
|
|
70
|
+
*
|
|
71
|
+
* Each method is the top-level function of the same name, with the same signature and doc
|
|
72
|
+
* comment, run against this runtime's state instead of the process's: its pooled clients and
|
|
73
|
+
* model listings, latched capabilities, measured characters per token, no-thinking hints, event
|
|
74
|
+
* bus, and hook settings. `resetAll` and the six narrower resets clear this runtime alone.
|
|
75
|
+
*/
|
|
76
|
+
export type Runtime = typeof STATEFUL & {
|
|
77
|
+
/**
|
|
78
|
+
* Runs `fn` with this runtime current, so every top-level function it calls — directly, after
|
|
79
|
+
* an `await`, or from a timer it sets — uses this runtime's state. What the methods do, for
|
|
80
|
+
* code that calls the top-level functions itself.
|
|
81
|
+
*
|
|
82
|
+
* It does not reach a callback that outlives `fn` and is called from elsewhere — a generator
|
|
83
|
+
* resumed later, a function handed back and called by someone else. Call a method there.
|
|
84
|
+
*
|
|
85
|
+
* @param fn What to run. Its return value, or what it throws, is handed straight back.
|
|
86
|
+
*/
|
|
87
|
+
run<T>(fn: () => T): T;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* A runtime of its own: separate clients, listings, capability latches, hints, event bus and hook
|
|
91
|
+
* settings from the process's and from every other runtime's.
|
|
92
|
+
*
|
|
93
|
+
* For a host that must not share them — one that mints a key per tenant, where a shared pool has
|
|
94
|
+
* tenants evicting each other's clients and a shared bus has one tenant's run id readable by
|
|
95
|
+
* another — and for tests, where a runtime per case needs no `resetAll` between them and can run
|
|
96
|
+
* in parallel. A host with one deployment's worth of endpoints does not need one: the top-level
|
|
97
|
+
* functions are the default runtime's methods.
|
|
98
|
+
*
|
|
99
|
+
* The runtime is carried by async context rather than by argument, so `runAgentLoop` called as a
|
|
100
|
+
* method uses this runtime all the way down, and so does a top-level function the host calls from
|
|
101
|
+
* inside it — from `dispatch`, a hook, `onEvent`. Outside a method or `run`, a top-level function
|
|
102
|
+
* is the default runtime's again; a request handler that emits to a run a runtime started calls
|
|
103
|
+
* `runtime.emit`.
|
|
104
|
+
*
|
|
105
|
+
* Nothing needs closing. A runtime is garbage once nothing holds it; its one timer, the event
|
|
106
|
+
* bus's sweep, is unreferenced and stops rescheduling when its last run has been swept.
|
|
107
|
+
*
|
|
108
|
+
* @param options What to configure it with before first use. A part left out keeps the
|
|
109
|
+
* defaults — not the default runtime's settings, which a new runtime does not inherit.
|
|
110
|
+
*/
|
|
111
|
+
export declare function createRuntime(options?: RuntimeOptions): Runtime;
|
|
112
|
+
/**
|
|
113
|
+
* The runtime the top-level functions use when no other is running.
|
|
114
|
+
*
|
|
115
|
+
* `defaultRuntime.getClient(endpoint)` and `getClient(endpoint)` are the same call outside any
|
|
116
|
+
* runtime. Inside one they differ: the top-level function follows the runtime that is running,
|
|
117
|
+
* and this always reaches the process-wide state.
|
|
118
|
+
*/
|
|
119
|
+
export declare const defaultRuntime: Runtime;
|
|
120
|
+
export {};
|
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/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.js
CHANGED
|
@@ -4,6 +4,7 @@ import { endpointId, getClient } from "./client.js";
|
|
|
4
4
|
import { errorMessage } from "./errors.js";
|
|
5
5
|
import { isTransient } from "./retry.js";
|
|
6
6
|
import { relaxTools, sanitizeTools } from "./schema-compat.js";
|
|
7
|
+
import { scoped } from "./scope.js";
|
|
7
8
|
import { stripThinking } from "./thinking.js";
|
|
8
9
|
/**
|
|
9
10
|
* One-shot calls that support a run without being one: picking tools, naming a session,
|
|
@@ -39,13 +40,13 @@ const NO_THINKING = { chat_template_kwargs: { enable_thinking: false } };
|
|
|
39
40
|
* Only the `chat_template_kwargs` half is here. `reasoning_effort` is `negotiate`'s to latch, on
|
|
40
41
|
* the same (endpoint, model) pair, where a run on that model can read it too.
|
|
41
42
|
*/
|
|
42
|
-
const noHints = new Set();
|
|
43
|
+
const noHints = scoped(() => new Set());
|
|
43
44
|
/** An (endpoint, model) pair as `noHints` holds it: `[endpointId, model]`, stringified. */
|
|
44
45
|
export const hintKey = (endpoint, model) => JSON.stringify([endpoint, model]);
|
|
45
46
|
/** The pairs that refused the hints, the live set, for `exportCapabilities` and `importCapabilities`. */
|
|
46
|
-
export const refusedHints = () => noHints;
|
|
47
|
+
export const refusedHints = () => noHints();
|
|
47
48
|
/** Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints. */
|
|
48
|
-
export const resetHints = () => noHints.clear();
|
|
49
|
+
export const resetHints = () => noHints().clear();
|
|
49
50
|
/**
|
|
50
51
|
* Whether a failure is the server complaining about the request, rather than failing to answer.
|
|
51
52
|
*
|
|
@@ -130,7 +131,7 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
|
|
|
130
131
|
const key = hintKey(endpointId(config), model);
|
|
131
132
|
// A chosen effort replaces the template hint rather than riding beside it: `enable_thinking:
|
|
132
133
|
// false` next to `reasoning_effort: "high"` is a request that contradicts itself.
|
|
133
|
-
const hints = !level && !noHints.has(key);
|
|
134
|
+
const hints = !level && !noHints().has(key);
|
|
134
135
|
let response;
|
|
135
136
|
try {
|
|
136
137
|
response = await attempt(hints, true);
|
|
@@ -153,7 +154,7 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
|
|
|
153
154
|
// cannot latch is blamed; if it was the effort after all, the next call is left with only
|
|
154
155
|
// the effort to drop and latches that instead.
|
|
155
156
|
if (hints)
|
|
156
|
-
noHints.add(key);
|
|
157
|
+
noHints().add(key);
|
|
157
158
|
else
|
|
158
159
|
modelCapabilitiesFor(supports, model).reasoningEffort = false;
|
|
159
160
|
}
|
package/llms.txt
CHANGED
|
@@ -222,6 +222,13 @@ One turn, given as many attempts as the caller allows.
|
|
|
222
222
|
- `RunTurnOptions` (type) — A retry is not the same event as a downgrade, but a watcher wants to be told about both.
|
|
223
223
|
- `runTurn` — `request` is a callback rather than a body because the body has to be rebuilt from whatever the last attempt latched off: the tools it sends depend on `strictSchemas`, and `relaxTools` has to apply to the schemas that were just sanitised.
|
|
224
224
|
|
|
225
|
+
### runtime
|
|
226
|
+
|
|
227
|
+
- `createRuntime` — A runtime of its own: separate clients, listings, capability latches, hints, event bus and hook settings from the process's and from every other runtime's.
|
|
228
|
+
- `defaultRuntime` — The runtime the top-level functions use when no other is running.
|
|
229
|
+
- `Runtime` (type) — One set of the package's caches, with the functions that use them as methods.
|
|
230
|
+
- `RuntimeOptions` (type) — What a runtime starts out configured with.
|
|
231
|
+
|
|
225
232
|
### schema-compat
|
|
226
233
|
|
|
227
234
|
JSON Schema compatibility for llama.cpp-backed servers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.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",
|