@cubicecho/agent-core 3.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 +34 -3
  2. package/dist/{compaction.d.ts → context/compaction.d.ts} +6 -6
  3. package/dist/{compaction.js → context/compaction.js} +9 -9
  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/{guards.d.ts → core/guards.d.ts} +2 -3
  7. package/dist/{guards.js → core/guards.js} +1 -2
  8. package/dist/{scope.d.ts → core/scope.d.ts} +3 -3
  9. package/dist/{calibration.js → endpoint/calibration.js} +4 -4
  10. package/dist/{capabilities.d.ts → endpoint/capabilities.d.ts} +2 -2
  11. package/dist/{capabilities.js → endpoint/capabilities.js} +3 -3
  12. package/dist/{client.d.ts → endpoint/client.d.ts} +27 -11
  13. package/dist/{client.js → endpoint/client.js} +85 -26
  14. package/dist/{schema-compat.js → endpoint/schema-compat.js} +2 -2
  15. package/dist/{hooks.js → hooks/hooks.js} +6 -6
  16. package/dist/index.d.ts +30 -30
  17. package/dist/index.js +29 -29
  18. package/dist/{agent-loop.d.ts → run/agent-loop.d.ts} +25 -16
  19. package/dist/{agent-loop.js → run/agent-loop.js} +34 -25
  20. package/dist/{events.d.ts → run/events.d.ts} +3 -3
  21. package/dist/{events.js → run/events.js} +6 -6
  22. package/dist/{request-body.d.ts → run/request-body.d.ts} +3 -3
  23. package/dist/{request-body.js → run/request-body.js} +4 -4
  24. package/dist/{run-calls.d.ts → run/run-calls.d.ts} +3 -3
  25. package/dist/{run-calls.js → run/run-calls.js} +4 -4
  26. package/dist/{reset.js → runtime/reset.js} +6 -6
  27. package/dist/{runtime.d.ts → runtime/runtime.d.ts} +11 -11
  28. package/dist/{runtime.js → runtime/runtime.js} +12 -12
  29. package/dist/{snapshot.js → runtime/snapshot.js} +4 -4
  30. package/dist/{spec.d.ts → spec/spec.d.ts} +2 -2
  31. package/dist/{spec.js → spec/spec.js} +3 -3
  32. package/dist/{preselect.d.ts → tools/preselect.d.ts} +5 -4
  33. package/dist/{preselect.js → tools/preselect.js} +6 -5
  34. package/dist/{tool-loading.js → tools/tool-loading.js} +3 -3
  35. package/dist/{continuation.d.ts → turn/continuation.d.ts} +4 -4
  36. package/dist/{continuation.js → turn/continuation.js} +7 -7
  37. package/dist/{run-turn.d.ts → turn/run-turn.d.ts} +2 -2
  38. package/dist/{run-turn.js → turn/run-turn.js} +7 -7
  39. package/dist/{side-task.d.ts → turn/side-task.d.ts} +2 -2
  40. package/dist/{side-task.js → turn/side-task.js} +9 -9
  41. package/dist/{errors.d.ts → wire/errors.d.ts} +4 -0
  42. package/dist/{errors.js → wire/errors.js} +4 -0
  43. package/dist/{retry.d.ts → wire/retry.d.ts} +1 -1
  44. package/dist/{retry.js → wire/retry.js} +2 -2
  45. package/dist/{stream.js → wire/stream.js} +1 -1
  46. package/dist/{tokens.d.ts → wire/tokens.d.ts} +2 -2
  47. package/dist/{tokens.js → wire/tokens.js} +2 -2
  48. package/dist/{tool-calls.js → wire/tool-calls.js} +1 -1
  49. package/llms.txt +177 -179
  50. package/package.json +3 -3
  51. /package/dist/{config.d.ts → core/config.d.ts} +0 -0
  52. /package/dist/{config.js → core/config.js} +0 -0
  53. /package/dist/{digest.d.ts → core/digest.d.ts} +0 -0
  54. /package/dist/{digest.js → core/digest.js} +0 -0
  55. /package/dist/{platform.d.ts → core/platform.d.ts} +0 -0
  56. /package/dist/{platform.js → core/platform.js} +0 -0
  57. /package/dist/{scope.js → core/scope.js} +0 -0
  58. /package/dist/{calibration.d.ts → endpoint/calibration.d.ts} +0 -0
  59. /package/dist/{schema-compat.d.ts → endpoint/schema-compat.d.ts} +0 -0
  60. /package/dist/{hook-events.d.ts → hooks/hook-events.d.ts} +0 -0
  61. /package/dist/{hook-events.js → hooks/hook-events.js} +0 -0
  62. /package/dist/{hooks.d.ts → hooks/hooks.d.ts} +0 -0
  63. /package/dist/{reset.d.ts → runtime/reset.d.ts} +0 -0
  64. /package/dist/{snapshot.d.ts → runtime/snapshot.d.ts} +0 -0
  65. /package/dist/{catalog.d.ts → tools/catalog.d.ts} +0 -0
  66. /package/dist/{catalog.js → tools/catalog.js} +0 -0
  67. /package/dist/{tool-loading.d.ts → tools/tool-loading.d.ts} +0 -0
  68. /package/dist/{stream.d.ts → wire/stream.d.ts} +0 -0
  69. /package/dist/{thinking.d.ts → wire/thinking.d.ts} +0 -0
  70. /package/dist/{thinking.js → wire/thinking.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
 
@@ -1245,7 +1275,8 @@ cannot drift. `endpointKey` holds the key in the clear; `endpointId`, its SHA-25
1245
1275
  that is safe to write down.
1246
1276
 
1247
1277
  `resetAll` drops all five, and `reset.ts` names each seam separately for a test that wants one —
1248
- `resetCalibration` for the measured ratios.
1278
+ `resetCalibration` for the measured ratios, `resetHints` for the models that refused the
1279
+ no-thinking hints.
1249
1280
 
1250
1281
  The latches can outlive the process as well, because otherwise every restart spends one refused
1251
1282
  request per endpoint and model learning the same facts again. `exportCapabilities` returns every
@@ -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.
@@ -186,7 +186,7 @@ export declare function summaryInput(plan: CompactionPlan): string;
186
186
  * @param model - The model to write it, which may be a smaller one than the run's.
187
187
  * @param [options] - Cancellation and notices, and the instruction and ceiling the summary is
188
188
  * written under.
189
- * @returns A function from `summaryInput`'s text to the summary. It rejects as `ask` does, and
189
+ * @returns A function from the text of what is being folded to the summary. It rejects as `ask` does, and
190
190
  * resolves with an empty string where the model wrote nothing.
191
191
  */
192
192
  export declare const summariser: (config: Endpoint, model: string, { system, maxTokens, ...options }?: SideTaskOptions & {
@@ -233,7 +233,7 @@ export interface CompactionRunOptions {
233
233
  *
234
234
  * @param messages - The transcript the plan was made for. Read only, and only for the hooks.
235
235
  * @param plan - What `planCompaction` returned for it.
236
- * @param summarise - Writes the summary from `summaryInput`'s text. See `summariser`. Not called
236
+ * @param summarise - Writes the summary from the text it is handed. See `summariser`. Not called
237
237
  * when a hook vetoes.
238
238
  * @param [options] - Hooks to tell and whether the window is already past. See `CompactionRunOptions`.
239
239
  * @returns `undefined` when nothing was folded — a hook vetoed, or the summary came back empty —
@@ -292,7 +292,7 @@ export declare const requestIndex: (index: number, record?: Pick<CompactionRecor
292
292
  *
293
293
  * @param messages - The transcript the plan was made for. Not written to.
294
294
  * @param plan - What `planCompaction` returned for it.
295
- * @param summarise - Writes the summary from `summaryInput`'s text. See `summariser`. Not called
295
+ * @param summarise - Writes the summary from the text it is handed. See `summariser`. Not called
296
296
  * when a hook vetoes.
297
297
  * @param [options] - Hooks to tell and whether the window is already past. See
298
298
  * `CompactionRunOptions`.
@@ -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. */
@@ -252,7 +252,7 @@ export function summaryInput(plan) {
252
252
  * @param model - The model to write it, which may be a smaller one than the run's.
253
253
  * @param [options] - Cancellation and notices, and the instruction and ceiling the summary is
254
254
  * written under.
255
- * @returns A function from `summaryInput`'s text to the summary. It rejects as `ask` does, and
255
+ * @returns A function from the text of what is being folded to the summary. It rejects as `ask` does, and
256
256
  * resolves with an empty string where the model wrote nothing.
257
257
  */
258
258
  export const summariser = (config, model, { system = SUMMARY_PROMPT, maxTokens = SUMMARY_TOKENS, ...options } = {}) => (text) => ask(config, model, system, text, { maxTokens, ...options });
@@ -261,7 +261,7 @@ export const summariser = (config, model, { system = SUMMARY_PROMPT, maxTokens =
261
261
  *
262
262
  * @param messages - The transcript the plan was made for. Read only, and only for the hooks.
263
263
  * @param plan - What `planCompaction` returned for it.
264
- * @param summarise - Writes the summary from `summaryInput`'s text. See `summariser`. Not called
264
+ * @param summarise - Writes the summary from the text it is handed. See `summariser`. Not called
265
265
  * when a hook vetoes.
266
266
  * @param [options] - Hooks to tell and whether the window is already past. See `CompactionRunOptions`.
267
267
  * @returns `undefined` when nothing was folded — a hook vetoed, or the summary came back empty —
@@ -357,7 +357,7 @@ export const requestIndex = (index, record, head = 0) => {
357
357
  *
358
358
  * @param messages - The transcript the plan was made for. Not written to.
359
359
  * @param plan - What `planCompaction` returned for it.
360
- * @param summarise - Writes the summary from `summaryInput`'s text. See `summariser`. Not called
360
+ * @param summarise - Writes the summary from the text it is handed. See `summariser`. Not called
361
361
  * when a hook vetoes.
362
362
  * @param [options] - Hooks to tell and whether the window is already past. See
363
363
  * `CompactionRunOptions`.
@@ -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
  *
@@ -37,12 +37,11 @@ interface Lookup<K, V> {
37
37
  *
38
38
  * @param map - Where to look. Gains an entry on a miss, which is the point of calling this.
39
39
  * @param key - What to look under.
40
- * @param create - Builds the value on a miss, and is not called on a hit. A stored `undefined`
41
- * reads as a miss, so one that returns it is called every time.
40
+ * @param create - Builds the value on a miss, and is not called on a hit.
42
41
  * @returns What the map holds under the key afterwards: the held value itself on a hit, what
43
42
  * `create` returned on a miss.
44
43
  */
45
- export declare function getOrCreate<K, V>(map: Lookup<K, V>, key: K, create: () => V): V;
44
+ export declare function getOrCreate<K, V extends {}>(map: Lookup<K, V>, key: K, create: () => V): V;
46
45
  /**
47
46
  * A count and its noun, plural unless the count is one: `1 tool`, `3 tools`.
48
47
  *
@@ -32,8 +32,7 @@ export const isPositive = (value) => typeof value === 'number' && value > 0;
32
32
  *
33
33
  * @param map - Where to look. Gains an entry on a miss, which is the point of calling this.
34
34
  * @param key - What to look under.
35
- * @param create - Builds the value on a miss, and is not called on a hit. A stored `undefined`
36
- * reads as a miss, so one that returns it is called every time.
35
+ * @param create - Builds the value on a miss, and is not called on a hit.
37
36
  * @returns What the map holds under the key afterwards: the held value itself on a hit, what
38
37
  * `create` returned on a miss.
39
38
  */
@@ -5,7 +5,7 @@
5
5
  * A map rather than a record of named caches, so a module that grows a cache declares it where
6
6
  * it is used and `runtime.ts` does not have to hear about it.
7
7
  */
8
- export type Scope = Map<symbol, unknown>;
8
+ export type Scope = Map<symbol, NonNullable<unknown>>;
9
9
  /** The default runtime's scope: what the top-level functions use when no runtime is running. */
10
10
  export declare const rootScope: Scope;
11
11
  /**
@@ -23,7 +23,7 @@ export declare const currentScope: () => Scope;
23
23
  */
24
24
  export declare const inScope: <T>(scope: Scope, fn: () => T) => T;
25
25
  /** One module's state, read from whichever scope is current. See `scoped`. */
26
- export interface Scoped<T> {
26
+ export interface Scoped<T extends {}> {
27
27
  /** The current scope's copy, made on first use. */
28
28
  (): T;
29
29
  /** Drops the current scope's copy, so the next read makes a fresh one. */
@@ -41,7 +41,7 @@ export interface Scoped<T> {
41
41
  * and each use reads `cache()`. The copy is made lazily, so a runtime pays only for the modules
42
42
  * it touches.
43
43
  */
44
- export declare function scoped<T>(create: () => T): Scoped<T>;
44
+ export declare function scoped<T extends {}>(create: () => T): Scoped<T>;
45
45
  /**
46
46
  * Writes what a caller gave onto the settings in force, field by field, skipping what is unusable.
47
47
  *
@@ -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
  *