@cubicecho/agent-core 2.3.0 → 2.4.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.
package/README.md CHANGED
@@ -273,6 +273,22 @@ host that stores what the user typed never stores the context as something they
273
273
  injecting hook shares `HOOK_CONTEXT_TOKENS` (2000) by default, each held to its own `maxTokens`
274
274
  inside that, so a generous hook cannot crowd out the conversation it was meant to inform.
275
275
 
276
+ The budget is the caller's to move, at either level. `configureHooks` sets it for the process, and
277
+ `maxTokens` on `gather` (or the second argument to `assembleContext`) sets it for one request and
278
+ wins over it — the one to reach for when the budget follows the model, since a 128k window can
279
+ afford more recall than an 8k one:
280
+
281
+ ```ts
282
+ import { configureHooks, gather } from "@cubicecho/agent-core";
283
+
284
+ configureHooks({ contextTokens: 4000 });
285
+ const gathered = await gather(run, ["beforeTurn"], context, { maxTokens: contextLimit / 20 });
286
+ ```
287
+
288
+ Either one given something that is not a number above zero keeps what was there, as
289
+ `configureEvents` does, so a `0` threaded through for "no opinion" does not switch recall off.
290
+ `resetHooks` (and `resetAll`) puts the default back.
291
+
276
292
  Neither function rejects. A hook failing is an outcome, and a runner that throws outright is
277
293
  noted once for its event and costs only that event's context. `notify` takes no signal: a reader
278
294
  who leaves once the turn is answered has not asked for it not to be remembered.
package/dist/hooks.d.ts CHANGED
@@ -122,12 +122,40 @@ export interface Gathered {
122
122
  notes: HookNote[];
123
123
  }
124
124
  /**
125
- * The most context all of a request's hooks add between them, in estimated tokens.
125
+ * The most context all of a request's hooks add between them by default, in estimated tokens.
126
126
  *
127
127
  * Enough for a handful of recalled memories, and small against any window worth running an agent
128
128
  * in. The point is that a generous hook cannot crowd out the conversation it was meant to inform.
129
+ * `configureHooks` moves it for a process, and `gather` and `assembleContext` for one request.
129
130
  */
130
131
  export declare const HOOK_CONTEXT_TOKENS = 2000;
132
+ /** What hooks are held to across a process. Every field optional; see `configureHooks`. */
133
+ export interface HookOptions {
134
+ /**
135
+ * The budget every injecting hook shares, when a call does not give its own. Each hook is still
136
+ * held to its own `maxTokens` inside it.
137
+ */
138
+ contextTokens?: number;
139
+ }
140
+ /**
141
+ * Changes what hooks are held to, for a process whose windows are not the size these defaults
142
+ * were chosen for.
143
+ *
144
+ * Module-level for the same reason `configureEvents` is: a budget is a deployment's setting, said
145
+ * once at startup. A caller that sizes it per model or per agent — a 128k window can afford more
146
+ * recall than an 8k one — passes `maxTokens` to `gather` instead, which wins over this.
147
+ *
148
+ * @param options The limits to change. A field left out — or given anything that is not a number
149
+ * above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
150
+ * above zero, and lifts the shared budget entirely.
151
+ * @returns Everything in force afterwards, including what this call did not change.
152
+ */
153
+ export declare function configureHooks(options?: HookOptions): Required<HookOptions>;
154
+ /**
155
+ * Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the next's.
156
+ * `resetAll` calls it.
157
+ */
158
+ export declare const resetHooks: () => void;
131
159
  /**
132
160
  * Said once, above the blocks, so the model reads them as background rather than instructions.
133
161
  * Names no host; `withContext` takes another for one that wants to.
@@ -148,7 +176,8 @@ export declare const HOOK_PREFACE: string;
148
176
  *
149
177
  * @param outcomes What the runners returned. An injecting outcome on an event that cannot inject
150
178
  * adds nothing; a failed one is noted wherever it falls, including past the budget.
151
- * @param maxTokens The budget every block shares. Defaults to `HOOK_CONTEXT_TOKENS`.
179
+ * @param maxTokens The budget every block shares. Absent, or not a number above zero, is what
180
+ * `configureHooks` last set — `HOOK_CONTEXT_TOKENS` unless something moved it.
152
181
  */
153
182
  export declare function assembleContext(outcomes: readonly HookOutcome[], maxTokens?: number): Gathered;
154
183
  /**
@@ -217,7 +246,8 @@ export declare const turnIndex: (messages: readonly {
217
246
  * @param context What the hooks are told.
218
247
  * @param options `signal` is handed to the runner, and should be the turn's own: a user who
219
248
  * stopped the turn stopped its recall. `onNote` hears each note as the whole is assembled.
220
- * `maxTokens` is the shared budget, `HOOK_CONTEXT_TOKENS` if absent.
249
+ * `maxTokens` is the shared budget for this request, read as `assembleContext` reads it: absent
250
+ * or unusable is the process's, from `configureHooks`.
221
251
  */
222
252
  export declare function gather(run: HookRunner, events: readonly HookEvent[], context: HookContext, { signal, onNote, maxTokens, }?: {
223
253
  signal?: AbortSignal;
package/dist/hooks.js CHANGED
@@ -16,12 +16,50 @@ export const HOOK_EVENTS = [
16
16
  */
17
17
  export const INJECT_EVENTS = new Set(["sessionStart", "beforeTurn"]);
18
18
  /**
19
- * The most context all of a request's hooks add between them, in estimated tokens.
19
+ * The most context all of a request's hooks add between them by default, in estimated tokens.
20
20
  *
21
21
  * Enough for a handful of recalled memories, and small against any window worth running an agent
22
22
  * in. The point is that a generous hook cannot crowd out the conversation it was meant to inform.
23
+ * `configureHooks` moves it for a process, and `gather` and `assembleContext` for one request.
23
24
  */
24
25
  export const HOOK_CONTEXT_TOKENS = 2000;
26
+ /** The numbers this module was written with. */
27
+ const HOOK_DEFAULTS = { contextTokens: HOOK_CONTEXT_TOKENS };
28
+ /** What is in force now. Read where it is used, so a change applies from the next request. */
29
+ let hookLimits = { ...HOOK_DEFAULTS };
30
+ /**
31
+ * Changes what hooks are held to, for a process whose windows are not the size these defaults
32
+ * were chosen for.
33
+ *
34
+ * Module-level for the same reason `configureEvents` is: a budget is a deployment's setting, said
35
+ * once at startup. A caller that sizes it per model or per agent — a 128k window can afford more
36
+ * recall than an 8k one — passes `maxTokens` to `gather` instead, which wins over this.
37
+ *
38
+ * @param options The limits to change. A field left out — or given anything that is not a number
39
+ * above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
40
+ * above zero, and lifts the shared budget entirely.
41
+ * @returns Everything in force afterwards, including what this call did not change.
42
+ */
43
+ export function configureHooks(options = {}) {
44
+ for (const [name, value] of Object.entries(options)) {
45
+ if (typeof value === "number" && value > 0)
46
+ hookLimits[name] = value;
47
+ }
48
+ return { ...hookLimits };
49
+ }
50
+ /**
51
+ * Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the next's.
52
+ * `resetAll` calls it.
53
+ */
54
+ export const resetHooks = () => {
55
+ hookLimits = { ...HOOK_DEFAULTS };
56
+ };
57
+ /**
58
+ * The budget a call is held to: its own when it gave a usable one, the process's otherwise. The
59
+ * same rule `configureHooks` applies, so a `0` threaded through for "no opinion" does not quietly
60
+ * turn every hook's context off.
61
+ */
62
+ const budget = (given) => typeof given === "number" && given > 0 ? given : hookLimits.contextTokens;
25
63
  /**
26
64
  * Said once, above the blocks, so the model reads them as background rather than instructions.
27
65
  * Names no host; `withContext` takes another for one that wants to.
@@ -44,12 +82,13 @@ const attribute = (text) => text.replaceAll("&", "&amp;").replaceAll('"', "&quot
44
82
  *
45
83
  * @param outcomes What the runners returned. An injecting outcome on an event that cannot inject
46
84
  * adds nothing; a failed one is noted wherever it falls, including past the budget.
47
- * @param maxTokens The budget every block shares. Defaults to `HOOK_CONTEXT_TOKENS`.
85
+ * @param maxTokens The budget every block shares. Absent, or not a number above zero, is what
86
+ * `configureHooks` last set — `HOOK_CONTEXT_TOKENS` unless something moved it.
48
87
  */
49
- export function assembleContext(outcomes, maxTokens = HOOK_CONTEXT_TOKENS) {
88
+ export function assembleContext(outcomes, maxTokens) {
50
89
  const blocks = [];
51
90
  const notes = [];
52
- let remaining = maxTokens;
91
+ let remaining = budget(maxTokens);
53
92
  for (const outcome of outcomes) {
54
93
  const base = { event: outcome.event, source: outcome.label, hookId: outcome.hookId };
55
94
  if (!outcome.ok) {
@@ -182,7 +221,8 @@ const runSafely = (run, event, context, signal) => Promise.resolve()
182
221
  * @param context What the hooks are told.
183
222
  * @param options `signal` is handed to the runner, and should be the turn's own: a user who
184
223
  * stopped the turn stopped its recall. `onNote` hears each note as the whole is assembled.
185
- * `maxTokens` is the shared budget, `HOOK_CONTEXT_TOKENS` if absent.
224
+ * `maxTokens` is the shared budget for this request, read as `assembleContext` reads it: absent
225
+ * or unusable is the process's, from `configureHooks`.
186
226
  */
187
227
  export async function gather(run, events, context, { signal, onNote, maxTokens, } = {}) {
188
228
  const outcomes = await Promise.all(events.map((event) => runSafely(run, event, context, signal)));
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { contextLimitFor, getClient, listModels, type ModelInfo, NO_KEY, resetCl
15
15
  export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } from "./config.ts";
16
16
  export { errorMessage } from "./errors.ts";
17
17
  export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunUsage, resetEvents, watch, } from "./events.ts";
18
- export { assembleContext, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, type HookContext, type HookEvent, type HookMessage, type HookNote, type HookOutcome, type HookRunner, INJECT_EVENTS, notify, turnIndex, turnMessages, withContext, } from "./hooks.ts";
18
+ export { assembleContext, configureHooks, 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, withContext, } from "./hooks.ts";
19
19
  export { resetAll } from "./reset.ts";
20
20
  export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.ts";
21
21
  export { type RunTurnOptions, runTurn } from "./run-turn.ts";
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ export { capabilitiesFor, modelCapabilitiesFor, negotiate, resetCapabilities, }
13
13
  export { contextLimitFor, getClient, listModels, NO_KEY, resetClients, timeoutMs, } from "./client.js";
14
14
  export { errorMessage } from "./errors.js";
15
15
  export { configureEvents, emit, endRun, fold, history, resetEvents, watch, } from "./events.js";
16
- export { assembleContext, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, turnIndex, turnMessages, withContext, } from "./hooks.js";
16
+ export { assembleContext, configureHooks, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, withContext, } from "./hooks.js";
17
17
  export { resetAll } from "./reset.js";
18
18
  export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
19
19
  export { runTurn } from "./run-turn.js";
package/dist/reset.d.ts CHANGED
@@ -1,20 +1,21 @@
1
1
  /**
2
2
  * Forgets everything this package remembers between calls.
3
3
  *
4
- * Four modules here keep state for the life of the process, each for a good reason and each
4
+ * Five modules here keep state for the life of the process, each for a good reason and each
5
5
  * with its own seam: the pooled clients and their model listings, the endpoints that turned
6
6
  * out not to take `stream_options` or a grammar, the models that refused the no-thinking
7
- * hints, and the event bus. `resetClients`, `resetCapabilities`, `resetHints` and `resetEvents` stay
8
- * exported, because a test that means to clear one thing should say so.
7
+ * hints, the event bus, and the hooks' configured budget. `resetClients`, `resetCapabilities`,
8
+ * `resetHints`, `resetEvents` and `resetHooks` stay exported, because a test that means to clear
9
+ * one thing should say so.
9
10
  *
10
- * This is for the other case, which is every teardown. What all four hold is *latched
11
+ * This is for the other case, which is every teardown. What they hold is *latched
11
12
  * refusals* — a fact one test taught the process about an endpoint, still true as far as the
12
13
  * next test can tell. Miss one and the suite becomes order-dependent in the way that passes
13
14
  * locally and fails in CI on a different shard: the test that latched it still passes, and the
14
15
  * one that reads the latch fails only when it happens to run second. `tests/side-task-hints.test.ts`
15
16
  * was written that way and only passed because every case had been handed a hostname of its own.
16
17
  *
17
- * It is also the seam that does not need finding again. A fifth module with a cache is a fifth
18
+ * It is also the seam that does not need finding again. A sixth module with a cache is a sixth
18
19
  * line here, rather than an edit to the teardown of three consumers who will not all notice.
19
20
  */
20
21
  export declare function resetAll(): void;
package/dist/reset.js CHANGED
@@ -1,24 +1,26 @@
1
1
  import { resetCapabilities } from "./capabilities.js";
2
2
  import { resetClients } from "./client.js";
3
3
  import { resetEvents } from "./events.js";
4
+ import { resetHooks } from "./hooks.js";
4
5
  import { resetHints } from "./side-task.js";
5
6
  /**
6
7
  * Forgets everything this package remembers between calls.
7
8
  *
8
- * Four modules here keep state for the life of the process, each for a good reason and each
9
+ * Five modules here keep state for the life of the process, each for a good reason and each
9
10
  * with its own seam: the pooled clients and their model listings, the endpoints that turned
10
11
  * out not to take `stream_options` or a grammar, the models that refused the no-thinking
11
- * hints, and the event bus. `resetClients`, `resetCapabilities`, `resetHints` and `resetEvents` stay
12
- * exported, because a test that means to clear one thing should say so.
12
+ * hints, the event bus, and the hooks' configured budget. `resetClients`, `resetCapabilities`,
13
+ * `resetHints`, `resetEvents` and `resetHooks` stay exported, because a test that means to clear
14
+ * one thing should say so.
13
15
  *
14
- * This is for the other case, which is every teardown. What all four hold is *latched
16
+ * This is for the other case, which is every teardown. What they hold is *latched
15
17
  * refusals* — a fact one test taught the process about an endpoint, still true as far as the
16
18
  * next test can tell. Miss one and the suite becomes order-dependent in the way that passes
17
19
  * locally and fails in CI on a different shard: the test that latched it still passes, and the
18
20
  * one that reads the latch fails only when it happens to run second. `tests/side-task-hints.test.ts`
19
21
  * was written that way and only passed because every case had been handed a hostname of its own.
20
22
  *
21
- * It is also the seam that does not need finding again. A fifth module with a cache is a fifth
23
+ * It is also the seam that does not need finding again. A sixth module with a cache is a sixth
22
24
  * line here, rather than an edit to the teardown of three consumers who will not all notice.
23
25
  */
24
26
  export function resetAll() {
@@ -26,4 +28,5 @@ export function resetAll() {
26
28
  resetCapabilities();
27
29
  resetHints();
28
30
  resetEvents();
31
+ resetHooks();
29
32
  }
package/llms.txt CHANGED
@@ -73,19 +73,22 @@ What a run is doing, while it is doing it.
73
73
  Lifecycle hooks, from the host's side: what a session looks like to them, where their context lands in a request, and what is said about each one.
74
74
 
75
75
  - `assembleContext` — Builds the context a set of outcomes adds and the notes that go with it.
76
+ - `configureHooks` — Changes what hooks are held to, for a process whose windows are not the size these defaults were chosen for.
76
77
  - `Gathered` (type) — The context a set of outcomes adds to a request, and a note for each hook worth mentioning.
77
78
  - `gather` — Runs the hooks ahead of a request and builds what they add to it.
78
- - `HOOK_CONTEXT_TOKENS` — The most context all of a request's hooks add between them, in estimated tokens.
79
+ - `HOOK_CONTEXT_TOKENS` — The most context all of a request's hooks add between them by default, in estimated tokens.
79
80
  - `HOOK_EVENTS` — Every event a hook can be bound to, in the order a session meets them.
80
81
  - `HOOK_PREFACE` — Said once, above the blocks, so the model reads them as background rather than instructions.
81
82
  - `HookContext` (type) — What a host knows at an event.
82
83
  - `HookEvent` (type) — A point in a session a hook can be bound to.
83
84
  - `HookMessage` (type) — One message of a session as a hook is handed it.
84
85
  - `HookNote` (type) — One hook's line for whoever is watching: the context it added, or why it added none.
86
+ - `HookOptions` (type) — What hooks are held to across a process.
85
87
  - `HookOutcome` (type) — What one hook did.
86
88
  - `HookRunner` (type) — Runs one event's hooks.
87
89
  - `INJECT_EVENTS` — The events whose hooks run before a request, and so the only ones whose output can reach it.
88
90
  - `notify` — Runs the hooks for an event that reads what happened and adds nothing to a request.
91
+ - `resetHooks` — Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the next's.
89
92
  - `turnIndex` — Which turn of a session begins at a point, from 0: the user messages ahead of it.
90
93
  - `turnMessages` — A stretch of a transcript as a hook reads it: what the user and the assistant said, and nothing else.
91
94
  - `withContext` — The request, with the hooks' context added to this turn's question.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
5
5
  "keywords": [
6
6
  "openai",