@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
package/README.md CHANGED
@@ -22,7 +22,7 @@ only, Node >=22.
22
22
 
23
23
  | Module | What it does |
24
24
  | --- | --- |
25
- | `schema-compat` | Makes an MCP tool schema something a strict or grammar-constrained server will accept. `sanitizeTools`, `relaxTools`, `isGrammarError`. |
25
+ | `schema-compat` | Makes an MCP tool schema something a strict or grammar-constrained server will accept. `sanitizeTools`, `relaxTools`, `isGrammarError`, and `sanitizeSchema` and `relaxSchema` for a schema that is not a tool's. |
26
26
  | `tool-loading` | On-demand tool discovery: a name-only catalogue plus a `load_tools` meta-tool, so a run pays for the schemas it asks for instead of all of them. Plus `preselectByKeywords`, which picks from it without a model, and the proxied form of the same thing — `PROXY_TOOLS`, `proxyLoadResult`, `proxiedCall` — for a server whose prompt cache a growing tool array throws away. |
27
27
  | `stream` | Reads one streamed turn back into a message: token callbacks, tool-call reassembly, fenced reasoning taken out of the answer, and the idle watchdog that turns a silent endpoint into `EndpointSilent`. |
28
28
  | `capabilities` | What an endpoint turned out not to support — and, under it, what one model on that endpoint did not — plus the loop that answers either when it says so. `capabilitiesFor`, `modelCapabilitiesFor`, `negotiate`. |
@@ -43,6 +43,7 @@ only, Node >=22.
43
43
  | `snapshot` | `exportCapabilities` and `importCapabilities`: the latched refusals as a JSON blob a consumer stores, so a restart need not learn them again. |
44
44
  | `spec` | `parseSpec`, `resolveAgentSpec` and `exportSpec`: an agent as a versioned JSON document, layered into the flat config the loop takes. Imports nothing but types, and is published separately at `@cubicecho/agent-core/spec`. |
45
45
  | `reset` | `resetAll`: drops every cache and latch in one call, so a teardown cannot forget one. |
46
+ | `runtime` | `createRuntime`: a second set of those caches, with the functions that use them as methods — for a host whose tenants must not share a client pool, a latch or an event bus. `defaultRuntime` is the one the top-level functions use. |
46
47
  | `tokens` | `estimateTokens`: characters over four, deliberately low, for everything here that has to guess at a window. |
47
48
  | `errors` | `errorMessage`: a caught `unknown` turned into something a run row can hold. And what `runAgentLoop` throws, with the run as it stood: `AgentLoopError`, `ToolIterationLimit`, `AgentLoopOverflow`, and `failedRun` to read it off any of them. |
48
49
  | `catalog` | `CatalogServer`: the name-only shape `tool-loading` reads a connected server as. |
@@ -231,7 +232,9 @@ tasks and model listings, the same way.
231
232
  hold the reply to, so a small model that wraps JSON in prose on its own cannot do so here. The
232
233
  schema is normalised the way a tool's parameters are, and relaxed where the endpoint could not
233
234
  build a grammar, because llama.cpp reads both with the same converter. It also rides on the system
234
- prompt, and the reply goes through `parseJson` either way.
235
+ prompt, and the reply goes through `parseJson` either way — which finds the JSON inside prose or a
236
+ fence and repairs what a local model gets nearly right: single quotes, `True` and `None`, bare keys,
237
+ a trailing comma.
235
238
 
236
239
  ```ts
237
240
  const picked = await askJson<{ tools: string[] }>(config, small, system, request, PRESELECT_SCHEMA, {
@@ -1197,8 +1200,9 @@ const result = await runAgentLoop({ config, preselect: { keywords: true }, compa
1197
1200
 
1198
1201
  Four caches outlive any one run: the `OpenAI` clients, the model listings, the latched
1199
1202
  capabilities, and `side-task`'s no-thinking hints. A fifth, the characters per token each model was
1200
- measured at, is keyed on the endpoint's capabilities object, so it goes with them. All four are module-level and keyed on the same
1201
- notion of an endpoint — its base URL and its API key — and the clients' key carries the request
1203
+ measured at, is keyed on the endpoint's capabilities object, so it goes with them. All four are
1204
+ held once per runtime — once per process, unless you make another, [below](#a-runtime-of-its-own)
1205
+ — and keyed on the same notion of an endpoint — its base URL and its API key — and the clients' key carries the request
1202
1206
  timeout as well, since that changes how a request is sent.
1203
1207
 
1204
1208
  **They are keyed per deployment, not per request.** What belongs in them is an endpoint an
@@ -1209,8 +1213,8 @@ bounding them assumes that, and a consumer that mints an API key per *user* brea
1209
1213
 
1210
1214
  That is bounded rather than unbounded: the client pool is a 32-entry LRU, and an evicted client
1211
1215
  costs its connection pool and nothing else, since the next request through that endpoint builds
1212
- another. But churning connection pools is not sharing them, and at that point a client of your
1213
- own, built and held per tenant, is the better answer than this.
1216
+ another. But churning connection pools is not sharing them, and at that point a runtime per
1217
+ tenant, [below](#a-runtime-of-its-own), is the better answer than this.
1214
1218
 
1215
1219
  The listings cache also remembers, for half a minute, that an endpoint did not name a model — the
1216
1220
  case where a configured name never matches anything the server serves (a llama.cpp `-a` alias, an
@@ -1284,6 +1288,57 @@ carries it in the snapshot so an imported latch keeps its real age instead of be
1284
1288
  every boot. Importing takes the older of the two ages, and a snapshot written before this field
1285
1289
  existed reads as met now.
1286
1290
 
1291
+ ### A runtime of its own
1292
+
1293
+ Everything above is the *default runtime*: one set of caches, shared by every caller in the
1294
+ process. `createRuntime` makes another, with its own clients, listings, latches, hints, measured
1295
+ ratios, event bus and hook settings, and the functions that use them as methods of the same names
1296
+ and signatures:
1297
+
1298
+ ```ts
1299
+ import { createRuntime } from "@cubicecho/agent-core";
1300
+
1301
+ const runtime = createRuntime({ clients: { maxClients: 8 }, events: { maxEvents: 200 } });
1302
+
1303
+ const run = await runtime.runAgentLoop({ config, messages, dispatch });
1304
+ for await (const event of runtime.watch(runId)) send(event);
1305
+ runtime.resetAll(); // this runtime's caches, and nobody else's
1306
+ ```
1307
+
1308
+ It is for the host the section above warns off — one that mints a key per tenant, where a shared
1309
+ pool has tenants evicting each other's clients and a shared bus makes one tenant's run id readable
1310
+ by another — and for tests, where a runtime per case needs no `resetAll` between cases and lets
1311
+ them run in parallel. A host with one deployment's worth of endpoints needs none: the top-level
1312
+ functions are the default runtime's methods, and nothing about them has changed.
1313
+
1314
+ The runtime travels by async context (`AsyncLocalStorage`), not by argument. A method runs its
1315
+ function with the runtime current, and it stays current across every `await` beneath it — so
1316
+ `runtime.runAgentLoop` uses the runtime's clients and latches all the way down, and so does a
1317
+ top-level function your code calls from inside it: an `emit` in `onEvent`, an `ask` in `dispatch`,
1318
+ a `gather` in a hook. `runtime.run(fn)` does the same for code that calls the top-level functions
1319
+ itself:
1320
+
1321
+ ```ts
1322
+ await runtime.run(async () => {
1323
+ const limit = await contextLimitFor(config); // the runtime's listing cache
1324
+ emit(runId, { kind: "output", text: "…" }); // the runtime's bus
1325
+ });
1326
+ ```
1327
+
1328
+ Outside a method or `run`, a top-level function is the default runtime's again. That is the one
1329
+ thing to keep straight: a request handler that watches or emits to a run a runtime started is not
1330
+ inside it, and calls `runtime.watch` or `runtime.emit`. What a method hands back keeps its runtime
1331
+ — the iterator from `runtime.watch`, the function from `runtime.summariser` — wherever it is read.
1332
+
1333
+ A new runtime starts from the package's defaults plus what `createRuntime` was given; it does not
1334
+ inherit what `configureClients` and the others set on the default one. Nothing needs closing: a
1335
+ runtime is garbage once nothing holds it. `defaultRuntime` is the process's own as an object, for
1336
+ code that takes a `Runtime` and should be handed the shared one.
1337
+
1338
+ The two schema caches (`sanitizeTools`, `relaxTools`) and `requestTokens`' tool-length cache stay
1339
+ process-wide. They are memoised pure functions keyed on the caller's own objects, so there is
1340
+ nothing in them for one tenant to learn about another.
1341
+
1287
1342
  ## Where the merged behaviour came from
1288
1343
 
1289
1344
  - `schema-compat` — `kanban_server`/`task_server`'s version, which strips **every** sibling of a
@@ -4,10 +4,11 @@ import type { CatalogServer } from "./catalog.ts";
4
4
  import { type CompactionOptions } from "./compaction.ts";
5
5
  import type { Endpoint, ModelParams, RetryPolicy, ToolPolicy } from "./config.ts";
6
6
  import { type RunEventInput, type RunMetrics } from "./events.ts";
7
- import { type HookContext, type HookEvent, type HookNote, type HookRunner } from "./hooks.ts";
7
+ import type { HookEvent } from "./hook-events.ts";
8
+ import { type HookContext, type HookNote, type HookRunner } from "./hooks.ts";
8
9
  import { type TokenLedger } from "./ledger.ts";
9
10
  import { type SideTask, type SideTaskOptions } from "./side-task.ts";
10
- import type { Turn, TurnUsage } from "./stream.ts";
11
+ import { type Turn, type TurnUsage } from "./stream.ts";
11
12
  import { type KeywordPreselectOptions, type ToolOrder } from "./tool-loading.ts";
12
13
  /**
13
14
  * The one place a streamed request's body is decided from a config and what the endpoint and