@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/dist/guards.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The checks a value read off the wire is put through before anything is read from it.
3
+ *
4
+ * A leaf with no imports, because the modules that need these — a browser-safe `spec`, the schema
5
+ * walk, the tool-call reader, the snapshot importer — otherwise share nothing, and each had grown
6
+ * its own copy.
7
+ */
8
+ /**
9
+ * Whether a value is a plain keyed object: not null, and not an array.
10
+ *
11
+ * @param value Anything, usually just parsed. An array is an object to `typeof` and not to this.
12
+ */
13
+ export const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
+ /**
15
+ * Whether a value is a number above zero, which is what a limit has to be to be one.
16
+ *
17
+ * `Infinity` passes and `NaN` does not, so a caller lifting a bound and one handing over a
18
+ * half-built config are told apart without either being asked.
19
+ *
20
+ * @param value Anything, usually a setting that may be absent, zero or mistyped.
21
+ */
22
+ export const isPositive = (value) => typeof value === "number" && value > 0;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The points in a session a hook can be bound to, apart from everything that runs one.
3
+ *
4
+ * A leaf with no imports, because two modules need these lists and only one of them may load
5
+ * `node:crypto`: `hooks.ts` hashes a turn into the uuid a memory server dedups on, and `spec.ts`
6
+ * has to stay importable in a browser. Before this each kept its own copy, with a test holding
7
+ * one pair together and nothing holding the other.
8
+ */
9
+ /**
10
+ * A point in a session a hook can be bound to. Named after Claude Code's hooks of the same shape
11
+ * — `sessionStart` (SessionStart), `beforeTurn` (UserPromptSubmit), `afterTurn` (Stop),
12
+ * `beforeCompact` (PreCompact), `sessionEnd` (SessionEnd) — plus `sessionDelete`, for when the host
13
+ * deletes a session's record.
14
+ */
15
+ export type HookEvent = "sessionStart" | "beforeTurn" | "afterTurn" | "beforeCompact" | "sessionEnd" | "sessionDelete";
16
+ /** Every event a hook can be bound to, in the order a session meets them. */
17
+ export declare const HOOK_EVENTS: readonly HookEvent[];
18
+ /**
19
+ * The events whose hooks run before a request, and so the only ones whose output can reach it.
20
+ * Anything later runs once the model has already answered.
21
+ */
22
+ export declare const INJECT_EVENTS: ReadonlySet<HookEvent>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The points in a session a hook can be bound to, apart from everything that runs one.
3
+ *
4
+ * A leaf with no imports, because two modules need these lists and only one of them may load
5
+ * `node:crypto`: `hooks.ts` hashes a turn into the uuid a memory server dedups on, and `spec.ts`
6
+ * has to stay importable in a browser. Before this each kept its own copy, with a test holding
7
+ * one pair together and nothing holding the other.
8
+ */
9
+ /** Every event a hook can be bound to, in the order a session meets them. */
10
+ export const HOOK_EVENTS = [
11
+ "sessionStart",
12
+ "beforeTurn",
13
+ "afterTurn",
14
+ "beforeCompact",
15
+ "sessionEnd",
16
+ "sessionDelete",
17
+ ];
18
+ /**
19
+ * The events whose hooks run before a request, and so the only ones whose output can reach it.
20
+ * Anything later runs once the model has already answered.
21
+ */
22
+ export const INJECT_EVENTS = new Set(["sessionStart", "beforeTurn"]);
package/dist/hooks.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type OpenAI from "openai";
2
+ import { type HookEvent } from "./hook-events.ts";
2
3
  /**
3
4
  * Lifecycle hooks, from the host's side: what a session looks like to them, where their context
4
5
  * lands in a request, and what is said about each one.
@@ -9,20 +10,6 @@ import type OpenAI from "openai";
9
10
  * `runHooks` is a runner as it stands and its outcomes pass straight through, without this
10
11
  * package depending on it.
11
12
  */
12
- /**
13
- * A point in a session a hook can be bound to. Named after Claude Code's hooks of the same shape
14
- * — `sessionStart` (SessionStart), `beforeTurn` (UserPromptSubmit), `afterTurn` (Stop),
15
- * `beforeCompact` (PreCompact), `sessionEnd` (SessionEnd) — plus `sessionDelete`, for when the host
16
- * deletes a session's record.
17
- */
18
- export type HookEvent = "sessionStart" | "beforeTurn" | "afterTurn" | "beforeCompact" | "sessionEnd" | "sessionDelete";
19
- /** Every event a hook can be bound to, in the order a session meets them. */
20
- export declare const HOOK_EVENTS: readonly HookEvent[];
21
- /**
22
- * The events whose hooks run before a request, and so the only ones whose output can reach it.
23
- * Anything later runs once the model has already answered.
24
- */
25
- export declare const INJECT_EVENTS: ReadonlySet<HookEvent>;
26
13
  /**
27
14
  * One message of a session as a hook is handed it. The shape a memory server's `remember` takes,
28
15
  * so a template can pass a turn straight through.
@@ -160,8 +147,8 @@ export interface HookOptions {
160
147
  * Changes what hooks are held to, for a process whose windows are not the size these defaults
161
148
  * were chosen for, or whose host wants its own name above the context.
162
149
  *
163
- * Module-level for the same reason `configureEvents` is: a budget and a preface are a deployment's
164
- * settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
150
+ * One setting per runtime for the same reason `configureEvents` is: a budget and a preface are a
151
+ * deployment's settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
165
152
  * window can afford more recall than an 8k one — passes `maxTokens` to `gather` instead, and a
166
153
  * `preface` passed to `withContext` or `runAgentLoop`'s hooks wins over this one the same way.
167
154
  *
@@ -248,6 +235,18 @@ export declare const UNTRUSTED_PREFACE: string;
248
235
  export declare function untrusted(text: string, { source }?: {
249
236
  source?: string;
250
237
  }): string;
238
+ /**
239
+ * A message's text, whether its content is a string or a list of parts.
240
+ *
241
+ * Parts are joined with nothing between them, because they are one text an API split rather than
242
+ * several: a space put in is a character the sender never wrote, and `turnMessages` hashes this
243
+ * into the uuid a memory server dedups on. Compaction reads the same function, so the summariser
244
+ * and the hook told about a fold see one message the same way. A refusal part is left out.
245
+ *
246
+ * @param content A message's `content`, in any of the shapes the API allows. One that is neither
247
+ * a string nor a list reads as empty.
248
+ */
249
+ export declare const textOf: (content: unknown) => string;
251
250
  /**
252
251
  * A stretch of a transcript as a hook reads it: what the user and the assistant said, and nothing
253
252
  * else.
package/dist/hooks.js CHANGED
@@ -1,20 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { errorMessage } from "./errors.js";
3
- import { estimateTokens } from "./tokens.js";
4
- /** Every event a hook can be bound to, in the order a session meets them. */
5
- export const HOOK_EVENTS = [
6
- "sessionStart",
7
- "beforeTurn",
8
- "afterTurn",
9
- "beforeCompact",
10
- "sessionEnd",
11
- "sessionDelete",
12
- ];
13
- /**
14
- * The events whose hooks run before a request, and so the only ones whose output can reach it.
15
- * Anything later runs once the model has already answered.
16
- */
17
- export const INJECT_EVENTS = new Set(["sessionStart", "beforeTurn"]);
3
+ import { isPositive } from "./guards.js";
4
+ import { INJECT_EVENTS } from "./hook-events.js";
5
+ import { scoped } from "./scope.js";
6
+ import { CHARS_PER_TOKEN, estimateTokens } from "./tokens.js";
18
7
  /**
19
8
  * The most context all of a request's hooks add between them by default, in estimated tokens.
20
9
  *
@@ -36,13 +25,13 @@ const HOOK_DEFAULTS = {
36
25
  preface: HOOK_PREFACE,
37
26
  };
38
27
  /** What is in force now. Read where it is used, so a change applies from the next request. */
39
- let hookSettings = { ...HOOK_DEFAULTS };
28
+ const hookSettings = scoped(() => ({ ...HOOK_DEFAULTS }));
40
29
  /**
41
30
  * Changes what hooks are held to, for a process whose windows are not the size these defaults
42
31
  * were chosen for, or whose host wants its own name above the context.
43
32
  *
44
- * Module-level for the same reason `configureEvents` is: a budget and a preface are a deployment's
45
- * settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
33
+ * One setting per runtime for the same reason `configureEvents` is: a budget and a preface are a
34
+ * deployment's settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
46
35
  * window can afford more recall than an 8k one — passes `maxTokens` to `gather` instead, and a
47
36
  * `preface` passed to `withContext` or `runAgentLoop`'s hooks wins over this one the same way.
48
37
  *
@@ -54,12 +43,11 @@ let hookSettings = { ...HOOK_DEFAULTS };
54
43
  */
55
44
  export function configureHooks(options = {}) {
56
45
  const { contextTokens, preface } = options;
57
- if (typeof contextTokens === "number" && contextTokens > 0) {
58
- hookSettings.contextTokens = contextTokens;
59
- }
46
+ if (isPositive(contextTokens))
47
+ hookSettings().contextTokens = contextTokens;
60
48
  if (typeof preface === "string")
61
- hookSettings.preface = preface;
62
- return { ...hookSettings };
49
+ hookSettings().preface = preface;
50
+ return { ...hookSettings() };
63
51
  }
64
52
  /**
65
53
  * Test seam: puts `configureHooks` back to the defaults, so one test's budget or preface is not the
@@ -67,15 +55,26 @@ export function configureHooks(options = {}) {
67
55
  * `resetAll` calls it.
68
56
  */
69
57
  export const resetHooks = () => {
70
- hookSettings = { ...HOOK_DEFAULTS };
58
+ hookSettings.reset();
71
59
  };
72
60
  /**
73
61
  * The budget a call is held to: its own when it gave a usable one, the process's otherwise. The
74
62
  * same rule `configureHooks` applies, so a `0` threaded through for "no opinion" does not quietly
75
63
  * turn every hook's context off.
76
64
  */
77
- const budget = (given) => typeof given === "number" && given > 0 ? given : hookSettings.contextTokens;
65
+ const budget = (given) => (isPositive(given) ? given : hookSettings().contextTokens);
78
66
  const attribute = (text) => text.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
67
+ /** What every note about an outcome opens with: which hook it was, and on which event. */
68
+ const noteOf = (outcome) => ({
69
+ event: outcome.event,
70
+ source: outcome.label,
71
+ hookId: outcome.hookId,
72
+ });
73
+ /** The note a failed outcome leaves, the same whichever of the three callers reports it. */
74
+ const failureNote = (outcome) => ({
75
+ ...noteOf(outcome),
76
+ error: outcome.error ?? "failed",
77
+ });
79
78
  /**
80
79
  * Builds the context a set of outcomes adds and the notes that go with it.
81
80
  *
@@ -99,9 +98,8 @@ export function assembleContext(outcomes, maxTokens) {
99
98
  const notes = [];
100
99
  let remaining = budget(maxTokens);
101
100
  for (const outcome of outcomes) {
102
- const base = { event: outcome.event, source: outcome.label, hookId: outcome.hookId };
103
101
  if (!outcome.ok) {
104
- notes.push({ ...base, error: outcome.error ?? "failed" });
102
+ notes.push(failureNote(outcome));
105
103
  continue;
106
104
  }
107
105
  if (!outcome.inject || !INJECT_EVENTS.has(outcome.event))
@@ -113,11 +111,11 @@ export function assembleContext(outcomes, maxTokens) {
113
111
  if (cap <= 0)
114
112
  continue;
115
113
  if (estimateTokens(text) > cap)
116
- text = `${text.slice(0, cap * 4 - 1).trimEnd()}…`;
114
+ text = `${text.slice(0, cap * CHARS_PER_TOKEN - 1).trimEnd()}…`;
117
115
  const tokens = estimateTokens(text);
118
116
  remaining -= tokens;
119
117
  blocks.push(`<context source="${attribute(outcome.label)}">\n${text}\n</context>`);
120
- notes.push({ ...base, tokens, text });
118
+ notes.push({ ...noteOf(outcome), tokens, text });
121
119
  }
122
120
  return { context: blocks.join("\n\n"), notes };
123
121
  }
@@ -143,7 +141,7 @@ export function assembleContext(outcomes, maxTokens) {
143
141
  * unless something moved it. Empty says nothing, rather than leaving a blank line where it was.
144
142
  * @returns `history` when there was nothing to add or nowhere to add it, otherwise a new array.
145
143
  */
146
- export function withContext(history, index, context, preface = hookSettings.preface) {
144
+ export function withContext(history, index, context, preface = hookSettings().preface) {
147
145
  const message = history[index];
148
146
  if (!context || message?.role !== "user")
149
147
  return history;
@@ -186,8 +184,18 @@ export function untrusted(text, { source } = {}) {
186
184
  const attrs = source === undefined ? "" : ` source="${attribute(source)}"`;
187
185
  return `<untrusted${attrs}>\n${text.replace(UNTRUSTED_TAG, "&lt;$1")}\n</untrusted>`;
188
186
  }
189
- /** A message's text, whether its content is a string or a list of parts. */
190
- const textOf = (content) => {
187
+ /**
188
+ * A message's text, whether its content is a string or a list of parts.
189
+ *
190
+ * Parts are joined with nothing between them, because they are one text an API split rather than
191
+ * several: a space put in is a character the sender never wrote, and `turnMessages` hashes this
192
+ * into the uuid a memory server dedups on. Compaction reads the same function, so the summariser
193
+ * and the hook told about a fold see one message the same way. A refusal part is left out.
194
+ *
195
+ * @param content A message's `content`, in any of the shapes the API allows. One that is neither
196
+ * a string nor a list reads as empty.
197
+ */
198
+ export const textOf = (content) => {
191
199
  if (typeof content === "string")
192
200
  return content;
193
201
  if (!Array.isArray(content))
@@ -317,9 +325,7 @@ export async function gather(run, events, context, { signal, onNote, maxTokens,
317
325
  */
318
326
  export async function notify(run, event, context, onNote) {
319
327
  const outcomes = await runSafely(run, event, context);
320
- const notes = outcomes
321
- .filter((outcome) => !outcome.ok)
322
- .map((outcome) => assembleContext([outcome]).notes[0]);
328
+ const notes = outcomes.filter((outcome) => !outcome.ok).map(failureNote);
323
329
  for (const note of notes)
324
330
  onNote?.(note);
325
331
  return notes;
@@ -344,15 +350,9 @@ export async function consult(run, event, context, onNote) {
344
350
  const notes = [];
345
351
  for (const outcome of outcomes) {
346
352
  if (!outcome.ok)
347
- notes.push(assembleContext([outcome]).notes[0]);
348
- else if (outcome.veto === true) {
349
- notes.push({
350
- event: outcome.event,
351
- source: outcome.label,
352
- hookId: outcome.hookId,
353
- veto: true,
354
- });
355
- }
353
+ notes.push(failureNote(outcome));
354
+ else if (outcome.veto === true)
355
+ notes.push({ ...noteOf(outcome), veto: true });
356
356
  }
357
357
  for (const note of notes)
358
358
  onNote?.(note);
package/dist/index.d.ts CHANGED
@@ -19,17 +19,19 @@ export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } fro
19
19
  export { type ContinueTurnOptions, continueTurn, isContinuable, } from "./continuation.ts";
20
20
  export { AgentLoopError, type AgentLoopFailure, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from "./errors.ts";
21
21
  export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunMetrics, type RunMetricsOptions, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from "./events.ts";
22
- export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, type HookContext, type HookEvent, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.ts";
22
+ export { HOOK_EVENTS, type 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";
23
24
  export { estimateFrom, type LedgerEntry, type LedgerEstimateOptions, type LedgerRequest, rebaseLedger, recordRequest, type TokenLedger, tokensBetween, } from "./ledger.ts";
24
25
  export { resetAll } from "./reset.ts";
25
- export { backoffMs, CHARS_PER_TOKEN, type ContextBreakdown, type ContextBreakdownOptions, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, type TokenEstimateOptions, toolsChars, } from "./retry.ts";
26
+ export { backoffMs, type ContextBreakdown, type ContextBreakdownOptions, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, type TokenEstimateOptions, toolsChars, } from "./retry.ts";
26
27
  export { type RunTurnOptions, runTurn } from "./run-turn.ts";
27
- export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.ts";
28
+ export { createRuntime, defaultRuntime, type Runtime, type RuntimeOptions } from "./runtime.ts";
29
+ export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from "./schema-compat.ts";
28
30
  export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTask, type SideTaskInput, type SideTaskOptions, tryAsk, } from "./side-task.ts";
29
31
  export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from "./snapshot.ts";
30
32
  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, RESOLVED_DEFAULTS, type ResolvedAgent, type ResolvedTask, type RetrySpec, resolveAgentSpec, SPEC_EVENTS, type SpecBundle, type SpecServer, type TaskSpec, type ToolsSpec, } from "./spec.ts";
31
33
  export { type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from "./stream.ts";
32
34
  export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, type Split, stripThinking, THINK_FENCE, } from "./thinking.ts";
33
- export { estimateTokens } from "./tokens.ts";
35
+ export { CHARS_PER_TOKEN, estimateTokens } from "./tokens.ts";
34
36
  export { parseToolArguments, recoverToolCalls, ToolArgumentsError, type ToolCall, } from "./tool-calls.ts";
35
37
  export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, KEYWORD_DROPOFF, KEYWORD_MIN_SCORE, type KeywordPreselection, type KeywordPreselectOptions, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, orderTools, PRESELECT_SCHEMA, PRESELECT_SYSTEM, PROXY_TOOLS, preselectByKeywords, preselectInput, preselection, preselectSystem, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, type ToolMatch, type ToolOrder, } from "./tool-loading.ts";
package/dist/index.js CHANGED
@@ -17,17 +17,19 @@ export { applyCompaction, COMPACT_AT, compactTranscript, KEEP_RATIO, planCompact
17
17
  export { continueTurn, isContinuable, } from "./continuation.js";
18
18
  export { AgentLoopError, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from "./errors.js";
19
19
  export { configureEvents, emit, endRun, fold, history, resetEvents, runMetrics, watch, } from "./events.js";
20
- export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.js";
20
+ export { HOOK_EVENTS, INJECT_EVENTS } from "./hook-events.js";
21
+ export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.js";
21
22
  export { estimateFrom, rebaseLedger, recordRequest, tokensBetween, } from "./ledger.js";
22
23
  export { resetAll } from "./reset.js";
23
- export { backoffMs, CHARS_PER_TOKEN, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, toolsChars, } from "./retry.js";
24
+ export { backoffMs, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, toolsChars, } from "./retry.js";
24
25
  export { runTurn } from "./run-turn.js";
25
- export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.js";
26
+ export { createRuntime, defaultRuntime } from "./runtime.js";
27
+ export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from "./schema-compat.js";
26
28
  export { ask, askJson, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
27
29
  export { CAPABILITY_SNAPSHOT_VERSION, exportCapabilities, importCapabilities, } from "./snapshot.js";
28
30
  export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, exportSpec, parseSpec, RESOLVED_DEFAULTS, resolveAgentSpec, SPEC_EVENTS, } from "./spec.js";
29
31
  export { streamTurn, } from "./stream.js";
30
32
  export { ALL_FENCES, DEFAULT_FENCES, FenceSplitter, stripThinking, THINK_FENCE, } from "./thinking.js";
31
- export { estimateTokens } from "./tokens.js";
33
+ export { CHARS_PER_TOKEN, estimateTokens } from "./tokens.js";
32
34
  export { parseToolArguments, recoverToolCalls, ToolArgumentsError, } from "./tool-calls.js";
33
35
  export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, KEYWORD_DROPOFF, KEYWORD_MIN_SCORE, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, orderTools, PRESELECT_SCHEMA, PRESELECT_SYSTEM, PROXY_TOOLS, preselectByKeywords, preselectInput, preselection, preselectSystem, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, } from "./tool-loading.js";
package/dist/reset.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Forgets everything this package remembers between calls.
3
3
  *
4
- * Six modules here keep state for the life of the process, each for a good reason and each
5
- * with its own seam: the pooled clients and their model listings, the endpoints that turned
4
+ * Six modules here keep state for the life of the runtime — the process's, unless this is called
5
+ * as a method of one `createRuntime` made, which clears that one alone — each for a good reason
6
+ * and each with its own seam: the pooled clients and their model listings, the endpoints that turned
6
7
  * out not to take `stream_options` or a grammar, each model's measured characters per token,
7
8
  * the models that refused the no-thinking hints, the event bus, and the hooks' configured
8
9
  * budget and preface. `resetClients`, `resetCapabilities`, `resetCalibration`, `resetHints`,
@@ -18,5 +19,8 @@
18
19
  *
19
20
  * It is also the seam that does not need finding again. A seventh module with a cache is a seventh
20
21
  * line here, rather than an edit to the teardown of three consumers who will not all notice.
22
+ *
23
+ * A test that builds a `createRuntime` per case has nothing to tear down, and is the better
24
+ * answer where cases run in parallel: this clears state every concurrent caller is sharing.
21
25
  */
22
26
  export declare function resetAll(): void;
package/dist/reset.js CHANGED
@@ -7,8 +7,9 @@ import { resetHints } from "./side-task.js";
7
7
  /**
8
8
  * Forgets everything this package remembers between calls.
9
9
  *
10
- * Six modules here keep state for the life of the process, each for a good reason and each
11
- * with its own seam: the pooled clients and their model listings, the endpoints that turned
10
+ * Six modules here keep state for the life of the runtime — the process's, unless this is called
11
+ * as a method of one `createRuntime` made, which clears that one alone — each for a good reason
12
+ * and each with its own seam: the pooled clients and their model listings, the endpoints that turned
12
13
  * out not to take `stream_options` or a grammar, each model's measured characters per token,
13
14
  * the models that refused the no-thinking hints, the event bus, and the hooks' configured
14
15
  * budget and preface. `resetClients`, `resetCapabilities`, `resetCalibration`, `resetHints`,
@@ -24,6 +25,9 @@ import { resetHints } from "./side-task.js";
24
25
  *
25
26
  * It is also the seam that does not need finding again. A seventh module with a cache is a seventh
26
27
  * line here, rather than an edit to the teardown of three consumers who will not all notice.
28
+ *
29
+ * A test that builds a `createRuntime` per case has nothing to tear down, and is the better
30
+ * answer where cases run in parallel: this clears state every concurrent caller is sharing.
27
31
  */
28
32
  export function resetAll() {
29
33
  resetClients();
package/dist/retry.d.ts CHANGED
@@ -29,13 +29,6 @@ export declare class ContextOverflow extends Error {
29
29
  * @param tokens The count to render.
30
30
  */
31
31
  export declare const compact: (tokens: number) => string;
32
- /**
33
- * The divisor behind `estimateTokens`, applied here to a character count rather than a string.
34
- *
35
- * The fallback, not the rule: once a turn has come back with a reported prompt count, `runTurn`
36
- * divides by what that endpoint's model was measured at instead. See `charsPerTokenFor`.
37
- */
38
- export declare const CHARS_PER_TOKEN = 4;
39
32
  /** What the two token estimates below take besides what they measure. */
40
33
  export interface TokenEstimateOptions {
41
34
  /**
@@ -192,6 +185,19 @@ export declare const SMALLEST_LIKELY_WINDOW = 8192;
192
185
  * @param error The rejection, as caught. What is not an SDK error is not transient.
193
186
  */
194
187
  export declare function isTransient(error: unknown): boolean;
188
+ /**
189
+ * Whether a failure is the endpoint refusing the request as written, rather than losing it.
190
+ *
191
+ * 400 and 422 are what a server says when it read the body and disliked it, which makes them the
192
+ * only statuses worth answering by sending something different. Any 4xx is too wide: 401, 404 and
193
+ * 429 are 4xx and none of them is about the fields, and a 429 is one `isTransient` accepts — a
194
+ * caller that re-sent on it at once doubled the rate against a server that had just asked for less.
195
+ * Catching everything is wider still, since an abort or a connection that never landed then
196
+ * latches off whatever the re-send left out.
197
+ *
198
+ * @param error The rejection, as caught. What is not an SDK error refuses nothing.
199
+ */
200
+ export declare const refusesRequest: (error: unknown) => boolean;
195
201
  /**
196
202
  * Whether a failure is a local server still loading the model, rather than one failing to serve.
197
203
  *
package/dist/retry.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import OpenAI from "openai";
2
+ import { CHARS_PER_TOKEN } from "./tokens.js";
2
3
  /**
3
4
  * Everything about a request failing that is not about what the request said.
4
5
  *
@@ -62,13 +63,6 @@ const REFUSAL_PART = 32;
62
63
  const REASONING_KEY = 23;
63
64
  /** The same for `"reasoning":"",`, OpenRouter's spelling of it. */
64
65
  const REASONING_ALT_KEY = 15;
65
- /**
66
- * The divisor behind `estimateTokens`, applied here to a character count rather than a string.
67
- *
68
- * The fallback, not the rule: once a turn has come back with a reported prompt count, `runTurn`
69
- * divides by what that endpoint's model was measured at instead. See `charsPerTokenFor`.
70
- */
71
- export const CHARS_PER_TOKEN = 4;
72
66
  /** The divisor an option asked for, or the fallback when it asked for nothing usable. */
73
67
  const divisor = (charsPerToken) => charsPerToken !== undefined && charsPerToken > 0 ? charsPerToken : CHARS_PER_TOKEN;
74
68
  /**
@@ -349,6 +343,19 @@ export function isTransient(error) {
349
343
  const { status } = error;
350
344
  return status === 408 || status === 409 || status === 429 || (status ?? 0) >= 500;
351
345
  }
346
+ /**
347
+ * Whether a failure is the endpoint refusing the request as written, rather than losing it.
348
+ *
349
+ * 400 and 422 are what a server says when it read the body and disliked it, which makes them the
350
+ * only statuses worth answering by sending something different. Any 4xx is too wide: 401, 404 and
351
+ * 429 are 4xx and none of them is about the fields, and a 429 is one `isTransient` accepts — a
352
+ * caller that re-sent on it at once doubled the rate against a server that had just asked for less.
353
+ * Catching everything is wider still, since an abort or a connection that never landed then
354
+ * latches off whatever the re-send left out.
355
+ *
356
+ * @param error The rejection, as caught. What is not an SDK error refuses nothing.
357
+ */
358
+ export const refusesRequest = (error) => error instanceof OpenAI.APIError && (error.status === 400 || error.status === 422);
352
359
  /**
353
360
  * Whether a failure is a local server still loading the model, rather than one failing to serve.
354
361
  *
@@ -77,6 +77,13 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
77
77
  */
78
78
  loadingTimeoutMs?: number;
79
79
  }
80
+ /**
81
+ * Builds a turn's request body from what the endpoint and the model have refused so far.
82
+ *
83
+ * A function rather than a body because a downgrade changes what is sent, so it is called again
84
+ * per attempt. Its second argument is absent where no model was named.
85
+ */
86
+ export type RequestBuilder = (supports: Capabilities, model: ModelCapabilities | undefined) => OpenAI.ChatCompletionCreateParamsStreaming;
80
87
  /**
81
88
  * `request` is a callback rather than a body because the body has to be rebuilt from whatever
82
89
  * the last attempt latched off: the tools it sends depend on `strictSchemas`, and `relaxTools`
@@ -90,4 +97,4 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
90
97
  * @param options Retry budget, context limit, the model to negotiate for, notices, and the
91
98
  * stream's own callbacks.
92
99
  */
93
- export declare function runTurn(client: OpenAI, supports: Capabilities, request: (supports: Capabilities, model: ModelCapabilities | undefined) => OpenAI.ChatCompletionCreateParamsStreaming, { maxRetries, onNotice, contextLimit, model, droppable, loadingTimeoutMs, ...stream }?: RunTurnOptions): Promise<Turn>;
100
+ export declare function runTurn(client: OpenAI, supports: Capabilities, request: RequestBuilder, { maxRetries, onNotice, contextLimit, model, droppable, loadingTimeoutMs, ...stream }?: RunTurnOptions): Promise<Turn>;
@@ -0,0 +1,120 @@
1
+ import { preselect, runAgentLoop } from "./agent-loop.ts";
2
+ import { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.ts";
3
+ import { capabilitiesFor, expireCapabilities, resetCapabilities } from "./capabilities.ts";
4
+ import { type ClientPoolOptions, configureClients, contextLimitFor, getClient, listModels, resetClients, servedWindow } from "./client.ts";
5
+ import { compactTranscript, runCompaction, summariser } from "./compaction.ts";
6
+ import { continueTurn } from "./continuation.ts";
7
+ import { configureEvents, type EventBusOptions, emit, endRun, history, resetEvents, watch } from "./events.ts";
8
+ import { assembleContext, configureHooks, consult, gather, type HookOptions, notify, resetHooks, withContext } from "./hooks.ts";
9
+ import { resetAll } from "./reset.ts";
10
+ import { runTurn } from "./run-turn.ts";
11
+ import { ask, askJson, resetHints, tryAsk } from "./side-task.ts";
12
+ import { exportCapabilities, importCapabilities } from "./snapshot.ts";
13
+ /**
14
+ * Every exported function that reads or writes what a runtime remembers, itself or through what
15
+ * it calls. The rest of the package is pure, and is the same function whichever runtime it is
16
+ * called beside.
17
+ */
18
+ declare const STATEFUL: {
19
+ runAgentLoop: typeof runAgentLoop;
20
+ preselect: typeof preselect;
21
+ runTurn: typeof runTurn;
22
+ continueTurn: typeof continueTurn;
23
+ ask: typeof ask;
24
+ askJson: typeof askJson;
25
+ tryAsk: typeof tryAsk;
26
+ summariser: typeof summariser;
27
+ runCompaction: typeof runCompaction;
28
+ compactTranscript: typeof compactTranscript;
29
+ getClient: typeof getClient;
30
+ listModels: typeof listModels;
31
+ contextLimitFor: typeof contextLimitFor;
32
+ servedWindow: typeof servedWindow;
33
+ configureClients: typeof configureClients;
34
+ capabilitiesFor: typeof capabilitiesFor;
35
+ expireCapabilities: typeof expireCapabilities;
36
+ exportCapabilities: typeof exportCapabilities;
37
+ importCapabilities: typeof importCapabilities;
38
+ calibrate: typeof calibrate;
39
+ charsPerTokenFor: typeof charsPerTokenFor;
40
+ emit: typeof emit;
41
+ endRun: typeof endRun;
42
+ history: typeof history;
43
+ watch: typeof watch;
44
+ configureEvents: typeof configureEvents;
45
+ configureHooks: typeof configureHooks;
46
+ gather: typeof gather;
47
+ notify: typeof notify;
48
+ consult: typeof consult;
49
+ assembleContext: typeof assembleContext;
50
+ withContext: typeof withContext;
51
+ resetClients: typeof resetClients;
52
+ resetCapabilities: typeof resetCapabilities;
53
+ resetCalibration: typeof resetCalibration;
54
+ resetHints: typeof resetHints;
55
+ resetEvents: typeof resetEvents;
56
+ resetHooks: typeof resetHooks;
57
+ resetAll: typeof resetAll;
58
+ };
59
+ /** What a runtime starts out configured with. Every field optional; see `createRuntime`. */
60
+ export interface RuntimeOptions {
61
+ /** The client pool's bounds, as `configureClients` takes them. */
62
+ clients?: ClientPoolOptions;
63
+ /** The event bus's bounds, as `configureEvents` takes them. */
64
+ events?: EventBusOptions;
65
+ /** The hooks' budget and preface, as `configureHooks` takes them. */
66
+ hooks?: HookOptions;
67
+ }
68
+ /**
69
+ * One set of the package's caches, with the functions that use them as methods.
70
+ *
71
+ * Each method is the top-level function of the same name, with the same signature and doc
72
+ * comment, run against this runtime's state instead of the process's: its pooled clients and
73
+ * model listings, latched capabilities, measured characters per token, no-thinking hints, event
74
+ * bus, and hook settings. `resetAll` and the six narrower resets clear this runtime alone.
75
+ */
76
+ export type Runtime = typeof STATEFUL & {
77
+ /**
78
+ * Runs `fn` with this runtime current, so every top-level function it calls — directly, after
79
+ * an `await`, or from a timer it sets — uses this runtime's state. What the methods do, for
80
+ * code that calls the top-level functions itself.
81
+ *
82
+ * It does not reach a callback that outlives `fn` and is called from elsewhere — a generator
83
+ * resumed later, a function handed back and called by someone else. Call a method there.
84
+ *
85
+ * @param fn What to run. Its return value, or what it throws, is handed straight back.
86
+ */
87
+ run<T>(fn: () => T): T;
88
+ };
89
+ /**
90
+ * A runtime of its own: separate clients, listings, capability latches, hints, event bus and hook
91
+ * settings from the process's and from every other runtime's.
92
+ *
93
+ * For a host that must not share them — one that mints a key per tenant, where a shared pool has
94
+ * tenants evicting each other's clients and a shared bus has one tenant's run id readable by
95
+ * another — and for tests, where a runtime per case needs no `resetAll` between them and can run
96
+ * in parallel. A host with one deployment's worth of endpoints does not need one: the top-level
97
+ * functions are the default runtime's methods.
98
+ *
99
+ * The runtime is carried by async context rather than by argument, so `runAgentLoop` called as a
100
+ * method uses this runtime all the way down, and so does a top-level function the host calls from
101
+ * inside it — from `dispatch`, a hook, `onEvent`. Outside a method or `run`, a top-level function
102
+ * is the default runtime's again; a request handler that emits to a run a runtime started calls
103
+ * `runtime.emit`.
104
+ *
105
+ * Nothing needs closing. A runtime is garbage once nothing holds it; its one timer, the event
106
+ * bus's sweep, is unreferenced and stops rescheduling when its last run has been swept.
107
+ *
108
+ * @param options What to configure it with before first use. A part left out keeps the
109
+ * defaults — not the default runtime's settings, which a new runtime does not inherit.
110
+ */
111
+ export declare function createRuntime(options?: RuntimeOptions): Runtime;
112
+ /**
113
+ * The runtime the top-level functions use when no other is running.
114
+ *
115
+ * `defaultRuntime.getClient(endpoint)` and `getClient(endpoint)` are the same call outside any
116
+ * runtime. Inside one they differ: the top-level function follows the runtime that is running,
117
+ * and this always reaches the process-wide state.
118
+ */
119
+ export declare const defaultRuntime: Runtime;
120
+ export {};