@cubicecho/agent-core 4.0.0 → 4.1.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.
Files changed (73) hide show
  1. package/README.md +32 -2
  2. package/dist/{compaction.d.ts → context/compaction.d.ts} +3 -3
  3. package/dist/{compaction.js → context/compaction.js} +6 -6
  4. package/dist/{ledger.d.ts → context/ledger.d.ts} +2 -2
  5. package/dist/{ledger.js → context/ledger.js} +3 -3
  6. package/dist/{calibration.js → endpoint/calibration.js} +4 -4
  7. package/dist/{capabilities.d.ts → endpoint/capabilities.d.ts} +2 -2
  8. package/dist/{capabilities.js → endpoint/capabilities.js} +3 -3
  9. package/dist/{client.d.ts → endpoint/client.d.ts} +27 -11
  10. package/dist/{client.js → endpoint/client.js} +85 -26
  11. package/dist/{schema-compat.js → endpoint/schema-compat.js} +2 -2
  12. package/dist/{hooks.js → hooks/hooks.js} +6 -6
  13. package/dist/index.d.ts +30 -30
  14. package/dist/index.js +29 -29
  15. package/dist/{agent-loop.d.ts → run/agent-loop.d.ts} +25 -16
  16. package/dist/{agent-loop.js → run/agent-loop.js} +34 -25
  17. package/dist/{events.d.ts → run/events.d.ts} +3 -3
  18. package/dist/{events.js → run/events.js} +6 -6
  19. package/dist/{request-body.d.ts → run/request-body.d.ts} +3 -3
  20. package/dist/{request-body.js → run/request-body.js} +4 -4
  21. package/dist/{run-calls.d.ts → run/run-calls.d.ts} +3 -3
  22. package/dist/{run-calls.js → run/run-calls.js} +4 -4
  23. package/dist/{reset.js → runtime/reset.js} +6 -6
  24. package/dist/{runtime.d.ts → runtime/runtime.d.ts} +11 -11
  25. package/dist/{runtime.js → runtime/runtime.js} +12 -12
  26. package/dist/{snapshot.js → runtime/snapshot.js} +4 -4
  27. package/dist/{spec.d.ts → spec/spec.d.ts} +2 -2
  28. package/dist/{spec.js → spec/spec.js} +3 -3
  29. package/dist/{preselect.d.ts → tools/preselect.d.ts} +5 -4
  30. package/dist/{preselect.js → tools/preselect.js} +6 -5
  31. package/dist/{tool-loading.js → tools/tool-loading.js} +3 -3
  32. package/dist/{continuation.d.ts → turn/continuation.d.ts} +2 -2
  33. package/dist/{continuation.js → turn/continuation.js} +5 -5
  34. package/dist/{run-turn.d.ts → turn/run-turn.d.ts} +2 -2
  35. package/dist/{run-turn.js → turn/run-turn.js} +7 -7
  36. package/dist/{side-task.d.ts → turn/side-task.d.ts} +2 -2
  37. package/dist/{side-task.js → turn/side-task.js} +9 -9
  38. package/dist/{errors.d.ts → wire/errors.d.ts} +4 -0
  39. package/dist/{errors.js → wire/errors.js} +4 -0
  40. package/dist/{retry.js → wire/retry.js} +1 -1
  41. package/dist/{stream.js → wire/stream.js} +1 -1
  42. package/dist/{tool-calls.js → wire/tool-calls.js} +1 -1
  43. package/llms.txt +177 -175
  44. package/package.json +3 -3
  45. /package/dist/{config.d.ts → core/config.d.ts} +0 -0
  46. /package/dist/{config.js → core/config.js} +0 -0
  47. /package/dist/{digest.d.ts → core/digest.d.ts} +0 -0
  48. /package/dist/{digest.js → core/digest.js} +0 -0
  49. /package/dist/{guards.d.ts → core/guards.d.ts} +0 -0
  50. /package/dist/{guards.js → core/guards.js} +0 -0
  51. /package/dist/{platform.d.ts → core/platform.d.ts} +0 -0
  52. /package/dist/{platform.js → core/platform.js} +0 -0
  53. /package/dist/{scope.d.ts → core/scope.d.ts} +0 -0
  54. /package/dist/{scope.js → core/scope.js} +0 -0
  55. /package/dist/{calibration.d.ts → endpoint/calibration.d.ts} +0 -0
  56. /package/dist/{schema-compat.d.ts → endpoint/schema-compat.d.ts} +0 -0
  57. /package/dist/{hook-events.d.ts → hooks/hook-events.d.ts} +0 -0
  58. /package/dist/{hook-events.js → hooks/hook-events.js} +0 -0
  59. /package/dist/{hooks.d.ts → hooks/hooks.d.ts} +0 -0
  60. /package/dist/{reset.d.ts → runtime/reset.d.ts} +0 -0
  61. /package/dist/{snapshot.d.ts → runtime/snapshot.d.ts} +0 -0
  62. /package/dist/{catalog.d.ts → tools/catalog.d.ts} +0 -0
  63. /package/dist/{catalog.js → tools/catalog.js} +0 -0
  64. /package/dist/{tool-loading.d.ts → tools/tool-loading.d.ts} +0 -0
  65. /package/dist/{retry.d.ts → wire/retry.d.ts} +0 -0
  66. /package/dist/{stream.d.ts → wire/stream.d.ts} +0 -0
  67. /package/dist/{thinking.d.ts → wire/thinking.d.ts} +0 -0
  68. /package/dist/{thinking.js → wire/thinking.js} +0 -0
  69. /package/dist/{tokens.d.ts → wire/tokens.d.ts} +0 -0
  70. /package/dist/{tokens.js → wire/tokens.js} +0 -0
  71. /package/dist/{tool-calls.d.ts → wire/tool-calls.d.ts} +0 -0
  72. /package/dist/{wire.d.ts → wire/wire.d.ts} +0 -0
  73. /package/dist/{wire.js → wire/wire.js} +0 -0
package/README.md CHANGED
@@ -299,6 +299,23 @@ neither native route gives a number. A server with neither route is latched and
299
299
  `/api/v0/models` is also dropped on its own: for the life of the process once it answers 404, 405
300
300
  or 501, and for `listingMissMs` after any other error.
301
301
 
302
+ A host that sizes the window before each run passes the run's signal as the third argument, so a
303
+ stopped run does not wait on the lookup:
304
+
305
+ ```ts
306
+ const contextLimit = await contextLimitFor({ ...settings, model }, settings.contextLength, { signal });
307
+ ```
308
+
309
+ It rejects with the signal's reason, closes the request it had open, and remembers nothing of the
310
+ lookup. Nothing else makes it reject. `listModels` and `servedWindow` take the same option.
311
+
312
+ The lookup is also bounded on its own, at ten seconds across the listing and the native routes
313
+ together, or at the endpoint's request timeout where that is shorter. `requestTimeoutSeconds` is
314
+ how long to wait for a model to speak, and an endpoint that accepts the connection and then says
315
+ nothing would otherwise hold the lookup for that long, or for ever where there is none. Past the
316
+ bound `contextLimitFor` answers zero, as it does for a server it could not reach, and
317
+ `listModels` rejects with a `TimeoutError`.
318
+
302
319
  A top-level listing key is trusted without a second opinion, so on a server that puts a trained
303
320
  window there, pass `contextLength`. Ollama reports no window on any route this reads, and truncates
304
321
  an over-long prompt rather than refusing it, so on Ollama pass `contextLength` or there is no guard
@@ -703,7 +720,7 @@ try {
703
720
  store.add(sessionId, turn?.reasoning ? { ...message, reasoning_content: turn.reasoning } : message),
704
721
  });
705
722
  } catch (error) {
706
- const run = failedRun(error); // messages, usage, toolCalls, loaded, used — or undefined
723
+ const run = failedRun(error); // messages, usage, toolCalls, loaded, used, preselected — or undefined
707
724
  if (run) await store.keep(sessionId, { usage: run.usage, loaded: run.loaded });
708
725
  if (signal.aborted) return; // a stop, told by the signal as it always was
709
726
  if (error instanceof ContextOverflow) return compactAndRetry(run?.messages);
@@ -1195,6 +1212,18 @@ with `tasks.compaction` writing the summary and no falling back to the main mode
1195
1212
  are off by default: a host that already calls `preselect` or compacts in `beforeStep` is not sent
1196
1213
  a second request.
1197
1214
 
1215
+ What the loop picked it says twice. A `notice` event names the tools before the first `turn` event
1216
+ — `PRESELECTED_NOTICE` and then the names, comma-separated — and the result's `preselected` is the
1217
+ shortlist the run opened with, the host's own where it gave one and empty where nothing was picked.
1218
+ A run that fails carries it too, beside `loaded` and `used`.
1219
+
1220
+ An empty `toolSelectModel` means no preselection, and says nothing about it. A host whose settings
1221
+ read `""` as "use the run's model" has to say so in the config it builds:
1222
+
1223
+ ```ts
1224
+ const config = { ...settings, toolSelectModel: settings.toolSelectModel || settings.model };
1225
+ ```
1226
+
1198
1227
  ```ts
1199
1228
  const result = await runAgentLoop({ config, preselect: { keywords: true }, compact: true, ... });
1200
1229
  ```
@@ -1225,7 +1254,8 @@ OpenRouter `:free` suffix, a typo) would otherwise fetch the listing on every ca
1225
1254
  same zero each time. Half a minute later it asks again, so a model pulled onto a box that has been
1226
1255
  up a week is still picked up without a restart.
1227
1256
 
1228
- Both numbers are defaults. `configureClients` moves them for the process, the way `configureEvents`
1257
+ Both numbers are defaults, as is the ten seconds a window lookup is given
1258
+ (`lookupTimeoutMs`). `configureClients` moves them for the process, the way `configureEvents`
1229
1259
  and `configureHooks` do — a multi-tenant host raising the pool so tenants stop evicting each other,
1230
1260
  a dev box shortening the miss window so a model it has just pulled shows up sooner:
1231
1261
 
@@ -1,7 +1,7 @@
1
1
  import type OpenAI from 'openai';
2
- import type { Endpoint } from './config.ts';
3
- import { type HookContext, type HookRunner, type OnNote } from './hooks.ts';
4
- import { type SideTaskOptions } from './side-task.ts';
2
+ import type { Endpoint } from '../core/config.ts';
3
+ import { type HookContext, type HookRunner, type OnNote } from '../hooks/hooks.ts';
4
+ import { type SideTaskOptions } from '../turn/side-task.ts';
5
5
  /**
6
6
  * Keeping a long run inside its window: stale tool results cleared, and the oldest stretch folded
7
7
  * into a summary the model writes itself.
@@ -1,9 +1,9 @@
1
- import { HookEvent } from './hook-events.js';
2
- import { consult, notify, textOf, turnMessages } from './hooks.js';
3
- import { ask } from './side-task.js';
4
- import { messageTokens } from './tokens.js';
5
- import { holdsDefinitions } from './tool-loading.js';
6
- import { FUNCTION_TOOL, Role } from './wire.js';
1
+ import { HookEvent } from '../hooks/hook-events.js';
2
+ import { consult, notify, textOf, turnMessages, } from '../hooks/hooks.js';
3
+ import { holdsDefinitions } from '../tools/tool-loading.js';
4
+ import { ask } from '../turn/side-task.js';
5
+ import { messageTokens } from '../wire/tokens.js';
6
+ import { FUNCTION_TOOL, Role } from '../wire/wire.js';
7
7
  /** The fraction of the window in use before a summary is worth its own round trip. */
8
8
  export const COMPACT_AT = 0.75;
9
9
  /** The fraction of the window the kept tail may fill, leaving room for the run to grow again. */
@@ -1,6 +1,6 @@
1
1
  import type OpenAI from 'openai';
2
- import type { TurnUsage } from './stream.ts';
3
- import { type TokenEstimateOptions } from './tokens.ts';
2
+ import type { TurnUsage } from '../wire/stream.ts';
3
+ import { type TokenEstimateOptions } from '../wire/tokens.ts';
4
4
  /**
5
5
  * What stretches of a transcript cost, read off the prompt counts the server already reported.
6
6
  *
@@ -1,6 +1,6 @@
1
- import { CacheBreakReason } from './stream.js';
2
- import { messageChars, messageTokens } from './tokens.js';
3
- import { Role } from './wire.js';
1
+ import { CacheBreakReason } from '../wire/stream.js';
2
+ import { messageChars, messageTokens } from '../wire/tokens.js';
3
+ import { Role } from '../wire/wire.js';
4
4
  /**
5
5
  * Whether two messages say the same thing, by identity first since most of a transcript is.
6
6
  *
@@ -1,7 +1,7 @@
1
- import { getOrCreate } from './guards.js';
2
- import { scoped } from './scope.js';
3
- import { CHARS_PER_TOKEN, requestChars, toolsChars } from './tokens.js';
4
- import { PartType } from './wire.js';
1
+ import { getOrCreate } from '../core/guards.js';
2
+ import { scoped } from '../core/scope.js';
3
+ import { CHARS_PER_TOKEN, requestChars, toolsChars } from '../wire/tokens.js';
4
+ import { PartType } from '../wire/wire.js';
5
5
  /**
6
6
  * How many characters a token is worth on one model, learned from what its endpoint reports.
7
7
  *
@@ -1,5 +1,5 @@
1
- import type { EndpointIdentity } from './config.ts';
2
- import type { Produced } from './stream.ts';
1
+ import type { EndpointIdentity } from '../core/config.ts';
2
+ import type { Produced } from '../wire/stream.ts';
3
3
  /**
4
4
  * What an endpoint turned out not to support, and answering it when it says so.
5
5
  *
@@ -1,8 +1,8 @@
1
+ import { getOrCreate } from '../core/guards.js';
2
+ import { scoped } from '../core/scope.js';
3
+ import { errorMessage } from '../wire/errors.js';
1
4
  import { endpointId } from './client.js';
2
- import { errorMessage } from './errors.js';
3
- import { getOrCreate } from './guards.js';
4
5
  import { isGrammarError } from './schema-compat.js';
5
- import { scoped } from './scope.js';
6
6
  /**
7
7
  * A model's on/off flags as it starts out, every one optimistic.
8
8
  *
@@ -1,5 +1,5 @@
1
1
  import OpenAI from 'openai';
2
- import type { Endpoint, EndpointIdentity, RetryPolicy } from './config.ts';
2
+ import type { Endpoint, EndpointIdentity, RetryPolicy } from '../core/config.ts';
3
3
  /**
4
4
  * The SDK insists on a non-empty key even where the server will not look at it. This is what it
5
5
  * gets. It also reads as "this endpoint has no key" at a call site, which an empty string does
@@ -36,6 +36,11 @@ export interface ClientPoolOptions {
36
36
  * missing route — is taken at its word before the endpoint is asked again.
37
37
  */
38
38
  listingMissMs?: number;
39
+ /**
40
+ * How long, in milliseconds, one `contextLimitFor`, `servedWindow` or `listModels` may take over
41
+ * all of its requests. An endpoint's own request timeout still applies where it is shorter.
42
+ */
43
+ lookupTimeoutMs?: number;
39
44
  }
40
45
  /**
41
46
  * Changes what the client pool is held to, for a process whose endpoints are not shaped like the
@@ -43,8 +48,8 @@ export interface ClientPoolOptions {
43
48
  *
44
49
  * @param [options] - The bounds to change. A field left out — or given anything that is not a number
45
50
  * above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
46
- * above zero: as `maxClients` it lifts the bound, and as `listingMissMs` a miss is never asked
47
- * about again until `resetClients`.
51
+ * above zero: as `maxClients` it lifts the bound, as `listingMissMs` a miss is never asked about
52
+ * again until `resetClients`, and as `lookupTimeoutMs` a lookup waits out the request timeout.
48
53
  * @returns Everything in force afterwards, including what this call did not change.
49
54
  *
50
55
  * @remarks
@@ -145,13 +150,19 @@ export declare const modelKey: (endpoint: string, model: string) => string;
145
150
  * is how a host finds its own endpoint's entry in a `CapabilitySnapshot`.
146
151
  */
147
152
  export declare const endpointId: (config: EndpointIdentity) => string;
153
+ /** What a lookup takes besides the endpoint: `listModels`, `servedWindow` and `contextLimitFor`. */
154
+ export interface LookupOptions {
155
+ /** Abandons the lookup, which then rejects with the signal's reason and remembers nothing. */
156
+ signal?: AbortSignal;
157
+ }
148
158
  /**
149
159
  * The window a local server's own API says it is serving a model in, for the servers whose
150
160
  * listing does not say.
151
161
  *
152
162
  * @param config - The endpoint, plus the model whose window is wanted.
153
- * @returns The window in tokens, or zero where none was found. Never rejects: a server that could
154
- * not be reached answers zero too.
163
+ * @param [options] - Cancellation. See `LookupOptions`.
164
+ * @returns The window in tokens, or zero where none was found. Rejects only with the reason of
165
+ * `options.signal`: a server that could not be reached answers zero too.
155
166
  *
156
167
  * @remarks
157
168
  * llama.cpp reports it on `/props` as `default_generation_settings.n_ctx`, per slot, and LM
@@ -169,21 +180,26 @@ export declare const endpointId: (config: EndpointIdentity) => string;
169
180
  */
170
181
  export declare function servedWindow(config: Endpoint & {
171
182
  model: string;
172
- }): Promise<number>;
183
+ }, { signal }?: LookupOptions): Promise<number>;
173
184
  /**
174
185
  * Asks an endpoint what it serves, and remembers the answer.
175
186
  *
176
187
  * @param config - The endpoint to ask. Remembered per base URL and key, not per model.
177
- * @returns The models in order of id, whatever order the endpoint listed them in.
188
+ * @param [options] - Cancellation. See `LookupOptions`.
189
+ * @returns The models in order of id, whatever order the endpoint listed them in. Rejects with the
190
+ * reason of `options.signal` where that stopped it, a `TimeoutError` at `lookupTimeoutMs`, and the
191
+ * request's own error otherwise.
178
192
  */
179
- export declare function listModels(config: Endpoint): Promise<ModelInfo[]>;
193
+ export declare function listModels(config: Endpoint, { signal }?: LookupOptions): Promise<ModelInfo[]>;
180
194
  /**
181
195
  * How much a model will read, in tokens. Zero means nobody knows.
182
196
  *
183
197
  * @param config - The endpoint, plus the model whose window is wanted.
184
198
  * @param [declared] - The operator's own number. Above zero it wins and the endpoint is not asked.
185
- * @returns The window in tokens, or zero. Never rejects: a listing or a native route that failed
186
- * is an unknown window.
199
+ * @param [options] - Cancellation. See `LookupOptions`.
200
+ * @returns The window in tokens, or zero. Rejects only with the reason of `options.signal`: a
201
+ * listing or a native route that failed, or the lookup running out of `lookupTimeoutMs`, is an
202
+ * unknown window.
187
203
  *
188
204
  * @remarks
189
205
  * `declared` is the operator's own number, and it wins outright: an endpoint can report the
@@ -206,7 +222,7 @@ export declare function listModels(config: Endpoint): Promise<ModelInfo[]>;
206
222
  */
207
223
  export declare function contextLimitFor(config: Endpoint & {
208
224
  model: string;
209
- }, declared?: number): Promise<number>;
225
+ }, declared?: number, { signal }?: LookupOptions): Promise<number>;
210
226
  /**
211
227
  * Forgets every cached client and listing, and puts `configureClients` back to the defaults. For
212
228
  * tests, and for a settings change under test.
@@ -1,9 +1,9 @@
1
1
  import OpenAI from 'openai';
2
- import { digestOf } from './digest.js';
3
- import { byCodeUnit, isPositive } from './guards.js';
4
- import { MS_PER_SECOND } from './platform.js';
5
- import { assignSettings, scoped } from './scope.js';
6
- import { HttpStatus } from './wire.js';
2
+ import { digestOf } from '../core/digest.js';
3
+ import { byCodeUnit, isPositive } from '../core/guards.js';
4
+ import { MS_PER_SECOND } from '../core/platform.js';
5
+ import { assignSettings, scoped } from '../core/scope.js';
6
+ import { HttpStatus } from '../wire/wire.js';
7
7
  /**
8
8
  * The SDK insists on a non-empty key even where the server will not look at it. This is what it
9
9
  * gets. It also reads as "this endpoint has no key" at a call site, which an empty string does
@@ -73,10 +73,19 @@ const clients = scoped(() => new Map());
73
73
  const MAX_CLIENTS = 32;
74
74
  /** How long a model an endpoint did not list stays unlisted before it is asked about again. */
75
75
  const LISTING_MISS_MS = 30_000;
76
+ /**
77
+ * How long a window lookup may take in all, until `configureClients` moves it.
78
+ *
79
+ * @remarks
80
+ * Its own number because `requestTimeoutSeconds` is how long to wait for a model to speak, and a
81
+ * listing that has not answered in ten seconds is not going to.
82
+ */
83
+ const LOOKUP_TIMEOUT_MS = 10_000;
76
84
  /** The numbers this module was written with. */
77
85
  const CLIENT_DEFAULTS = {
78
86
  maxClients: MAX_CLIENTS,
79
87
  listingMissMs: LISTING_MISS_MS,
88
+ lookupTimeoutMs: LOOKUP_TIMEOUT_MS,
80
89
  };
81
90
  /** What is in force now. Read where it is used, so a change applies from the next call. */
82
91
  const poolLimits = scoped(() => ({ ...CLIENT_DEFAULTS }));
@@ -100,8 +109,8 @@ const evict = () => {
100
109
  *
101
110
  * @param [options] - The bounds to change. A field left out — or given anything that is not a number
102
111
  * above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
103
- * above zero: as `maxClients` it lifts the bound, and as `listingMissMs` a miss is never asked
104
- * about again until `resetClients`.
112
+ * above zero: as `maxClients` it lifts the bound, as `listingMissMs` a miss is never asked about
113
+ * again until `resetClients`, and as `lookupTimeoutMs` a lookup waits out the request timeout.
105
114
  * @returns Everything in force afterwards, including what this call did not change.
106
115
  *
107
116
  * @remarks
@@ -339,8 +348,20 @@ const refused = (endpoint) => {
339
348
  const refusal = refusals().get(endpoint);
340
349
  return refusal !== undefined && (refusal.gone || Date.now() - refusal.at < poolLimits().listingMissMs);
341
350
  };
342
- /** How long a probe may take before the window is taken from the listing instead. */
343
- const PROBE_TIMEOUT_MS = 10_000;
351
+ /**
352
+ * The signal every request of one lookup is made under, so the lookup is bounded as a whole.
353
+ *
354
+ * @param config - Read for `requestTimeoutSeconds`, which bounds the lookup where it is shorter
355
+ * than `lookupTimeoutMs`.
356
+ * @param [signal] - The caller's own, which aborts the lookup with its reason.
357
+ * @returns A signal that fires with a `TimeoutError` at the bound or with the caller's reason,
358
+ * whichever is first. Never fires where neither is set.
359
+ */
360
+ const lookupSignal = (config, signal) => {
361
+ const boundMs = Math.min(timeoutMs(config) ?? Number.POSITIVE_INFINITY, poolLimits().lookupTimeoutMs);
362
+ const bound = Number.isFinite(boundMs) ? [AbortSignal.timeout(boundMs)] : [];
363
+ return AbortSignal.any([...bound, ...(signal ? [signal] : [])]);
364
+ };
344
365
  /**
345
366
  * The server root behind an OpenAI-compatible base URL, which is where the native APIs live.
346
367
  *
@@ -358,9 +379,9 @@ const NOT_THERE = new Set([
358
379
  /**
359
380
  * Asks one native endpoint.
360
381
  *
361
- * @param config - The endpoint. Its key goes as a bearer token where it has one, and its request
362
- * timeout bounds the wait — `PROBE_TIMEOUT_MS` where it has none.
382
+ * @param config - The endpoint. Its key goes as a bearer token where it has one.
363
383
  * @param path - The route and its query, from the server root rather than from the base URL.
384
+ * @param within - The lookup's signal. Fired, the request throws as an unreachable server does.
364
385
  * @returns How the server answered, with `body` only for a 2xx that parsed as JSON.
365
386
  *
366
387
  * @remarks
@@ -368,11 +389,11 @@ const NOT_THERE = new Set([
368
389
  * without the route; `body` is absent for both, and for a 2xx that was not JSON. A server that
369
390
  * could not be reached throws.
370
391
  */
371
- async function probe(config, path) {
392
+ async function probe(config, path, within) {
372
393
  const apiKey = config.apiKey || undefined;
373
394
  const response = await fetch(`${rootOf(config.baseUrl)}${path}`, {
374
395
  headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {},
375
- signal: AbortSignal.timeout(timeoutMs(config) ?? PROBE_TIMEOUT_MS),
396
+ signal: within,
376
397
  });
377
398
  if (!response.ok) {
378
399
  return { failed: true, missing: NOT_THERE.has(response.status) };
@@ -389,8 +410,9 @@ async function probe(config, path) {
389
410
  * listing does not say.
390
411
  *
391
412
  * @param config - The endpoint, plus the model whose window is wanted.
392
- * @returns The window in tokens, or zero where none was found. Never rejects: a server that could
393
- * not be reached answers zero too.
413
+ * @param [options] - Cancellation. See `LookupOptions`.
414
+ * @returns The window in tokens, or zero where none was found. Rejects only with the reason of
415
+ * `options.signal`: a server that could not be reached answers zero too.
394
416
  *
395
417
  * @remarks
396
418
  * llama.cpp reports it on `/props` as `default_generation_settings.n_ctx`, per slot, and LM
@@ -406,7 +428,19 @@ async function probe(config, path) {
406
428
  *
407
429
  * The listing is not read here; `contextLimitFor` reads it first and comes here after.
408
430
  */
409
- export async function servedWindow(config) {
431
+ export async function servedWindow(config, { signal } = {}) {
432
+ const window = await readServed(config, lookupSignal(config, signal));
433
+ signal?.throwIfAborted();
434
+ return window;
435
+ }
436
+ /**
437
+ * `servedWindow` under a lookup's signal, which `contextLimitFor` shares with its listing.
438
+ *
439
+ * @param config - The endpoint, plus the model whose window is wanted.
440
+ * @param within - The lookup's signal. Fired, the answer is zero and nothing is remembered.
441
+ * @returns The window in tokens, or zero where none was found. Never rejects.
442
+ */
443
+ async function readServed(config, within) {
410
444
  const endpoint = endpointKey(config);
411
445
  if (unserved().has(endpoint)) {
412
446
  return 0;
@@ -419,12 +453,12 @@ export async function servedWindow(config) {
419
453
  let window = 0;
420
454
  try {
421
455
  // Named, for a llama.cpp router serving several models; a single-model server ignores it.
422
- const props = await probe(config, `/props?model=${encodeURIComponent(config.model)}`);
456
+ const props = await probe(config, `/props?model=${encodeURIComponent(config.model)}`, within);
423
457
  const settings = props.body?.default_generation_settings;
424
458
  window = positive(settings?.n_ctx);
425
459
  if (!window) {
426
460
  if (!refused(endpoint)) {
427
- const lmstudio = await probe(config, '/api/v0/models');
461
+ const lmstudio = await probe(config, '/api/v0/models', within);
428
462
  if (lmstudio.failed) {
429
463
  refusals().set(endpoint, { at: Date.now(), gone: lmstudio.missing });
430
464
  }
@@ -453,10 +487,29 @@ export async function servedWindow(config) {
453
487
  * Asks an endpoint what it serves, and remembers the answer.
454
488
  *
455
489
  * @param config - The endpoint to ask. Remembered per base URL and key, not per model.
456
- * @returns The models in order of id, whatever order the endpoint listed them in.
490
+ * @param [options] - Cancellation. See `LookupOptions`.
491
+ * @returns The models in order of id, whatever order the endpoint listed them in. Rejects with the
492
+ * reason of `options.signal` where that stopped it, a `TimeoutError` at `lookupTimeoutMs`, and the
493
+ * request's own error otherwise.
457
494
  */
458
- export async function listModels(config) {
459
- const { data } = await getClient(config).models.list();
495
+ export function listModels(config, { signal } = {}) {
496
+ return readListing(config, lookupSignal(config, signal));
497
+ }
498
+ /**
499
+ * `listModels` under a lookup's signal, which `contextLimitFor` shares with the native routes.
500
+ *
501
+ * @param config - The endpoint to ask.
502
+ * @param within - The lookup's signal. Fired, the listing rejects with its reason.
503
+ * @returns The models in order of id. A listing that fails leaves the last one remembered.
504
+ */
505
+ async function readListing(config, within) {
506
+ const { data } = await getClient(config)
507
+ .models.list({ signal: within })
508
+ .catch((error) => {
509
+ // The SDK throws an abort error of its own, whatever the signal's reason was.
510
+ within.throwIfAborted();
511
+ throw error;
512
+ });
460
513
  const trained = new Set();
461
514
  const models = [];
462
515
  for (const model of data) {
@@ -477,8 +530,10 @@ export async function listModels(config) {
477
530
  *
478
531
  * @param config - The endpoint, plus the model whose window is wanted.
479
532
  * @param [declared] - The operator's own number. Above zero it wins and the endpoint is not asked.
480
- * @returns The window in tokens, or zero. Never rejects: a listing or a native route that failed
481
- * is an unknown window.
533
+ * @param [options] - Cancellation. See `LookupOptions`.
534
+ * @returns The window in tokens, or zero. Rejects only with the reason of `options.signal`: a
535
+ * listing or a native route that failed, or the lookup running out of `lookupTimeoutMs`, is an
536
+ * unknown window.
482
537
  *
483
538
  * @remarks
484
539
  * `declared` is the operator's own number, and it wins outright: an endpoint can report the
@@ -499,11 +554,13 @@ export async function listModels(config) {
499
554
  * first, they were also asked again every `listingMissMs` for the life of the process on a server
500
555
  * whose `/props` answers without a window, by any host that sizes the window per turn.
501
556
  */
502
- export async function contextLimitFor(config, declared = 0) {
557
+ export async function contextLimitFor(config, declared = 0, { signal } = {}) {
503
558
  if (declared > 0) {
504
559
  return declared;
505
560
  }
506
561
  const key = endpointKey(config);
562
+ // One signal for the listing and the native routes, so three silent requests wait once.
563
+ const within = lookupSignal(config, signal);
507
564
  const listed = () => listings()
508
565
  .get(key)
509
566
  ?.find((model) => model.id === config.model);
@@ -523,7 +580,8 @@ export async function contextLimitFor(config, declared = 0) {
523
580
  if (asked === undefined || Date.now() - asked >= poolLimits().listingMissMs) {
524
581
  // A failure is not remembered: an endpoint that was down when the last run started is not
525
582
  // an endpoint with no models, and a window nobody could ask about is not a failed run.
526
- const answered = await listModels(config).then(() => true, () => false);
583
+ const answered = await readListing(config, within).then(() => true, () => false);
584
+ signal?.throwIfAborted();
527
585
  // Recorded where the endpoint was actually asked, and only there. Stamping it on the calls
528
586
  // that skipped the request would push the deadline out ahead of any caller polling faster
529
587
  // than the interval, which is a memory that never expires rather than one that expires in
@@ -545,7 +603,8 @@ export async function contextLimitFor(config, declared = 0) {
545
603
  if (fromListing > 0 && !trainedOnly().get(key)?.has(config.model)) {
546
604
  return fromListing;
547
605
  }
548
- const window = await servedWindow(config);
606
+ const window = await readServed(config, within);
607
+ signal?.throwIfAborted();
549
608
  return window > 0 ? window : fromListing;
550
609
  }
551
610
  /**
@@ -1,5 +1,5 @@
1
- import { getOrCreate, isRecord } from './guards.js';
2
- import { FUNCTION_TOOL, SchemaType } from './wire.js';
1
+ import { getOrCreate, isRecord } from '../core/guards.js';
2
+ import { FUNCTION_TOOL, SchemaType } from '../wire/wire.js';
3
3
  /** Lookahead and lookbehind: `(?=`, `(?!`, `(?<=`, `(?<!`. */
4
4
  const LOOKAROUND = /\(\?<?[=!]/;
5
5
  const PRIMITIVES = new Set(Object.values(SchemaType));
@@ -1,10 +1,10 @@
1
- import { digestOf } from './digest.js';
2
- import { errorMessage } from './errors.js';
3
- import { isPositive } from './guards.js';
1
+ import { digestOf } from '../core/digest.js';
2
+ import { isPositive } from '../core/guards.js';
3
+ import { assignSettings, scoped } from '../core/scope.js';
4
+ import { errorMessage } from '../wire/errors.js';
5
+ import { CHARS_PER_TOKEN, estimateTokens } from '../wire/tokens.js';
6
+ import { PartType, Role } from '../wire/wire.js';
4
7
  import { INJECT_EVENTS } from './hook-events.js';
5
- import { assignSettings, scoped } from './scope.js';
6
- import { CHARS_PER_TOKEN, estimateTokens } from './tokens.js';
7
- import { PartType, Role } from './wire.js';
8
8
  /**
9
9
  * The most context all of a request's hooks add between them by default, in estimated tokens.
10
10
  *
package/dist/index.d.ts CHANGED
@@ -9,33 +9,33 @@
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, PRESELECT_APPEND, PRESELECT_EXCLUSIVE, runAgentLoop, type StepWindow, } from './agent-loop.ts';
13
- export { calibrate, charsPerTokenFor, resetCalibration } from './calibration.ts';
14
- export { type Capabilities, capabilitiesFor, EFFORT_LADDER, EFFORT_NONE, EFFORT_OFF, effortFor, expireCapabilities, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from './capabilities.ts';
15
- export type { CatalogServer } from './catalog.ts';
16
- export { type ClientPoolOptions, configureClients, contextLimitFor, endpointId, endpointKey, firstTokenMs, getClient, listModels, type ModelInfo, NO_KEY, resetClients, resolveApiKey, servedWindow, timeoutMs, } from './client.ts';
17
- export { applyCompaction, COMPACT_AT, type CompactionOptions, type CompactionPlan, type CompactionRecord, type CompactionRunOptions, compactTranscript, KEEP_RATIO, type PruneOptions, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, } from './compaction.ts';
18
- export { type AgentConfig, type Endpoint, type ModelParams, type RetryPolicy, ToolDiscovery, type ToolPolicy, } from './config.ts';
19
- export { type ContinueTurnOptions, continueTurn, } from './continuation.ts';
20
- export { AgentLoopError, type AgentLoopFailure, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from './errors.ts';
21
- export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, RunEventKind, type RunMetrics, type RunMetricsOptions, RunOutcome, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from './events.ts';
22
- export { HOOK_EVENTS, HookEvent, INJECT_EVENTS } from './hook-events.ts';
23
- export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, type HookContext, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from './hooks.ts';
24
- export { estimateFrom, type LedgerEntry, type LedgerEstimateOptions, type LedgerRequest, rebaseLedger, recordRequest, type TokenLedger, tokensBetween, } from './ledger.ts';
25
- export { type KeywordPreselection, type KeywordPreselectOptions, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselect, preselectByKeywords, preselectInput, preselection, preselectSystem, type ToolMatch, } from './preselect.ts';
26
- export { buildBody } from './request-body.ts';
27
- export { resetAll } from './reset.ts';
28
- export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow, isTransient, SMALLEST_LIKELY_WINDOW, } from './retry.ts';
29
- export { preview } from './run-calls.ts';
30
- export { type RunTurnOptions, runTurn } from './run-turn.ts';
31
- export { createRuntime, defaultRuntime, type Runtime, type RuntimeOptions } from './runtime.ts';
32
- export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from './schema-compat.ts';
33
- export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTask, type SideTaskInput, type SideTaskOptions, taskCall, tryAsk, } from './side-task.ts';
34
- export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from './snapshot.ts';
35
- 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, REF_FILE, REF_URL, RESOLVED_DEFAULTS, type ResolvedAgent, type ResolvedTask, type RetrySpec, resolveAgentSpec, SPEC_EVENTS, type SpecBundle, type SpecServer, type TaskSpec, type ToolsSpec, } from './spec.ts';
36
- export { CacheBreakReason, type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from './stream.ts';
37
- export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, SPLIT_OUTPUT, SPLIT_REASONING, type Split, stripThinking, THINK_FENCE, } from './thinking.ts';
38
- export { type ContextBreakdown, type ContextBreakdownOptions, compact, contextChars, contextTokens, estimateTokens, messageTokens, requestChars, requestTokens, type TokenEstimateOptions, } from './tokens.ts';
39
- export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, recoverToolCalls, ToolArgumentsError, type ToolCall, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult, } from './tool-calls.ts';
40
- export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, orderTools, PROXY_TOOLS, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, type ToolOrder, } from './tool-loading.ts';
41
- export { FinishReason, FUNCTION_TOOL, JSON_SCHEMA_FORMAT, PartType, Role, SchemaType } from './wire.ts';
12
+ export { applyCompaction, COMPACT_AT, type CompactionOptions, type CompactionPlan, type CompactionRecord, type CompactionRunOptions, compactTranscript, KEEP_RATIO, type PruneOptions, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, } from './context/compaction.ts';
13
+ export { estimateFrom, type LedgerEntry, type LedgerEstimateOptions, type LedgerRequest, rebaseLedger, recordRequest, type TokenLedger, tokensBetween, } from './context/ledger.ts';
14
+ export { type AgentConfig, type Endpoint, type ModelParams, type RetryPolicy, ToolDiscovery, type ToolPolicy, } from './core/config.ts';
15
+ export { calibrate, charsPerTokenFor, resetCalibration } from './endpoint/calibration.ts';
16
+ export { type Capabilities, capabilitiesFor, EFFORT_LADDER, EFFORT_NONE, EFFORT_OFF, effortFor, expireCapabilities, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from './endpoint/capabilities.ts';
17
+ export { type ClientPoolOptions, configureClients, contextLimitFor, endpointId, endpointKey, firstTokenMs, getClient, type LookupOptions, listModels, type ModelInfo, NO_KEY, resetClients, resolveApiKey, servedWindow, timeoutMs, } from './endpoint/client.ts';
18
+ export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from './endpoint/schema-compat.ts';
19
+ export { HOOK_EVENTS, HookEvent, INJECT_EVENTS } from './hooks/hook-events.ts';
20
+ export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, type HookContext, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from './hooks/hooks.ts';
21
+ export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopRequest, type AgentLoopResult, PRESELECT_APPEND, PRESELECT_EXCLUSIVE, PRESELECTED_NOTICE, runAgentLoop, type StepWindow, } from './run/agent-loop.ts';
22
+ export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, RunEventKind, type RunMetrics, type RunMetricsOptions, RunOutcome, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from './run/events.ts';
23
+ export { buildBody } from './run/request-body.ts';
24
+ export { preview } from './run/run-calls.ts';
25
+ export { resetAll } from './runtime/reset.ts';
26
+ export { createRuntime, defaultRuntime, type Runtime, type RuntimeOptions } from './runtime/runtime.ts';
27
+ export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from './runtime/snapshot.ts';
28
+ 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, REF_FILE, REF_URL, RESOLVED_DEFAULTS, type ResolvedAgent, type ResolvedTask, type RetrySpec, resolveAgentSpec, SPEC_EVENTS, type SpecBundle, type SpecServer, type TaskSpec, type ToolsSpec, } from './spec/spec.ts';
29
+ export type { CatalogServer } from './tools/catalog.ts';
30
+ export { type KeywordPreselection, type KeywordPreselectOptions, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselect, preselectByKeywords, preselectInput, preselection, preselectSystem, type ToolMatch, } from './tools/preselect.ts';
31
+ export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, orderTools, PROXY_TOOLS, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, type ToolOrder, } from './tools/tool-loading.ts';
32
+ export { type ContinueTurnOptions, continueTurn, } from './turn/continuation.ts';
33
+ export { type RunTurnOptions, runTurn } from './turn/run-turn.ts';
34
+ export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTask, type SideTaskInput, type SideTaskOptions, taskCall, tryAsk, } from './turn/side-task.ts';
35
+ export { AgentLoopError, type AgentLoopFailure, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from './wire/errors.ts';
36
+ export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow, isTransient, SMALLEST_LIKELY_WINDOW, } from './wire/retry.ts';
37
+ export { CacheBreakReason, type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from './wire/stream.ts';
38
+ export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, SPLIT_OUTPUT, SPLIT_REASONING, type Split, stripThinking, THINK_FENCE, } from './wire/thinking.ts';
39
+ export { type ContextBreakdown, type ContextBreakdownOptions, compact, contextChars, contextTokens, estimateTokens, messageTokens, requestChars, requestTokens, type TokenEstimateOptions, } from './wire/tokens.ts';
40
+ export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, recoverToolCalls, ToolArgumentsError, type ToolCall, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult, } from './wire/tool-calls.ts';
41
+ export { FinishReason, FUNCTION_TOOL, JSON_SCHEMA_FORMAT, PartType, Role, SchemaType } from './wire/wire.ts';