@cubicecho/agent-core 2.22.0 → 2.24.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 (49) hide show
  1. package/README.md +61 -6
  2. package/dist/agent-loop.d.ts +3 -2
  3. package/dist/agent-loop.js +385 -303
  4. package/dist/calibration.js +8 -6
  5. package/dist/capabilities.d.ts +36 -0
  6. package/dist/capabilities.js +116 -65
  7. package/dist/client.d.ts +5 -4
  8. package/dist/client.js +62 -57
  9. package/dist/compaction.js +5 -9
  10. package/dist/continuation.d.ts +4 -4
  11. package/dist/continuation.js +1 -4
  12. package/dist/events.d.ts +16 -1
  13. package/dist/events.js +64 -44
  14. package/dist/guards.d.ts +22 -0
  15. package/dist/guards.js +22 -0
  16. package/dist/hook-events.d.ts +22 -0
  17. package/dist/hook-events.js +22 -0
  18. package/dist/hooks.d.ts +15 -16
  19. package/dist/hooks.js +44 -44
  20. package/dist/index.d.ts +6 -4
  21. package/dist/index.js +6 -4
  22. package/dist/reset.d.ts +6 -2
  23. package/dist/reset.js +6 -2
  24. package/dist/retry.d.ts +13 -7
  25. package/dist/retry.js +14 -7
  26. package/dist/run-turn.d.ts +8 -1
  27. package/dist/runtime.d.ts +120 -0
  28. package/dist/runtime.js +113 -0
  29. package/dist/schema-compat.d.ts +23 -0
  30. package/dist/schema-compat.js +76 -59
  31. package/dist/scope.d.ts +35 -0
  32. package/dist/scope.js +42 -0
  33. package/dist/side-task.d.ts +5 -0
  34. package/dist/side-task.js +24 -52
  35. package/dist/snapshot.d.ts +2 -2
  36. package/dist/snapshot.js +27 -48
  37. package/dist/spec.d.ts +11 -12
  38. package/dist/spec.js +36 -66
  39. package/dist/stream.d.ts +7 -0
  40. package/dist/stream.js +8 -1
  41. package/dist/thinking.js +6 -4
  42. package/dist/tokens.d.ts +9 -2
  43. package/dist/tokens.js +10 -3
  44. package/dist/tool-calls.d.ts +9 -0
  45. package/dist/tool-calls.js +11 -6
  46. package/dist/tool-loading.d.ts +10 -0
  47. package/dist/tool-loading.js +15 -7
  48. package/llms.txt +19 -5
  49. package/package.json +1 -1
@@ -0,0 +1,113 @@
1
+ import { preselect, runAgentLoop } from "./agent-loop.js";
2
+ import { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.js";
3
+ import { capabilitiesFor, expireCapabilities, resetCapabilities } from "./capabilities.js";
4
+ import { configureClients, contextLimitFor, getClient, listModels, resetClients, servedWindow, } from "./client.js";
5
+ import { compactTranscript, runCompaction, summariser } from "./compaction.js";
6
+ import { continueTurn } from "./continuation.js";
7
+ import { configureEvents, emit, endRun, history, resetEvents, watch, } from "./events.js";
8
+ import { assembleContext, configureHooks, consult, gather, notify, resetHooks, withContext, } from "./hooks.js";
9
+ import { resetAll } from "./reset.js";
10
+ import { runTurn } from "./run-turn.js";
11
+ import { inScope, rootScope } from "./scope.js";
12
+ import { ask, askJson, resetHints, tryAsk } from "./side-task.js";
13
+ import { exportCapabilities, importCapabilities } from "./snapshot.js";
14
+ /**
15
+ * Every exported function that reads or writes what a runtime remembers, itself or through what
16
+ * it calls. The rest of the package is pure, and is the same function whichever runtime it is
17
+ * called beside.
18
+ */
19
+ const STATEFUL = {
20
+ runAgentLoop,
21
+ preselect,
22
+ runTurn,
23
+ continueTurn,
24
+ ask,
25
+ askJson,
26
+ tryAsk,
27
+ summariser,
28
+ runCompaction,
29
+ compactTranscript,
30
+ getClient,
31
+ listModels,
32
+ contextLimitFor,
33
+ servedWindow,
34
+ configureClients,
35
+ capabilitiesFor,
36
+ expireCapabilities,
37
+ exportCapabilities,
38
+ importCapabilities,
39
+ calibrate,
40
+ charsPerTokenFor,
41
+ emit,
42
+ endRun,
43
+ history,
44
+ watch,
45
+ configureEvents,
46
+ configureHooks,
47
+ gather,
48
+ notify,
49
+ consult,
50
+ assembleContext,
51
+ withContext,
52
+ resetClients,
53
+ resetCapabilities,
54
+ resetCalibration,
55
+ resetHints,
56
+ resetEvents,
57
+ resetHooks,
58
+ resetAll,
59
+ };
60
+ /** Makes the methods of the runtime whose state is `scope`. */
61
+ function bind(scope) {
62
+ const within = (fn) => (...args) => inScope(scope, () => fn(...args));
63
+ const methods = Object.fromEntries(Object.entries(STATEFUL).map(([name, fn]) => [
64
+ name,
65
+ within(fn),
66
+ ]));
67
+ return {
68
+ ...methods,
69
+ // What it hands back is called later, by `compactTranscript` or by the host, and asks then.
70
+ summariser: (...args) => within(summariser(...args)),
71
+ run: (fn) => inScope(scope, fn),
72
+ };
73
+ }
74
+ /**
75
+ * A runtime of its own: separate clients, listings, capability latches, hints, event bus and hook
76
+ * settings from the process's and from every other runtime's.
77
+ *
78
+ * For a host that must not share them — one that mints a key per tenant, where a shared pool has
79
+ * tenants evicting each other's clients and a shared bus has one tenant's run id readable by
80
+ * another — and for tests, where a runtime per case needs no `resetAll` between them and can run
81
+ * in parallel. A host with one deployment's worth of endpoints does not need one: the top-level
82
+ * functions are the default runtime's methods.
83
+ *
84
+ * The runtime is carried by async context rather than by argument, so `runAgentLoop` called as a
85
+ * method uses this runtime all the way down, and so does a top-level function the host calls from
86
+ * inside it — from `dispatch`, a hook, `onEvent`. Outside a method or `run`, a top-level function
87
+ * is the default runtime's again; a request handler that emits to a run a runtime started calls
88
+ * `runtime.emit`.
89
+ *
90
+ * Nothing needs closing. A runtime is garbage once nothing holds it; its one timer, the event
91
+ * bus's sweep, is unreferenced and stops rescheduling when its last run has been swept.
92
+ *
93
+ * @param options What to configure it with before first use. A part left out keeps the
94
+ * defaults — not the default runtime's settings, which a new runtime does not inherit.
95
+ */
96
+ export function createRuntime(options = {}) {
97
+ const runtime = bind(new Map());
98
+ if (options.clients)
99
+ runtime.configureClients(options.clients);
100
+ if (options.events)
101
+ runtime.configureEvents(options.events);
102
+ if (options.hooks)
103
+ runtime.configureHooks(options.hooks);
104
+ return runtime;
105
+ }
106
+ /**
107
+ * The runtime the top-level functions use when no other is running.
108
+ *
109
+ * `defaultRuntime.getClient(endpoint)` and `getClient(endpoint)` are the same call outside any
110
+ * runtime. Inside one they differ: the top-level function follows the runtime that is running,
111
+ * and this always reaches the process-wide state.
112
+ */
113
+ export const defaultRuntime = bind(rootScope);
@@ -1,4 +1,17 @@
1
1
  import type OpenAI from "openai";
2
+ /**
3
+ * One JSON Schema as a strict server will accept it, for a schema that is not a tool's parameters.
4
+ *
5
+ * What `sanitizeTools` does to each tool, without the tool: a `response_format` schema goes through
6
+ * the same grammar converter a tool's parameters do, and a caller holding only the schema had to
7
+ * wrap it in a definition to get here. Nothing is remembered — the cache is keyed on a tool
8
+ * definition's identity, and a bare schema has no object that stands for it across requests.
9
+ *
10
+ * @param schema The schema as written. Never mutated. Its root is held to an object, as a tool's
11
+ * parameters are, and anything that is not a schema at all comes back as an object with no
12
+ * properties.
13
+ */
14
+ export declare function sanitizeSchema(schema: unknown): Record<string, unknown>;
2
15
  /**
3
16
  * Tool definitions a strict server will accept, remembered per definition object.
4
17
  *
@@ -9,6 +22,16 @@ import type OpenAI from "openai";
9
22
  * changed, a new definition is returned in its place.
10
23
  */
11
24
  export declare const sanitizeTools: (tools: OpenAI.ChatCompletionTool[]) => OpenAI.ChatCompletionTool[];
25
+ /**
26
+ * One schema without its `pattern` and `format` keywords, for a schema that is not a tool's.
27
+ *
28
+ * The retry `relaxTools` makes, on a bare schema. Only the keywords go: a property that happens to
29
+ * be called `format` is an argument name and stays.
30
+ *
31
+ * @param schema Already sanitised. Relaxing is the retry, not a substitute for `sanitizeSchema`.
32
+ * Never mutated; anything that is not a schema comes back as an object with no properties.
33
+ */
34
+ export declare function relaxSchema(schema: unknown): Record<string, unknown>;
12
35
  /**
13
36
  * The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
14
37
  * `pattern` and most `format` values, both of which only ever narrowed a string the tool
@@ -1,4 +1,4 @@
1
- const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1
+ import { isRecord } from "./guards.js";
2
2
  /** Lookahead and lookbehind: `(?=`, `(?!`, `(?<=`, `(?<!`. */
3
3
  const LOOKAROUND = /\(\?<?[=!]/;
4
4
  const PRIMITIVES = new Set(["object", "string", "number", "integer", "boolean", "array", "null"]);
@@ -20,6 +20,24 @@ const SCHEMA_KEYS = new Set([
20
20
  ]);
21
21
  /** Keys whose value is a name -> schema map. */
22
22
  const SCHEMA_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions"]);
23
+ /** The two spellings of a schema's own pool of named definitions. */
24
+ const POOL_KEYS = ["definitions", "$defs"];
25
+ /**
26
+ * One keyword's value with `fn` applied wherever it holds a schema, and untouched where it holds
27
+ * data — which is what keeps a walk out of `default`, `enum` and `const`.
28
+ */
29
+ const mapChildren = (key, value, fn) => {
30
+ if (SCHEMA_KEYS.has(key))
31
+ return Array.isArray(value) ? value.map((sub) => fn(sub)) : fn(value);
32
+ // The keys here are argument names; only the values are schemas.
33
+ if (SCHEMA_MAPS.has(key) && isRecord(value))
34
+ return Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, fn(sub)]));
35
+ return value;
36
+ };
37
+ /** The names a schema's `required` lists, without whatever in it is not a name. */
38
+ const requiredOf = (node) => Array.isArray(node.required)
39
+ ? node.required.filter((name) => typeof name === "string")
40
+ : [];
23
41
  /**
24
42
  * Coerces one schema position. Malformed MCP output sometimes puts a bare type name where a
25
43
  * whole schema belongs, which the grammar converter reports as `Unrecognized schema: "object"`.
@@ -29,7 +47,7 @@ function asSchema(node) {
29
47
  return PRIMITIVES.has(node) && node !== "object" ? { type: node } : EMPTY_OBJECT();
30
48
  if (typeof node === "boolean")
31
49
  return node;
32
- if (!isObject(node))
50
+ if (!isRecord(node))
33
51
  return EMPTY_OBJECT();
34
52
  return normalize(node);
35
53
  }
@@ -50,14 +68,8 @@ function normalize(node) {
50
68
  else
51
69
  out.type = "null";
52
70
  }
53
- else if (SCHEMA_KEYS.has(key)) {
54
- out[key] = Array.isArray(value) ? value.map(asSchema) : asSchema(value);
55
- }
56
- else if (SCHEMA_MAPS.has(key) && isObject(value)) {
57
- out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, asSchema(sub)]));
58
- }
59
71
  else {
60
- out[key] = value;
72
+ out[key] = mapChildren(key, value, asSchema);
61
73
  }
62
74
  }
63
75
  collapseNullableUnion(out);
@@ -66,7 +78,7 @@ function normalize(node) {
66
78
  if (typeof out.pattern === "string" && LOOKAROUND.test(out.pattern))
67
79
  delete out.pattern;
68
80
  // `{"type": "object"}` with no properties produces invalid GBNF.
69
- if (out.type === "object" && !isObject(out.properties))
81
+ if (out.type === "object" && !isRecord(out.properties))
70
82
  out.properties = {};
71
83
  // Strict validators reject any sibling of `$ref`, and draft-07 ignores them, so a reference
72
84
  // stands alone or not at all. This is not hypothetical tidying: collapsing `anyOf: [{$ref},
@@ -88,11 +100,11 @@ function collapseNullableUnion(node) {
88
100
  const variants = node[key];
89
101
  if (!Array.isArray(variants))
90
102
  continue;
91
- const concrete = variants.filter((item) => !(isObject(item) && item.type === "null"));
103
+ const concrete = variants.filter((item) => !(isRecord(item) && item.type === "null"));
92
104
  if (concrete.length !== 1 || concrete.length === variants.length)
93
105
  continue;
94
106
  delete node[key];
95
- Object.assign(node, { nullable: true, ...(isObject(concrete[0]) ? concrete[0] : {}) });
107
+ Object.assign(node, { nullable: true, ...(isRecord(concrete[0]) ? concrete[0] : {}) });
96
108
  }
97
109
  }
98
110
  /** Combinators at the top level of a parameters schema; strict backends reject them outright. */
@@ -102,8 +114,8 @@ const LOCAL_POINTER = /^#\/(definitions|\$defs)\/([^/]+)$/;
102
114
  /** The `definitions` and `$defs` that a local pointer in this schema resolves against. */
103
115
  function poolsOf(parameters) {
104
116
  const defs = {};
105
- for (const key of ["definitions", "$defs"])
106
- if (isObject(parameters[key]))
117
+ for (const key of POOL_KEYS)
118
+ if (isRecord(parameters[key]))
107
119
  defs[key] = parameters[key];
108
120
  return defs;
109
121
  }
@@ -119,16 +131,16 @@ function poolsOf(parameters) {
119
131
  function resolveRef(node, defs) {
120
132
  const seen = new Set();
121
133
  let current = node;
122
- while (isObject(current) && typeof current.$ref === "string") {
134
+ while (isRecord(current) && typeof current.$ref === "string") {
123
135
  const pointer = current.$ref;
124
136
  const target = LOCAL_POINTER.exec(pointer);
125
137
  if (!target || seen.has(pointer))
126
138
  return undefined;
127
139
  seen.add(pointer);
128
140
  const pool = defs[target[1]];
129
- current = isObject(pool) ? pool[target[2]] : undefined;
141
+ current = isRecord(pool) ? pool[target[2]] : undefined;
130
142
  }
131
- return isObject(current) ? current : undefined;
143
+ return isRecord(current) ? current : undefined;
132
144
  }
133
145
  /**
134
146
  * Replaces a root-level `$ref` with what it points at.
@@ -164,18 +176,16 @@ function mergeRootAllOf(out) {
164
176
  if (!Array.isArray(branches))
165
177
  return;
166
178
  const defs = poolsOf(out);
167
- const properties = isObject(out.properties) ? { ...out.properties } : {};
168
- const required = new Set(Array.isArray(out.required) ? out.required.filter((name) => typeof name === "string") : []);
179
+ const properties = isRecord(out.properties) ? { ...out.properties } : {};
180
+ const required = new Set(requiredOf(out));
169
181
  for (const raw of branches) {
170
182
  const branch = resolveRef(raw, defs);
171
183
  if (!branch)
172
184
  continue;
173
- if (isObject(branch.properties))
185
+ if (isRecord(branch.properties))
174
186
  Object.assign(properties, branch.properties);
175
- if (Array.isArray(branch.required))
176
- for (const name of branch.required)
177
- if (typeof name === "string")
178
- required.add(name);
187
+ for (const name of requiredOf(branch))
188
+ required.add(name);
179
189
  }
180
190
  if (!Object.keys(properties).length)
181
191
  return;
@@ -214,22 +224,16 @@ function mergeRootUnion(out) {
214
224
  shared = new Set();
215
225
  continue;
216
226
  }
217
- if (isObject(branch.properties))
227
+ if (isRecord(branch.properties))
218
228
  Object.assign(properties, branch.properties);
219
- const names = Array.isArray(branch.required)
220
- ? branch.required.filter((name) => typeof name === "string")
221
- : [];
229
+ const names = requiredOf(branch);
222
230
  shared = previous === null ? new Set(names) : new Set(names.filter((n) => previous.has(n)));
223
231
  }
224
232
  if (!Object.keys(properties).length)
225
233
  continue;
226
- out.properties = { ...(isObject(out.properties) ? out.properties : {}), ...properties };
227
- if (shared?.size) {
228
- const already = Array.isArray(out.required)
229
- ? out.required.filter((name) => typeof name === "string")
230
- : [];
231
- out.required = [...new Set([...already, ...shared])];
232
- }
234
+ out.properties = { ...(isRecord(out.properties) ? out.properties : {}), ...properties };
235
+ if (shared?.size)
236
+ out.required = [...new Set([...requiredOf(out), ...shared])];
233
237
  }
234
238
  }
235
239
  /**
@@ -246,7 +250,7 @@ function collectRefs(node, into) {
246
250
  collectRefs(item, into);
247
251
  return;
248
252
  }
249
- if (!isObject(node))
253
+ if (!isRecord(node))
250
254
  return;
251
255
  for (const [key, value] of Object.entries(node)) {
252
256
  if (key === "$ref" && typeof value === "string")
@@ -269,7 +273,7 @@ function collectRefs(node, into) {
269
273
  * outside it still refers in.
270
274
  */
271
275
  function pruneDefs(out) {
272
- const pools = ["definitions", "$defs"].filter((key) => isObject(out[key]));
276
+ const pools = POOL_KEYS.filter((key) => isRecord(out[key]));
273
277
  if (!pools.length)
274
278
  return;
275
279
  const live = {};
@@ -282,7 +286,7 @@ function pruneDefs(out) {
282
286
  continue;
283
287
  const [, poolKey, name] = target;
284
288
  const pool = out[poolKey];
285
- if (!isObject(pool) || !(name in pool))
289
+ if (!isRecord(pool) || !(name in pool))
286
290
  continue;
287
291
  live[poolKey] ??= new Set();
288
292
  const names = live[poolKey];
@@ -317,24 +321,36 @@ function pruneDefs(out) {
317
321
  function pruneRequired(out) {
318
322
  if (!Array.isArray(out.required))
319
323
  return;
320
- const properties = isObject(out.properties) ? out.properties : {};
324
+ const properties = isRecord(out.properties) ? out.properties : {};
321
325
  const kept = out.required.filter((name) => typeof name === "string" && name in properties);
322
326
  if (kept.length)
323
327
  out.required = kept;
324
328
  else
325
329
  delete out.required;
326
330
  }
327
- function sanitizeParameters(parameters) {
328
- if (!isObject(parameters))
331
+ /**
332
+ * One JSON Schema as a strict server will accept it, for a schema that is not a tool's parameters.
333
+ *
334
+ * What `sanitizeTools` does to each tool, without the tool: a `response_format` schema goes through
335
+ * the same grammar converter a tool's parameters do, and a caller holding only the schema had to
336
+ * wrap it in a definition to get here. Nothing is remembered — the cache is keyed on a tool
337
+ * definition's identity, and a bare schema has no object that stands for it across requests.
338
+ *
339
+ * @param schema The schema as written. Never mutated. Its root is held to an object, as a tool's
340
+ * parameters are, and anything that is not a schema at all comes back as an object with no
341
+ * properties.
342
+ */
343
+ export function sanitizeSchema(schema) {
344
+ if (!isRecord(schema))
329
345
  return EMPTY_OBJECT();
330
- const out = normalize(inlineRootRef(parameters));
346
+ const out = normalize(inlineRootRef(schema));
331
347
  mergeRootAllOf(out);
332
348
  mergeRootUnion(out);
333
349
  for (const key of TOP_LEVEL_COMBINATORS)
334
350
  delete out[key];
335
351
  if (out.type !== "object")
336
352
  out.type = "object";
337
- if (!isObject(out.properties))
353
+ if (!isRecord(out.properties))
338
354
  out.properties = {};
339
355
  pruneRequired(out);
340
356
  pruneDefs(out);
@@ -386,7 +402,7 @@ const through = (cache, tools, fn) => tools.map((tool) => {
386
402
  * @param tools The definitions as the pool hands them over. Never mutated — where a schema
387
403
  * changed, a new definition is returned in its place.
388
404
  */
389
- export const sanitizeTools = (tools) => through(sanitized, tools, sanitizeParameters);
405
+ export const sanitizeTools = (tools) => through(sanitized, tools, sanitizeSchema);
390
406
  /**
391
407
  * Walked as a schema rather than as arbitrary JSON, because `pattern` and `format` are keyword
392
408
  * names and perfectly ordinary argument names at once. Matching on the key alone deleted a
@@ -394,29 +410,33 @@ export const sanitizeTools = (tools) => through(sanitized, tools, sanitizeParame
394
410
  * argument that no longer existed — which every strict validator rejects, so the retry produced
395
411
  * the failure it was reaching for. The same distinction keeps the walk out of `default`, `enum`
396
412
  * and `const`, whose contents are data, not schema.
397
- *
398
- * At module scope rather than inside `relaxTools`, so the closure is made once rather than per
399
- * call — which, on the path this is on, is per request.
400
413
  */
401
414
  const strip = (node) => {
402
415
  if (Array.isArray(node))
403
416
  return node.map(strip);
404
- if (!isObject(node))
417
+ if (!isRecord(node))
405
418
  return node;
406
419
  const out = {};
407
420
  for (const [key, value] of Object.entries(node)) {
408
421
  if (key === "pattern" || key === "format")
409
422
  continue;
410
- if (SCHEMA_KEYS.has(key))
411
- out[key] = Array.isArray(value) ? value.map(strip) : strip(value);
412
- else if (SCHEMA_MAPS.has(key) && isObject(value))
413
- // The keys here are argument names; only the values are schemas.
414
- out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, strip(sub)]));
415
- else
416
- out[key] = value;
423
+ out[key] = mapChildren(key, value, strip);
417
424
  }
418
425
  return out;
419
426
  };
427
+ /**
428
+ * One schema without its `pattern` and `format` keywords, for a schema that is not a tool's.
429
+ *
430
+ * The retry `relaxTools` makes, on a bare schema. Only the keywords go: a property that happens to
431
+ * be called `format` is an argument name and stays.
432
+ *
433
+ * @param schema Already sanitised. Relaxing is the retry, not a substitute for `sanitizeSchema`.
434
+ * Never mutated; anything that is not a schema comes back as an object with no properties.
435
+ */
436
+ export function relaxSchema(schema) {
437
+ const stripped = strip(schema);
438
+ return isRecord(stripped) ? stripped : EMPTY_OBJECT();
439
+ }
420
440
  /**
421
441
  * The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
422
442
  * `pattern` and most `format` values, both of which only ever narrowed a string the tool
@@ -424,10 +444,7 @@ const strip = (node) => {
424
444
  *
425
445
  * @param tools Already sanitised. Relaxing is the retry, not a substitute for `sanitizeTools`.
426
446
  */
427
- export const relaxTools = (tools) => through(relaxed, tools, (parameters) => {
428
- const stripped = strip(parameters);
429
- return isObject(stripped) ? stripped : EMPTY_OBJECT();
430
- });
447
+ export const relaxTools = (tools) => through(relaxed, tools, relaxSchema);
431
448
  /**
432
449
  * Qwen chat templates raise this when the transcript has no user turn. Some servers wrap it
433
450
  * in the same "unable to generate parser" wording as a real schema failure, and stripping
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Everything one runtime remembers: each module's state, under a key only that module holds.
3
+ *
4
+ * A map rather than a record of named caches, so a module that grows a cache declares it where
5
+ * it is used and `runtime.ts` does not have to hear about it.
6
+ */
7
+ export type Scope = Map<symbol, unknown>;
8
+ /** The default runtime's scope: what the top-level functions use when no runtime is running. */
9
+ export declare const rootScope: Scope;
10
+ /** The scope of the runtime whose method is running, or the default one outside any. */
11
+ export declare const currentScope: () => Scope;
12
+ /**
13
+ * Runs `fn` with `scope` as the current one, for it and for everything it awaits or schedules.
14
+ *
15
+ * @param scope The runtime's state. Nested calls replace it for their own duration only.
16
+ * @param fn What to run. Its return value, or what it throws, is handed straight back.
17
+ */
18
+ export declare const inScope: <T>(scope: Scope, fn: () => T) => T;
19
+ /** One module's state, read from whichever scope is current. See `scoped`. */
20
+ export interface Scoped<T> {
21
+ /** The current scope's copy, made on first use. */
22
+ (): T;
23
+ /** Drops the current scope's copy, so the next read makes a fresh one. */
24
+ reset(): void;
25
+ }
26
+ /**
27
+ * Declares a piece of module state that every runtime has its own copy of.
28
+ *
29
+ * What used to be `const cache = new Map()` at the top of a module is `scoped(() => new Map())`,
30
+ * and each use reads `cache()`. The copy is made lazily, so a runtime pays only for the modules
31
+ * it touches.
32
+ *
33
+ * @param create Builds a fresh, empty copy. Called once per scope, and again after `reset`.
34
+ */
35
+ export declare function scoped<T>(create: () => T): Scoped<T>;
package/dist/scope.js ADDED
@@ -0,0 +1,42 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ /**
3
+ * Which scope the code running now belongs to, carried across every `await` beneath a runtime's
4
+ * method. Async context rather than a parameter, because the state is read five calls down —
5
+ * `runAgentLoop` to `runTurn` to `negotiate` to `capabilitiesFor` — and threading it would put a
6
+ * runtime argument on every exported function in between.
7
+ */
8
+ const active = new AsyncLocalStorage();
9
+ /** The default runtime's scope: what the top-level functions use when no runtime is running. */
10
+ export const rootScope = new Map();
11
+ /** The scope of the runtime whose method is running, or the default one outside any. */
12
+ export const currentScope = () => active.getStore() ?? rootScope;
13
+ /**
14
+ * Runs `fn` with `scope` as the current one, for it and for everything it awaits or schedules.
15
+ *
16
+ * @param scope The runtime's state. Nested calls replace it for their own duration only.
17
+ * @param fn What to run. Its return value, or what it throws, is handed straight back.
18
+ */
19
+ export const inScope = (scope, fn) => active.run(scope, fn);
20
+ /**
21
+ * Declares a piece of module state that every runtime has its own copy of.
22
+ *
23
+ * What used to be `const cache = new Map()` at the top of a module is `scoped(() => new Map())`,
24
+ * and each use reads `cache()`. The copy is made lazily, so a runtime pays only for the modules
25
+ * it touches.
26
+ *
27
+ * @param create Builds a fresh, empty copy. Called once per scope, and again after `reset`.
28
+ */
29
+ export function scoped(create) {
30
+ const key = Symbol();
31
+ const read = () => {
32
+ const scope = currentScope();
33
+ if (!scope.has(key))
34
+ scope.set(key, create());
35
+ return scope.get(key);
36
+ };
37
+ return Object.assign(read, {
38
+ reset: () => {
39
+ currentScope().delete(key);
40
+ },
41
+ });
42
+ }
@@ -126,6 +126,11 @@ export declare function tryAsk<T>(label: string, run: () => Promise<T>, { onNoti
126
126
  * Models are asked for JSON and often answer with prose around it, or a fenced block. Pull out
127
127
  * the first array or object rather than failing the task over a wrapper.
128
128
  *
129
+ * What is pulled out is read the way a tool call's arguments are: as written where that parses,
130
+ * and otherwise with the almost-JSON a local model writes repaired — single quotes, `True` and
131
+ * `None`, bare keys, a trailing comma. A small model asked for a list gets these wrong as often in
132
+ * a reply as in a call, and a side task that fails over one costs the run a second request.
133
+ *
129
134
  * @param text The reply, fences and prose included. Nothing parseable gives `undefined`.
130
135
  */
131
136
  export declare function parseJson<T>(text: string): T | undefined;