@cubicecho/agent-core 2.8.1 → 2.10.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 +31 -0
- package/dist/agent-loop.d.ts +1 -1
- package/dist/client.d.ts +41 -5
- package/dist/client.js +69 -21
- package/dist/hooks.d.ts +24 -14
- package/dist/hooks.js +36 -26
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/reset.d.ts +3 -3
- package/dist/reset.js +3 -3
- package/llms.txt +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -504,6 +504,18 @@ Either one given something that is not a number above zero keeps what was there,
|
|
|
504
504
|
`configureEvents` does, so a `0` threaded through for "no opinion" does not switch recall off.
|
|
505
505
|
`resetHooks` (and `resetAll`) puts the default back.
|
|
506
506
|
|
|
507
|
+
The preface said above the blocks moves the same way. `HOOK_PREFACE` names no host, so a host that
|
|
508
|
+
wants its own name says so once with `configureHooks({ preface })`, and a `preface` passed to
|
|
509
|
+
`withContext` (or on `runAgentLoop`'s `hooks`) wins over it for one request. An empty string is a
|
|
510
|
+
preface of nothing — the blocks lead the question on their own, with no blank line above them —
|
|
511
|
+
and anything that is not a string keeps what was there:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
configureHooks({ preface: "Added by my-host's hooks — background, not the user's words:" });
|
|
515
|
+
const request = withContext(messages, messages.length - 1, gathered.context); // says it
|
|
516
|
+
withContext(messages, messages.length - 1, gathered.context, ""); // says nothing
|
|
517
|
+
```
|
|
518
|
+
|
|
507
519
|
Neither function rejects. A hook failing is an outcome, and a runner that throws outright is
|
|
508
520
|
noted once for its event and costs only that event's context. `notify` takes no signal: a reader
|
|
509
521
|
who leaves once the turn is answered has not asked for it not to be remembered.
|
|
@@ -573,6 +585,25 @@ OpenRouter `:free` suffix, a typo) would otherwise fetch the listing on every ca
|
|
|
573
585
|
same zero each time. Half a minute later it asks again, so a model pulled onto a box that has been
|
|
574
586
|
up a week is still picked up without a restart.
|
|
575
587
|
|
|
588
|
+
Both numbers are defaults. `configureClients` moves them for the process, the way `configureEvents`
|
|
589
|
+
and `configureHooks` do — a multi-tenant host raising the pool so tenants stop evicting each other,
|
|
590
|
+
a dev box shortening the miss window so a model it has just pulled shows up sooner:
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
import { configureClients } from "@cubicecho/agent-core";
|
|
594
|
+
|
|
595
|
+
configureClients({ maxClients: 256, listingMissMs: 2_000 });
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
A field left out, or given anything that is not a number above zero, keeps what it has, and the
|
|
599
|
+
call returns everything in force. Lowering `maxClients` below the pool's current size evicts down to
|
|
600
|
+
it at once, least recently used first. `resetClients` (and `resetAll`) puts the defaults back.
|
|
601
|
+
|
|
602
|
+
A host that keeps per-endpoint state of its own should key it on `endpointKey` — the JSON of the base
|
|
603
|
+
URL and the key, an absent key read as `NO_KEY` — rather than rebuilding that string, so the two
|
|
604
|
+
cannot drift. `endpointKey` holds the key in the clear; `endpointId`, its SHA-256 digest, is the one
|
|
605
|
+
that is safe to write down.
|
|
606
|
+
|
|
576
607
|
`resetAll` drops all four, and `reset.ts` names each seam separately for a test that wants one.
|
|
577
608
|
|
|
578
609
|
The latches can outlive the process as well, because otherwise every restart spends one refused
|
package/dist/agent-loop.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ export interface AgentLoopHooks {
|
|
|
105
105
|
events?: readonly HookEvent[];
|
|
106
106
|
/** The shared context budget. Absent is `configureHooks`'s. */
|
|
107
107
|
maxTokens?: number;
|
|
108
|
-
/** Said above the context blocks. Absent is `
|
|
108
|
+
/** Said above the context blocks. Absent is `configureHooks`'s; empty is none. */
|
|
109
109
|
preface?: string;
|
|
110
110
|
/** Hears each note, from before the request and from `afterTurn`. */
|
|
111
111
|
onNote?: (note: HookNote) => void;
|
package/dist/client.d.ts
CHANGED
|
@@ -12,6 +12,36 @@ export declare const NO_KEY = "agent-core";
|
|
|
12
12
|
* @param config Read for `requestTimeoutSeconds` alone.
|
|
13
13
|
*/
|
|
14
14
|
export declare const timeoutMs: (config: Pick<Endpoint, "requestTimeoutSeconds">) => number | undefined;
|
|
15
|
+
/** What the client pool is held to across a process. Every field optional; see `configureClients`. */
|
|
16
|
+
export interface ClientPoolOptions {
|
|
17
|
+
/** How many endpoints' clients are kept at once. The least recently asked for goes first. */
|
|
18
|
+
maxClients?: number;
|
|
19
|
+
/**
|
|
20
|
+
* How long, in milliseconds, a model an endpoint did not list — or a server that answered
|
|
21
|
+
* without a served window — is taken at its word before the endpoint is asked again.
|
|
22
|
+
*/
|
|
23
|
+
listingMissMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Changes what the client pool is held to, for a process whose endpoints are not shaped like the
|
|
27
|
+
* deployments these defaults were chosen for.
|
|
28
|
+
*
|
|
29
|
+
* Module-level for the same reason `configureEvents` is: the pool is one module-level thing, and
|
|
30
|
+
* its size is a deployment's setting, said once at startup. A multi-tenant host keying on a key
|
|
31
|
+
* per user raises `maxClients` so its tenants stop evicting each other's connection pools; a dev
|
|
32
|
+
* box shortens `listingMissMs` so a model it has just pulled is seen sooner.
|
|
33
|
+
*
|
|
34
|
+
* A `maxClients` below the pool's current size evicts down to it at once, least recently asked
|
|
35
|
+
* for first, as the next `getClient` would have. A shorter `listingMissMs` applies to misses
|
|
36
|
+
* already remembered, since each is a timestamp compared against it when read.
|
|
37
|
+
*
|
|
38
|
+
* @param options The bounds to change. A field left out — or given anything that is not a number
|
|
39
|
+
* above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
|
|
40
|
+
* above zero: as `maxClients` it lifts the bound, and as `listingMissMs` a miss is never asked
|
|
41
|
+
* about again until `resetClients`.
|
|
42
|
+
* @returns Everything in force afterwards, including what this call did not change.
|
|
43
|
+
*/
|
|
44
|
+
export declare function configureClients(options?: ClientPoolOptions): Required<ClientPoolOptions>;
|
|
15
45
|
/** How many idle windows the first chunk gets when `firstTokenSeconds` is not given. */
|
|
16
46
|
export declare const FIRST_TOKEN_FACTOR = 5;
|
|
17
47
|
/**
|
|
@@ -29,7 +59,7 @@ export declare const firstTokenMs: (config: Pick<Endpoint, "requestTimeoutSecond
|
|
|
29
59
|
* The pool is per *deployment*, not per request: what belongs in this map is an endpoint an
|
|
30
60
|
* operator configured, and everything cached in this package is bounded on that reading. A
|
|
31
61
|
* consumer that mints an API key per user still gets a working client, but it is churning
|
|
32
|
-
* connection pools rather than sharing them and holding every one of them — `
|
|
62
|
+
* connection pools rather than sharing them and holding every one of them — `maxClients` keeps
|
|
33
63
|
* that from being unbounded, and it is the point at which a client of your own, built and held
|
|
34
64
|
* per tenant, is the better answer than this.
|
|
35
65
|
*
|
|
@@ -43,7 +73,7 @@ export interface ModelInfo {
|
|
|
43
73
|
}
|
|
44
74
|
/**
|
|
45
75
|
* What counts as one endpoint, everywhere in this package that has to remember something about
|
|
46
|
-
* one —
|
|
76
|
+
* one — the model listings, `capabilities`, and the no-thinking hints in `side-task`.
|
|
47
77
|
*
|
|
48
78
|
* The URL and the key together, because the key is part of what is on the other end rather than
|
|
49
79
|
* only how it is paid for: a router is free to send two keys to two different backends, and then
|
|
@@ -51,7 +81,9 @@ export interface ModelInfo {
|
|
|
51
81
|
* endpoint with no key and one that passes `undefined` are one entry rather than two.
|
|
52
82
|
*
|
|
53
83
|
* Stringified rather than joined on a separator, for the reason `getClient` gives: no character
|
|
54
|
-
* is impossible in a URL or a key, and two endpoints must never collide on one entry.
|
|
84
|
+
* is impossible in a URL or a key, and two endpoints must never collide on one entry. A host
|
|
85
|
+
* keeping its own per-endpoint state keys it on this rather than on a copy of it, so the two
|
|
86
|
+
* cannot drift apart. It holds the key in the clear; `endpointId` is the one to write down.
|
|
55
87
|
*
|
|
56
88
|
* @param config Read for `baseUrl` and `apiKey` alone, and the key is optional here where
|
|
57
89
|
* `Endpoint` requires it — `capabilitiesFor` is handed a URL and maybe a key rather than a whole
|
|
@@ -67,7 +99,8 @@ export declare const endpointKey: (config: {
|
|
|
67
99
|
*
|
|
68
100
|
* What an endpoint refused is exported by `exportCapabilities` to be written into a settings row
|
|
69
101
|
* or a file, and a key inside that blob is a credential copied somewhere nobody meant to keep one.
|
|
70
|
-
* A digest identifies the same endpoint on the next boot without saying what the key was
|
|
102
|
+
* A digest identifies the same endpoint on the next boot without saying what the key was, and it
|
|
103
|
+
* is how a host finds its own endpoint's entry in a `CapabilitySnapshot`.
|
|
71
104
|
*
|
|
72
105
|
* @param config Read for `baseUrl` and `apiKey` alone, as `endpointKey` reads it.
|
|
73
106
|
*/
|
|
@@ -118,5 +151,8 @@ export declare function listModels(config: Endpoint): Promise<ModelInfo[]>;
|
|
|
118
151
|
export declare function contextLimitFor(config: Endpoint & {
|
|
119
152
|
model: string;
|
|
120
153
|
}, declared?: number): Promise<number>;
|
|
121
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* Forgets every cached client and listing, and puts `configureClients` back to the defaults. For
|
|
156
|
+
* tests, and for a settings change under test.
|
|
157
|
+
*/
|
|
122
158
|
export declare function resetClients(): void;
|
package/dist/client.js
CHANGED
|
@@ -33,7 +33,7 @@ export const timeoutMs = (config) => {
|
|
|
33
33
|
*/
|
|
34
34
|
const clients = new Map();
|
|
35
35
|
/**
|
|
36
|
-
* How many endpoints' clients are kept at once.
|
|
36
|
+
* How many endpoints' clients are kept at once, until `configureClients` moves it.
|
|
37
37
|
*
|
|
38
38
|
* This is a backstop rather than a design. The key includes the API key, and every argument in
|
|
39
39
|
* this file for what bounds these caches is "a settings row's worth" — true of the deployments
|
|
@@ -47,6 +47,56 @@ const clients = new Map();
|
|
|
47
47
|
* constraint.
|
|
48
48
|
*/
|
|
49
49
|
const MAX_CLIENTS = 32;
|
|
50
|
+
/** How long a model an endpoint did not list stays unlisted before it is asked about again. */
|
|
51
|
+
const LISTING_MISS_MS = 30_000;
|
|
52
|
+
/** The numbers this module was written with. */
|
|
53
|
+
const CLIENT_DEFAULTS = {
|
|
54
|
+
maxClients: MAX_CLIENTS,
|
|
55
|
+
listingMissMs: LISTING_MISS_MS,
|
|
56
|
+
};
|
|
57
|
+
/** What is in force now. Read where it is used, so a change applies from the next call. */
|
|
58
|
+
let poolLimits = { ...CLIENT_DEFAULTS };
|
|
59
|
+
/**
|
|
60
|
+
* Drops the least recently asked-for clients until the pool fits. Nothing is closed on the way
|
|
61
|
+
* out: the SDK holds no handle a caller can release, and an evicted client is garbage once
|
|
62
|
+
* whatever request is still in flight on it has finished — dropping the reference is the whole
|
|
63
|
+
* of the eviction.
|
|
64
|
+
*/
|
|
65
|
+
const evict = () => {
|
|
66
|
+
for (const oldest of clients.keys()) {
|
|
67
|
+
if (clients.size <= poolLimits.maxClients)
|
|
68
|
+
break;
|
|
69
|
+
clients.delete(oldest);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Changes what the client pool is held to, for a process whose endpoints are not shaped like the
|
|
74
|
+
* deployments these defaults were chosen for.
|
|
75
|
+
*
|
|
76
|
+
* Module-level for the same reason `configureEvents` is: the pool is one module-level thing, and
|
|
77
|
+
* its size is a deployment's setting, said once at startup. A multi-tenant host keying on a key
|
|
78
|
+
* per user raises `maxClients` so its tenants stop evicting each other's connection pools; a dev
|
|
79
|
+
* box shortens `listingMissMs` so a model it has just pulled is seen sooner.
|
|
80
|
+
*
|
|
81
|
+
* A `maxClients` below the pool's current size evicts down to it at once, least recently asked
|
|
82
|
+
* for first, as the next `getClient` would have. A shorter `listingMissMs` applies to misses
|
|
83
|
+
* already remembered, since each is a timestamp compared against it when read.
|
|
84
|
+
*
|
|
85
|
+
* @param options The bounds to change. A field left out — or given anything that is not a number
|
|
86
|
+
* above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
|
|
87
|
+
* above zero: as `maxClients` it lifts the bound, and as `listingMissMs` a miss is never asked
|
|
88
|
+
* about again until `resetClients`.
|
|
89
|
+
* @returns Everything in force afterwards, including what this call did not change.
|
|
90
|
+
*/
|
|
91
|
+
export function configureClients(options = {}) {
|
|
92
|
+
for (const [name, value] of Object.entries(options)) {
|
|
93
|
+
if (typeof value === "number" && value > 0) {
|
|
94
|
+
poolLimits[name] = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
evict();
|
|
98
|
+
return { ...poolLimits };
|
|
99
|
+
}
|
|
50
100
|
/** How many idle windows the first chunk gets when `firstTokenSeconds` is not given. */
|
|
51
101
|
export const FIRST_TOKEN_FACTOR = 5;
|
|
52
102
|
/**
|
|
@@ -70,7 +120,7 @@ export const firstTokenMs = (config) => {
|
|
|
70
120
|
* The pool is per *deployment*, not per request: what belongs in this map is an endpoint an
|
|
71
121
|
* operator configured, and everything cached in this package is bounded on that reading. A
|
|
72
122
|
* consumer that mints an API key per user still gets a working client, but it is churning
|
|
73
|
-
* connection pools rather than sharing them and holding every one of them — `
|
|
123
|
+
* connection pools rather than sharing them and holding every one of them — `maxClients` keeps
|
|
74
124
|
* that from being unbounded, and it is the point at which a client of your own, built and held
|
|
75
125
|
* per tenant, is the better answer than this.
|
|
76
126
|
*
|
|
@@ -92,14 +142,7 @@ export function getClient(config) {
|
|
|
92
142
|
}
|
|
93
143
|
const client = new OpenAI({ baseURL: config.baseUrl, apiKey, timeout, maxRetries: 0 });
|
|
94
144
|
clients.set(key, client);
|
|
95
|
-
|
|
96
|
-
// evicted client is garbage once whatever request is still in flight on it has finished —
|
|
97
|
-
// dropping the reference is the whole of the eviction.
|
|
98
|
-
for (const oldest of clients.keys()) {
|
|
99
|
-
if (clients.size <= MAX_CLIENTS)
|
|
100
|
-
break;
|
|
101
|
-
clients.delete(oldest);
|
|
102
|
-
}
|
|
145
|
+
evict();
|
|
103
146
|
return client;
|
|
104
147
|
}
|
|
105
148
|
/**
|
|
@@ -156,17 +199,15 @@ const listings = new Map();
|
|
|
156
199
|
* match the configured name, an OpenRouter `:free` suffix, a typo in a settings row — and there
|
|
157
200
|
* every call fetches the listing, re-reads it, finds the same absence and answers the same zero.
|
|
158
201
|
*
|
|
159
|
-
* Remembering the miss for a moment answers both: within `
|
|
202
|
+
* Remembering the miss for a moment answers both: within `listingMissMs` nobody is asked, and
|
|
160
203
|
* after it the endpoint is asked again, so an `ollama pull` on a box that has been up a week is
|
|
161
204
|
* picked up within the minute instead of at the next restart. Bounded by the (endpoint, model)
|
|
162
205
|
* pairs actually asked about, which is the bound the listings themselves have.
|
|
163
206
|
*/
|
|
164
207
|
const misses = new Map();
|
|
165
|
-
/** How long a model an endpoint did not list stays unlisted before it is asked about again. */
|
|
166
|
-
const LISTING_MISS_MS = 30_000;
|
|
167
208
|
/**
|
|
168
209
|
* What counts as one endpoint, everywhere in this package that has to remember something about
|
|
169
|
-
* one —
|
|
210
|
+
* one — the model listings, `capabilities`, and the no-thinking hints in `side-task`.
|
|
170
211
|
*
|
|
171
212
|
* The URL and the key together, because the key is part of what is on the other end rather than
|
|
172
213
|
* only how it is paid for: a router is free to send two keys to two different backends, and then
|
|
@@ -174,7 +215,9 @@ const LISTING_MISS_MS = 30_000;
|
|
|
174
215
|
* endpoint with no key and one that passes `undefined` are one entry rather than two.
|
|
175
216
|
*
|
|
176
217
|
* Stringified rather than joined on a separator, for the reason `getClient` gives: no character
|
|
177
|
-
* is impossible in a URL or a key, and two endpoints must never collide on one entry.
|
|
218
|
+
* is impossible in a URL or a key, and two endpoints must never collide on one entry. A host
|
|
219
|
+
* keeping its own per-endpoint state keys it on this rather than on a copy of it, so the two
|
|
220
|
+
* cannot drift apart. It holds the key in the clear; `endpointId` is the one to write down.
|
|
178
221
|
*
|
|
179
222
|
* @param config Read for `baseUrl` and `apiKey` alone, and the key is optional here where
|
|
180
223
|
* `Endpoint` requires it — `capabilitiesFor` is handed a URL and maybe a key rather than a whole
|
|
@@ -187,7 +230,8 @@ export const endpointKey = (config) => JSON.stringify([config.baseUrl, config.ap
|
|
|
187
230
|
*
|
|
188
231
|
* What an endpoint refused is exported by `exportCapabilities` to be written into a settings row
|
|
189
232
|
* or a file, and a key inside that blob is a credential copied somewhere nobody meant to keep one.
|
|
190
|
-
* A digest identifies the same endpoint on the next boot without saying what the key was
|
|
233
|
+
* A digest identifies the same endpoint on the next boot without saying what the key was, and it
|
|
234
|
+
* is how a host finds its own endpoint's entry in a `CapabilitySnapshot`.
|
|
191
235
|
*
|
|
192
236
|
* @param config Read for `baseUrl` and `apiKey` alone, as `endpointKey` reads it.
|
|
193
237
|
*/
|
|
@@ -196,7 +240,7 @@ export const endpointId = (config) => createHash("sha256").update(endpointKey(co
|
|
|
196
240
|
* Served windows found by asking a server's own API, keyed on endpoint and model together.
|
|
197
241
|
*
|
|
198
242
|
* A model's entry stays until `resetClients`, like a listing; a server that answered without one
|
|
199
|
-
* is asked again after `
|
|
243
|
+
* is asked again after `listingMissMs`, like a listing that did not name the model.
|
|
200
244
|
*/
|
|
201
245
|
const served = new Map();
|
|
202
246
|
/** Endpoints that answered both probes with a refusal, and are not asked again. */
|
|
@@ -246,7 +290,7 @@ export async function servedWindow(config) {
|
|
|
246
290
|
return 0;
|
|
247
291
|
const key = JSON.stringify([endpoint, config.model]);
|
|
248
292
|
const known = served.get(key);
|
|
249
|
-
if (known && (known.window > 0 || Date.now() - known.at <
|
|
293
|
+
if (known && (known.window > 0 || Date.now() - known.at < poolLimits.listingMissMs))
|
|
250
294
|
return known.window;
|
|
251
295
|
let window = 0;
|
|
252
296
|
try {
|
|
@@ -324,8 +368,8 @@ export async function contextLimitFor(config, declared = 0) {
|
|
|
324
368
|
const asked = misses.get(missKey);
|
|
325
369
|
// Asked again, but not on every call. A model that is never coming answers the same zero
|
|
326
370
|
// however often the endpoint is asked, and a caller sizing a window per turn pays a round
|
|
327
|
-
// trip for each of them; `
|
|
328
|
-
if (asked !== undefined && Date.now() - asked <
|
|
371
|
+
// trip for each of them; `listingMissMs` is how long that answer is allowed to stand.
|
|
372
|
+
if (asked !== undefined && Date.now() - asked < poolLimits.listingMissMs)
|
|
329
373
|
return 0;
|
|
330
374
|
// A failure is not remembered: an endpoint that was down when the last run started is not
|
|
331
375
|
// an endpoint with no models, and a window nobody could ask about is not a failed run.
|
|
@@ -346,8 +390,12 @@ export async function contextLimitFor(config, declared = 0) {
|
|
|
346
390
|
}
|
|
347
391
|
return listed()?.contextLength ?? 0;
|
|
348
392
|
}
|
|
349
|
-
/**
|
|
393
|
+
/**
|
|
394
|
+
* Forgets every cached client and listing, and puts `configureClients` back to the defaults. For
|
|
395
|
+
* tests, and for a settings change under test.
|
|
396
|
+
*/
|
|
350
397
|
export function resetClients() {
|
|
398
|
+
poolLimits = { ...CLIENT_DEFAULTS };
|
|
351
399
|
clients.clear();
|
|
352
400
|
listings.clear();
|
|
353
401
|
misses.clear();
|
package/dist/hooks.d.ts
CHANGED
|
@@ -129,6 +129,12 @@ export interface Gathered {
|
|
|
129
129
|
* `configureHooks` moves it for a process, and `gather` and `assembleContext` for one request.
|
|
130
130
|
*/
|
|
131
131
|
export declare const HOOK_CONTEXT_TOKENS = 2000;
|
|
132
|
+
/**
|
|
133
|
+
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
134
|
+
* Names no host; `configureHooks` sets another for a process that wants to, and `withContext`
|
|
135
|
+
* for one request.
|
|
136
|
+
*/
|
|
137
|
+
export declare const HOOK_PREFACE: string;
|
|
132
138
|
/** What hooks are held to across a process. Every field optional; see `configureHooks`. */
|
|
133
139
|
export interface HookOptions {
|
|
134
140
|
/**
|
|
@@ -136,31 +142,34 @@ export interface HookOptions {
|
|
|
136
142
|
* held to its own `maxTokens` inside it.
|
|
137
143
|
*/
|
|
138
144
|
contextTokens?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Said above the context blocks, when a call does not give its own. Empty says nothing, and the
|
|
147
|
+
* blocks lead the question on their own.
|
|
148
|
+
*/
|
|
149
|
+
preface?: string;
|
|
139
150
|
}
|
|
140
151
|
/**
|
|
141
152
|
* Changes what hooks are held to, for a process whose windows are not the size these defaults
|
|
142
|
-
* were chosen for.
|
|
153
|
+
* were chosen for, or whose host wants its own name above the context.
|
|
143
154
|
*
|
|
144
|
-
* Module-level for the same reason `configureEvents` is: a budget
|
|
145
|
-
* once at startup. A caller that sizes
|
|
146
|
-
* recall than an 8k one — passes `maxTokens` to `gather` instead,
|
|
155
|
+
* Module-level for the same reason `configureEvents` is: a budget and a preface are a deployment's
|
|
156
|
+
* settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
|
|
157
|
+
* window can afford more recall than an 8k one — passes `maxTokens` to `gather` instead, and a
|
|
158
|
+
* `preface` passed to `withContext` or `runAgentLoop`'s hooks wins over this one the same way.
|
|
147
159
|
*
|
|
148
|
-
* @param options The
|
|
149
|
-
*
|
|
150
|
-
*
|
|
160
|
+
* @param options The settings to change. A field left out keeps what it has, and so does one given
|
|
161
|
+
* the wrong kind of value — `contextTokens` anything but a number above zero, `preface` anything
|
|
162
|
+
* but a string — so a half-built config narrows nothing. `Infinity` is a number above zero, and
|
|
163
|
+
* lifts the shared budget entirely. An empty `preface` is a string, and turns the preface off.
|
|
151
164
|
* @returns Everything in force afterwards, including what this call did not change.
|
|
152
165
|
*/
|
|
153
166
|
export declare function configureHooks(options?: HookOptions): Required<HookOptions>;
|
|
154
167
|
/**
|
|
155
|
-
* Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the
|
|
168
|
+
* Test seam: puts `configureHooks` back to the defaults, so one test's budget or preface is not the
|
|
169
|
+
* next's.
|
|
156
170
|
* `resetAll` calls it.
|
|
157
171
|
*/
|
|
158
172
|
export declare const resetHooks: () => void;
|
|
159
|
-
/**
|
|
160
|
-
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
161
|
-
* Names no host; `withContext` takes another for one that wants to.
|
|
162
|
-
*/
|
|
163
|
-
export declare const HOOK_PREFACE: string;
|
|
164
173
|
/**
|
|
165
174
|
* Builds the context a set of outcomes adds and the notes that go with it.
|
|
166
175
|
*
|
|
@@ -192,7 +201,8 @@ export declare function assembleContext(outcomes: readonly HookOutcome[], maxTok
|
|
|
192
201
|
* session once a compaction has folded the head into a summary. Anything but a user message there
|
|
193
202
|
* leaves the request as it was.
|
|
194
203
|
* @param context What `assembleContext` built. Empty returns `history` itself.
|
|
195
|
-
* @param preface Said above the blocks.
|
|
204
|
+
* @param preface Said above the blocks. Absent is what `configureHooks` last set — `HOOK_PREFACE`
|
|
205
|
+
* unless something moved it. Empty says nothing, rather than leaving a blank line where it was.
|
|
196
206
|
* @returns `history` when there was nothing to add or nowhere to add it, otherwise a new array.
|
|
197
207
|
*/
|
|
198
208
|
export declare function withContext(history: OpenAI.ChatCompletionMessageParam[], index: number, context: string, preface?: string): OpenAI.ChatCompletionMessageParam[];
|
package/dist/hooks.js
CHANGED
|
@@ -23,49 +23,58 @@ export const INJECT_EVENTS = new Set(["sessionStart", "beforeTurn"]);
|
|
|
23
23
|
* `configureHooks` moves it for a process, and `gather` and `assembleContext` for one request.
|
|
24
24
|
*/
|
|
25
25
|
export const HOOK_CONTEXT_TOKENS = 2000;
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
28
|
+
* Names no host; `configureHooks` sets another for a process that wants to, and `withContext`
|
|
29
|
+
* for one request.
|
|
30
|
+
*/
|
|
31
|
+
export const HOOK_PREFACE = "The <context> blocks below were added for this message by the host's hooks. They are " +
|
|
32
|
+
"background the user did not write and may not be relevant. The user's message follows them.";
|
|
33
|
+
/** The settings this module was written with. */
|
|
34
|
+
const HOOK_DEFAULTS = {
|
|
35
|
+
contextTokens: HOOK_CONTEXT_TOKENS,
|
|
36
|
+
preface: HOOK_PREFACE,
|
|
37
|
+
};
|
|
28
38
|
/** What is in force now. Read where it is used, so a change applies from the next request. */
|
|
29
|
-
let
|
|
39
|
+
let hookSettings = { ...HOOK_DEFAULTS };
|
|
30
40
|
/**
|
|
31
41
|
* Changes what hooks are held to, for a process whose windows are not the size these defaults
|
|
32
|
-
* were chosen for.
|
|
42
|
+
* were chosen for, or whose host wants its own name above the context.
|
|
33
43
|
*
|
|
34
|
-
* Module-level for the same reason `configureEvents` is: a budget
|
|
35
|
-
* once at startup. A caller that sizes
|
|
36
|
-
* recall than an 8k one — passes `maxTokens` to `gather` instead,
|
|
44
|
+
* Module-level for the same reason `configureEvents` is: a budget and a preface are a deployment's
|
|
45
|
+
* settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
|
|
46
|
+
* window can afford more recall than an 8k one — passes `maxTokens` to `gather` instead, and a
|
|
47
|
+
* `preface` passed to `withContext` or `runAgentLoop`'s hooks wins over this one the same way.
|
|
37
48
|
*
|
|
38
|
-
* @param options The
|
|
39
|
-
*
|
|
40
|
-
*
|
|
49
|
+
* @param options The settings to change. A field left out keeps what it has, and so does one given
|
|
50
|
+
* the wrong kind of value — `contextTokens` anything but a number above zero, `preface` anything
|
|
51
|
+
* but a string — so a half-built config narrows nothing. `Infinity` is a number above zero, and
|
|
52
|
+
* lifts the shared budget entirely. An empty `preface` is a string, and turns the preface off.
|
|
41
53
|
* @returns Everything in force afterwards, including what this call did not change.
|
|
42
54
|
*/
|
|
43
55
|
export function configureHooks(options = {}) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
const { contextTokens, preface } = options;
|
|
57
|
+
if (typeof contextTokens === "number" && contextTokens > 0) {
|
|
58
|
+
hookSettings.contextTokens = contextTokens;
|
|
47
59
|
}
|
|
48
|
-
|
|
60
|
+
if (typeof preface === "string")
|
|
61
|
+
hookSettings.preface = preface;
|
|
62
|
+
return { ...hookSettings };
|
|
49
63
|
}
|
|
50
64
|
/**
|
|
51
|
-
* Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the
|
|
65
|
+
* Test seam: puts `configureHooks` back to the defaults, so one test's budget or preface is not the
|
|
66
|
+
* next's.
|
|
52
67
|
* `resetAll` calls it.
|
|
53
68
|
*/
|
|
54
69
|
export const resetHooks = () => {
|
|
55
|
-
|
|
70
|
+
hookSettings = { ...HOOK_DEFAULTS };
|
|
56
71
|
};
|
|
57
72
|
/**
|
|
58
73
|
* The budget a call is held to: its own when it gave a usable one, the process's otherwise. The
|
|
59
74
|
* same rule `configureHooks` applies, so a `0` threaded through for "no opinion" does not quietly
|
|
60
75
|
* turn every hook's context off.
|
|
61
76
|
*/
|
|
62
|
-
const budget = (given) => typeof given === "number" && given > 0 ? given :
|
|
63
|
-
/**
|
|
64
|
-
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
65
|
-
* Names no host; `withContext` takes another for one that wants to.
|
|
66
|
-
*/
|
|
67
|
-
export const HOOK_PREFACE = "The <context> blocks below were added for this message by the host's hooks. They are " +
|
|
68
|
-
"background the user did not write and may not be relevant. The user's message follows them.";
|
|
77
|
+
const budget = (given) => typeof given === "number" && given > 0 ? given : hookSettings.contextTokens;
|
|
69
78
|
const attribute = (text) => text.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
70
79
|
/**
|
|
71
80
|
* Builds the context a set of outcomes adds and the notes that go with it.
|
|
@@ -124,14 +133,15 @@ export function assembleContext(outcomes, maxTokens) {
|
|
|
124
133
|
* session once a compaction has folded the head into a summary. Anything but a user message there
|
|
125
134
|
* leaves the request as it was.
|
|
126
135
|
* @param context What `assembleContext` built. Empty returns `history` itself.
|
|
127
|
-
* @param preface Said above the blocks.
|
|
136
|
+
* @param preface Said above the blocks. Absent is what `configureHooks` last set — `HOOK_PREFACE`
|
|
137
|
+
* unless something moved it. Empty says nothing, rather than leaving a blank line where it was.
|
|
128
138
|
* @returns `history` when there was nothing to add or nowhere to add it, otherwise a new array.
|
|
129
139
|
*/
|
|
130
|
-
export function withContext(history, index, context, preface =
|
|
140
|
+
export function withContext(history, index, context, preface = hookSettings.preface) {
|
|
131
141
|
const message = history[index];
|
|
132
142
|
if (!context || message?.role !== "user")
|
|
133
143
|
return history;
|
|
134
|
-
const lead = `${preface}\n\n${context}\n\n`;
|
|
144
|
+
const lead = preface ? `${preface}\n\n${context}\n\n` : `${context}\n\n`;
|
|
135
145
|
const content = typeof message.content === "string"
|
|
136
146
|
? `${lead}${message.content}`
|
|
137
147
|
: [{ type: "text", text: lead }, ...message.content];
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopResult, buildBody, preselect, preview, resolveApiKey, runAgentLoop, type ToolCallOutcome, type ToolCallRequest, } from "./agent-loop.ts";
|
|
13
13
|
export { type Capabilities, capabilitiesFor, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
14
14
|
export type { CatalogServer } from "./catalog.ts";
|
|
15
|
-
export { contextLimitFor, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, type ModelInfo, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.ts";
|
|
15
|
+
export { type ClientPoolOptions, configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, type ModelInfo, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.ts";
|
|
16
16
|
export { COMPACT_AT, type CompactionOptions, type CompactionPlan, compactTranscript, KEEP_RATIO, type PruneOptions, planCompaction, pruneToolResults, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.ts";
|
|
17
17
|
export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } from "./config.ts";
|
|
18
18
|
export { errorMessage } from "./errors.ts";
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { buildBody, preselect, preview, resolveApiKey, runAgentLoop, } from "./agent-loop.js";
|
|
13
13
|
export { capabilitiesFor, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
14
|
-
export { contextLimitFor, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.js";
|
|
14
|
+
export { configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.js";
|
|
15
15
|
export { COMPACT_AT, compactTranscript, KEEP_RATIO, planCompaction, pruneToolResults, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.js";
|
|
16
16
|
export { errorMessage } from "./errors.js";
|
|
17
17
|
export { configureEvents, emit, endRun, fold, history, resetEvents, watch, } from "./events.js";
|
package/dist/reset.d.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Five modules here keep state for the life of the process, each for a good reason and each
|
|
5
5
|
* with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
6
6
|
* out not to take `stream_options` or a grammar, the models that refused the no-thinking
|
|
7
|
-
* hints, the event bus, and the hooks' configured budget. `resetClients`,
|
|
8
|
-
* `resetHints`, `resetEvents` and `resetHooks` stay exported, because a
|
|
9
|
-
* one thing should say so.
|
|
7
|
+
* hints, the event bus, and the hooks' configured budget and preface. `resetClients`,
|
|
8
|
+
* `resetCapabilities`, `resetHints`, `resetEvents` and `resetHooks` stay exported, because a
|
|
9
|
+
* test that means to clear one thing should say so.
|
|
10
10
|
*
|
|
11
11
|
* This is for the other case, which is every teardown. What they hold is *latched
|
|
12
12
|
* refusals* — a fact one test taught the process about an endpoint, still true as far as the
|
package/dist/reset.js
CHANGED
|
@@ -9,9 +9,9 @@ import { resetHints } from "./side-task.js";
|
|
|
9
9
|
* Five modules here keep state for the life of the process, each for a good reason and each
|
|
10
10
|
* with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
11
11
|
* out not to take `stream_options` or a grammar, the models that refused the no-thinking
|
|
12
|
-
* hints, the event bus, and the hooks' configured budget. `resetClients`,
|
|
13
|
-
* `resetHints`, `resetEvents` and `resetHooks` stay exported, because a
|
|
14
|
-
* one thing should say so.
|
|
12
|
+
* hints, the event bus, and the hooks' configured budget and preface. `resetClients`,
|
|
13
|
+
* `resetCapabilities`, `resetHints`, `resetEvents` and `resetHooks` stay exported, because a
|
|
14
|
+
* test that means to clear one thing should say so.
|
|
15
15
|
*
|
|
16
16
|
* This is for the other case, which is every teardown. What they hold is *latched
|
|
17
17
|
* refusals* — a fact one test taught the process about an endpoint, still true as far as the
|
package/llms.txt
CHANGED
|
@@ -44,14 +44,18 @@ The contract between whatever holds the tools and the loop that offers them to a
|
|
|
44
44
|
|
|
45
45
|
### client
|
|
46
46
|
|
|
47
|
+
- `ClientPoolOptions` (type) — What the client pool is held to across a process.
|
|
48
|
+
- `configureClients` — Changes what the client pool is held to, for a process whose endpoints are not shaped like the deployments these defaults were chosen for.
|
|
47
49
|
- `contextLimitFor` — How much a model will read, in tokens.
|
|
50
|
+
- `endpointId` — `endpointKey` hashed, for the remembered facts that can leave the process.
|
|
51
|
+
- `endpointKey` — What counts as one endpoint, everywhere in this package that has to remember something about one — the model listings, `capabilities`, and the no-thinking hints in `side-task`.
|
|
48
52
|
- `FIRST_TOKEN_FACTOR` — How many idle windows the first chunk gets when `firstTokenSeconds` is not given.
|
|
49
53
|
- `firstTokenMs` — The wait for a streamed turn's first chunk, in the SDK's spelling: `undefined` is no limit.
|
|
50
54
|
- `getClient` — The client for an endpoint, built once and kept.
|
|
51
55
|
- `listModels` — Asks an endpoint what it serves, and remembers the answer.
|
|
52
56
|
- `ModelInfo` (type) — A model an endpoint offers, and what it says the model will read.
|
|
53
57
|
- `NO_KEY` — The SDK insists on a non-empty key even where the server will not look at it.
|
|
54
|
-
- `resetClients` — Forgets every cached client and listing.
|
|
58
|
+
- `resetClients` — Forgets every cached client and listing, and puts `configureClients` back to the defaults.
|
|
55
59
|
- `servedWindow` — The window a local server is actually serving a model in, which its listing does not say.
|
|
56
60
|
- `timeoutMs` — Zero, less, or absent means no limit, which the SDK spells as `undefined`.
|
|
57
61
|
|
|
@@ -108,7 +112,7 @@ What a run is doing, while it is doing it.
|
|
|
108
112
|
Lifecycle hooks, from the host's side: what a session looks like to them, where their context lands in a request, and what is said about each one.
|
|
109
113
|
|
|
110
114
|
- `assembleContext` — Builds the context a set of outcomes adds and the notes that go with it.
|
|
111
|
-
- `configureHooks` — Changes what hooks are held to, for a process whose windows are not the size these defaults were chosen for.
|
|
115
|
+
- `configureHooks` — Changes what hooks are held to, for a process whose windows are not the size these defaults were chosen for, or whose host wants its own name above the context.
|
|
112
116
|
- `Gathered` (type) — The context a set of outcomes adds to a request, and a note for each hook worth mentioning.
|
|
113
117
|
- `gather` — Runs the hooks ahead of a request and builds what they add to it.
|
|
114
118
|
- `HOOK_CONTEXT_TOKENS` — The most context all of a request's hooks add between them by default, in estimated tokens.
|
|
@@ -123,7 +127,7 @@ Lifecycle hooks, from the host's side: what a session looks like to them, where
|
|
|
123
127
|
- `HookRunner` (type) — Runs one event's hooks.
|
|
124
128
|
- `INJECT_EVENTS` — The events whose hooks run before a request, and so the only ones whose output can reach it.
|
|
125
129
|
- `notify` — Runs the hooks for an event that reads what happened and adds nothing to a request.
|
|
126
|
-
- `resetHooks` — Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the next's.
|
|
130
|
+
- `resetHooks` — Test seam: puts `configureHooks` back to the defaults, so one test's budget or preface is not the next's.
|
|
127
131
|
- `turnIndex` — Which turn of a session begins at a point, from 0: the user messages ahead of it.
|
|
128
132
|
- `turnMessages` — A stretch of a transcript as a hook reads it: what the user and the assistant said, and nothing else.
|
|
129
133
|
- `UNTRUSTED_PREFACE` — One sentence for a system prompt, saying what an `untrusted` block is.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.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",
|