@cubicecho/agent-core 2.21.1 → 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 +102 -6
- package/dist/agent-loop.d.ts +89 -3
- package/dist/agent-loop.js +123 -6
- 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 +3 -2
- package/dist/index.js +2 -1
- 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.d.ts +35 -2
- package/dist/side-task.js +25 -13
- package/dist/spec.d.ts +13 -1
- package/llms.txt +9 -0
- package/package.json +1 -1
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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* prompts, and whatever the run is about — because that is the caller's, and it is the part
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
12
|
-
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopRequest, type AgentLoopResult, buildBody, preselect, preview, resolveApiKey, runAgentLoop, type StepWindow, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult, } from "./agent-loop.ts";
|
|
12
|
+
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopRequest, type AgentLoopResult, buildBody, preselect, preview, resolveApiKey, runAgentLoop, type StepWindow, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult, taskCall, } from "./agent-loop.ts";
|
|
13
13
|
export { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.ts";
|
|
14
14
|
export { type Capabilities, capabilitiesFor, EFFORT_LADDER, effortFor, expireCapabilities, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
15
15
|
export type { CatalogServer } from "./catalog.ts";
|
|
@@ -24,8 +24,9 @@ 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
|
-
export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTaskInput, type SideTaskOptions, tryAsk, } from "./side-task.ts";
|
|
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";
|
|
30
31
|
export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, type AgentHook, type AgentSpec, type EndpointSpec, type ExportSpecOptions, exportSpec, type ModelSpec, type ParsedSpec, type ParseSpecOptions, type PromptPart, parseSpec, RESOLVED_DEFAULTS, type ResolvedAgent, type ResolvedTask, type RetrySpec, resolveAgentSpec, SPEC_EVENTS, type SpecBundle, type SpecServer, type TaskSpec, type ToolsSpec, } from "./spec.ts";
|
|
31
32
|
export { type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from "./stream.ts";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* prompts, and whatever the run is about — because that is the caller's, and it is the part
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
12
|
-
export { buildBody, preselect, preview, resolveApiKey, runAgentLoop, } from "./agent-loop.js";
|
|
12
|
+
export { buildBody, preselect, preview, resolveApiKey, runAgentLoop, taskCall, } from "./agent-loop.js";
|
|
13
13
|
export { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.js";
|
|
14
14
|
export { capabilitiesFor, EFFORT_LADDER, effortFor, expireCapabilities, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
15
15
|
export { configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.js";
|
|
@@ -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();
|