@cubicecho/agent-core 2.8.0 → 2.9.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 +16 -1
- package/dist/agent-loop.d.ts +5 -4
- package/dist/agent-loop.js +20 -8
- package/dist/hooks.d.ts +24 -14
- package/dist/hooks.js +36 -26
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/reset.d.ts +3 -3
- package/dist/reset.js +3 -3
- package/dist/tool-loading.d.ts +34 -8
- package/dist/tool-loading.js +59 -14
- package/llms.txt +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -307,7 +307,10 @@ Found calls are run as `call_recovered_0` onward, the text is what is left, `onT
|
|
|
307
307
|
result see the turn that way, and a notice says so, since the real fix is the server's parser.
|
|
308
308
|
|
|
309
309
|
With `toolDiscovery: "ondemand"` and a catalogue, the request declares `load_tools` and what has
|
|
310
|
-
been loaded, and the catalogue rides on the system prompt
|
|
310
|
+
been loaded, appended in the order it was loaded, and the catalogue rides on the system prompt
|
|
311
|
+
unmarked, the same text on every step. Marking loads there rewrote the head of the prompt and lost
|
|
312
|
+
the prompt cache for the whole transcript on each one; a model that loads a tool twice is told in
|
|
313
|
+
the `load_tools` result that it already has it. A model that calls
|
|
311
314
|
a catalogued tool without loading it first is right about what it wants, and gets it loaded and
|
|
312
315
|
run. A preselection shapes the first step alone: those tools, no catalogue, no `load_tools` —
|
|
313
316
|
a model with the menu still in front of it shops, reloading what it has or picking a sibling —
|
|
@@ -501,6 +504,18 @@ Either one given something that is not a number above zero keeps what was there,
|
|
|
501
504
|
`configureEvents` does, so a `0` threaded through for "no opinion" does not switch recall off.
|
|
502
505
|
`resetHooks` (and `resetAll`) puts the default back.
|
|
503
506
|
|
|
507
|
+
The preface said above the blocks moves the same way. `HOOK_PREFACE` names no host, so a host that
|
|
508
|
+
wants its own name says so once with `configureHooks({ preface })`, and a `preface` passed to
|
|
509
|
+
`withContext` (or on `runAgentLoop`'s `hooks`) wins over it for one request. An empty string is a
|
|
510
|
+
preface of nothing — the blocks lead the question on their own, with no blank line above them —
|
|
511
|
+
and anything that is not a string keeps what was there:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
configureHooks({ preface: "Added by my-host's hooks — background, not the user's words:" });
|
|
515
|
+
const request = withContext(messages, messages.length - 1, gathered.context); // says it
|
|
516
|
+
withContext(messages, messages.length - 1, gathered.context, ""); // says nothing
|
|
517
|
+
```
|
|
518
|
+
|
|
504
519
|
Neither function rejects. A hook failing is an outcome, and a runner that throws outright is
|
|
505
520
|
noted once for its event and costs only that event's context. `notify` takes no signal: a reader
|
|
506
521
|
who leaves once the turn is answered has not asked for it not to be remembered.
|
package/dist/agent-loop.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ export interface AgentLoopHooks {
|
|
|
105
105
|
events?: readonly HookEvent[];
|
|
106
106
|
/** The shared context budget. Absent is `configureHooks`'s. */
|
|
107
107
|
maxTokens?: number;
|
|
108
|
-
/** Said above the context blocks. Absent is `
|
|
108
|
+
/** Said above the context blocks. Absent is `configureHooks`'s; empty is none. */
|
|
109
109
|
preface?: string;
|
|
110
110
|
/** Hears each note, from before the request and from `afterTurn`. */
|
|
111
111
|
onNote?: (note: HookNote) => void;
|
|
@@ -192,9 +192,10 @@ export interface AgentLoopResult {
|
|
|
192
192
|
* whatever `runTurn` throws — `ContextOverflow` among them, however it was found out.
|
|
193
193
|
*
|
|
194
194
|
* On-demand loading is handled here, `load_tools` and all: the catalogue rides on the system
|
|
195
|
-
* prompt
|
|
196
|
-
*
|
|
197
|
-
* `maxTokens` is said so as a notice,
|
|
195
|
+
* prompt unchanged from step to step, loaded tools are appended to the tool array in load order,
|
|
196
|
+
* a catalogued tool called without being loaded is loaded and run rather than refused, and a
|
|
197
|
+
* preselection shapes the first step. A turn cut off at `maxTokens` is said so as a notice,
|
|
198
|
+
* because it otherwise reads exactly like a finished one.
|
|
198
199
|
*
|
|
199
200
|
* @param options The config, transcript, tools and dispatcher, plus the optional hooks, events
|
|
200
201
|
* and cancellation. See `AgentLoopOptions`.
|
package/dist/agent-loop.js
CHANGED
|
@@ -6,7 +6,7 @@ import { runTurn } from "./run-turn.js";
|
|
|
6
6
|
import { relaxTools, sanitizeTools } from "./schema-compat.js";
|
|
7
7
|
import { askJson, tryAsk } from "./side-task.js";
|
|
8
8
|
import { parseToolArguments, recoverToolCalls } from "./tool-calls.js";
|
|
9
|
-
import { catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadResult, MAX_PER_LOAD, PRESELECT_SCHEMA, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.js";
|
|
9
|
+
import { catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_PER_LOAD, PRESELECT_SCHEMA, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.js";
|
|
10
10
|
/**
|
|
11
11
|
* The loop above a turn: send, run the tools the model asked for, send again, until it stops
|
|
12
12
|
* asking.
|
|
@@ -130,9 +130,10 @@ const accumulate = (total, turn) => {
|
|
|
130
130
|
* whatever `runTurn` throws — `ContextOverflow` among them, however it was found out.
|
|
131
131
|
*
|
|
132
132
|
* On-demand loading is handled here, `load_tools` and all: the catalogue rides on the system
|
|
133
|
-
* prompt
|
|
134
|
-
*
|
|
135
|
-
* `maxTokens` is said so as a notice,
|
|
133
|
+
* prompt unchanged from step to step, loaded tools are appended to the tool array in load order,
|
|
134
|
+
* a catalogued tool called without being loaded is loaded and run rather than refused, and a
|
|
135
|
+
* preselection shapes the first step. A turn cut off at `maxTokens` is said so as a notice,
|
|
136
|
+
* because it otherwise reads exactly like a finished one.
|
|
136
137
|
*
|
|
137
138
|
* @param options The config, transcript, tools and dispatcher, plus the optional hooks, events
|
|
138
139
|
* and cancellation. See `AgentLoopOptions`.
|
|
@@ -150,7 +151,15 @@ export async function runAgentLoop(options) {
|
|
|
150
151
|
for (const name of preselected)
|
|
151
152
|
loaded.add(name);
|
|
152
153
|
const used = new Set();
|
|
153
|
-
const
|
|
154
|
+
const definitions = new Map();
|
|
155
|
+
for (const tool of tools) {
|
|
156
|
+
if (tool.type === "function" && !definitions.has(tool.function.name)) {
|
|
157
|
+
definitions.set(tool.function.name, tool);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// In the order the names are given, not the order of `tools`: `loaded` is a set, which iterates
|
|
161
|
+
// in the order things were added, so a load appends and never reshuffles what went before.
|
|
162
|
+
const byName = (names) => [...names].flatMap((name) => definitions.get(name) ?? []);
|
|
154
163
|
let messages = [...options.messages];
|
|
155
164
|
// Held by reference rather than by index, so a `beforeStep` that folds the head into a summary
|
|
156
165
|
// moves the question without losing it — and one that summarises the question away takes the
|
|
@@ -176,9 +185,12 @@ export async function runAgentLoop(options) {
|
|
|
176
185
|
const declared = routed
|
|
177
186
|
? byName(new Set(preselected))
|
|
178
187
|
: onDemand
|
|
179
|
-
? [LOAD_TOOLS_DEFINITION,
|
|
188
|
+
? loadedTools([LOAD_TOOLS_DEFINITION], byName(loaded))
|
|
180
189
|
: tools;
|
|
181
|
-
|
|
190
|
+
// Unmarked, so the system prompt is the same text on every step and a load does not throw
|
|
191
|
+
// away the cache for the whole transcript. What is loaded is said in `declared` and in the
|
|
192
|
+
// `load_tools` result instead. The preselected first step is the one exception, by design.
|
|
193
|
+
const prompt = onDemand && !routed ? `${system}\n\n${catalogPrompt(catalog)}`.trim() : system;
|
|
182
194
|
const request = [
|
|
183
195
|
...(prompt ? [{ role: "system", content: prompt }] : []),
|
|
184
196
|
...withContext(messages, question ? messages.indexOf(question) : -1, gathered.context, hooks?.preface),
|
|
@@ -293,9 +305,9 @@ export async function runAgentLoop(options) {
|
|
|
293
305
|
throw unreadable;
|
|
294
306
|
if (onDemand && name === LOAD_TOOLS) {
|
|
295
307
|
const resolved = expandNames(requestedNames(args), catalog);
|
|
308
|
+
content = loadResult(resolved, catalog, loaded);
|
|
296
309
|
for (const hit of resolved.matched)
|
|
297
310
|
loaded.add(hit);
|
|
298
|
-
content = loadResult(resolved, catalog);
|
|
299
311
|
ok = resolved.matched.length > 0;
|
|
300
312
|
}
|
|
301
313
|
else {
|
package/dist/hooks.d.ts
CHANGED
|
@@ -129,6 +129,12 @@ export interface Gathered {
|
|
|
129
129
|
* `configureHooks` moves it for a process, and `gather` and `assembleContext` for one request.
|
|
130
130
|
*/
|
|
131
131
|
export declare const HOOK_CONTEXT_TOKENS = 2000;
|
|
132
|
+
/**
|
|
133
|
+
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
134
|
+
* Names no host; `configureHooks` sets another for a process that wants to, and `withContext`
|
|
135
|
+
* for one request.
|
|
136
|
+
*/
|
|
137
|
+
export declare const HOOK_PREFACE: string;
|
|
132
138
|
/** What hooks are held to across a process. Every field optional; see `configureHooks`. */
|
|
133
139
|
export interface HookOptions {
|
|
134
140
|
/**
|
|
@@ -136,31 +142,34 @@ export interface HookOptions {
|
|
|
136
142
|
* held to its own `maxTokens` inside it.
|
|
137
143
|
*/
|
|
138
144
|
contextTokens?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Said above the context blocks, when a call does not give its own. Empty says nothing, and the
|
|
147
|
+
* blocks lead the question on their own.
|
|
148
|
+
*/
|
|
149
|
+
preface?: string;
|
|
139
150
|
}
|
|
140
151
|
/**
|
|
141
152
|
* Changes what hooks are held to, for a process whose windows are not the size these defaults
|
|
142
|
-
* were chosen for.
|
|
153
|
+
* were chosen for, or whose host wants its own name above the context.
|
|
143
154
|
*
|
|
144
|
-
* Module-level for the same reason `configureEvents` is: a budget
|
|
145
|
-
* once at startup. A caller that sizes
|
|
146
|
-
* recall than an 8k one — passes `maxTokens` to `gather` instead,
|
|
155
|
+
* Module-level for the same reason `configureEvents` is: a budget and a preface are a deployment's
|
|
156
|
+
* settings, said once at startup. A caller that sizes the budget per model or per agent — a 128k
|
|
157
|
+
* window can afford more recall than an 8k one — passes `maxTokens` to `gather` instead, and a
|
|
158
|
+
* `preface` passed to `withContext` or `runAgentLoop`'s hooks wins over this one the same way.
|
|
147
159
|
*
|
|
148
|
-
* @param options The
|
|
149
|
-
*
|
|
150
|
-
*
|
|
160
|
+
* @param options The settings to change. A field left out keeps what it has, and so does one given
|
|
161
|
+
* the wrong kind of value — `contextTokens` anything but a number above zero, `preface` anything
|
|
162
|
+
* but a string — so a half-built config narrows nothing. `Infinity` is a number above zero, and
|
|
163
|
+
* lifts the shared budget entirely. An empty `preface` is a string, and turns the preface off.
|
|
151
164
|
* @returns Everything in force afterwards, including what this call did not change.
|
|
152
165
|
*/
|
|
153
166
|
export declare function configureHooks(options?: HookOptions): Required<HookOptions>;
|
|
154
167
|
/**
|
|
155
|
-
* Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the
|
|
168
|
+
* Test seam: puts `configureHooks` back to the defaults, so one test's budget or preface is not the
|
|
169
|
+
* next's.
|
|
156
170
|
* `resetAll` calls it.
|
|
157
171
|
*/
|
|
158
172
|
export declare const resetHooks: () => void;
|
|
159
|
-
/**
|
|
160
|
-
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
161
|
-
* Names no host; `withContext` takes another for one that wants to.
|
|
162
|
-
*/
|
|
163
|
-
export declare const HOOK_PREFACE: string;
|
|
164
173
|
/**
|
|
165
174
|
* Builds the context a set of outcomes adds and the notes that go with it.
|
|
166
175
|
*
|
|
@@ -192,7 +201,8 @@ export declare function assembleContext(outcomes: readonly HookOutcome[], maxTok
|
|
|
192
201
|
* session once a compaction has folded the head into a summary. Anything but a user message there
|
|
193
202
|
* leaves the request as it was.
|
|
194
203
|
* @param context What `assembleContext` built. Empty returns `history` itself.
|
|
195
|
-
* @param preface Said above the blocks.
|
|
204
|
+
* @param preface Said above the blocks. Absent is what `configureHooks` last set — `HOOK_PREFACE`
|
|
205
|
+
* unless something moved it. Empty says nothing, rather than leaving a blank line where it was.
|
|
196
206
|
* @returns `history` when there was nothing to add or nowhere to add it, otherwise a new array.
|
|
197
207
|
*/
|
|
198
208
|
export declare function withContext(history: OpenAI.ChatCompletionMessageParam[], index: number, context: string, preface?: string): OpenAI.ChatCompletionMessageParam[];
|
package/dist/hooks.js
CHANGED
|
@@ -23,49 +23,58 @@ export const INJECT_EVENTS = new Set(["sessionStart", "beforeTurn"]);
|
|
|
23
23
|
* `configureHooks` moves it for a process, and `gather` and `assembleContext` for one request.
|
|
24
24
|
*/
|
|
25
25
|
export const HOOK_CONTEXT_TOKENS = 2000;
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
28
|
+
* Names no host; `configureHooks` sets another for a process that wants to, and `withContext`
|
|
29
|
+
* for one request.
|
|
30
|
+
*/
|
|
31
|
+
export const HOOK_PREFACE = "The <context> blocks below were added for this message by the host's hooks. They are " +
|
|
32
|
+
"background the user did not write and may not be relevant. The user's message follows them.";
|
|
33
|
+
/** The settings this module was written with. */
|
|
34
|
+
const HOOK_DEFAULTS = {
|
|
35
|
+
contextTokens: HOOK_CONTEXT_TOKENS,
|
|
36
|
+
preface: HOOK_PREFACE,
|
|
37
|
+
};
|
|
28
38
|
/** What is in force now. Read where it is used, so a change applies from the next request. */
|
|
29
|
-
let
|
|
39
|
+
let hookSettings = { ...HOOK_DEFAULTS };
|
|
30
40
|
/**
|
|
31
41
|
* Changes what hooks are held to, for a process whose windows are not the size these defaults
|
|
32
|
-
* were chosen for.
|
|
42
|
+
* were chosen for, or whose host wants its own name above the context.
|
|
33
43
|
*
|
|
34
|
-
* Module-level for the same reason `configureEvents` is: a budget
|
|
35
|
-
* once at startup. A caller that sizes
|
|
36
|
-
* recall than an 8k one — passes `maxTokens` to `gather` instead,
|
|
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
|
|
46
|
+
* window can afford more recall than an 8k one — passes `maxTokens` to `gather` instead, and a
|
|
47
|
+
* `preface` passed to `withContext` or `runAgentLoop`'s hooks wins over this one the same way.
|
|
37
48
|
*
|
|
38
|
-
* @param options The
|
|
39
|
-
*
|
|
40
|
-
*
|
|
49
|
+
* @param options The settings to change. A field left out keeps what it has, and so does one given
|
|
50
|
+
* the wrong kind of value — `contextTokens` anything but a number above zero, `preface` anything
|
|
51
|
+
* but a string — so a half-built config narrows nothing. `Infinity` is a number above zero, and
|
|
52
|
+
* lifts the shared budget entirely. An empty `preface` is a string, and turns the preface off.
|
|
41
53
|
* @returns Everything in force afterwards, including what this call did not change.
|
|
42
54
|
*/
|
|
43
55
|
export function configureHooks(options = {}) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
const { contextTokens, preface } = options;
|
|
57
|
+
if (typeof contextTokens === "number" && contextTokens > 0) {
|
|
58
|
+
hookSettings.contextTokens = contextTokens;
|
|
47
59
|
}
|
|
48
|
-
|
|
60
|
+
if (typeof preface === "string")
|
|
61
|
+
hookSettings.preface = preface;
|
|
62
|
+
return { ...hookSettings };
|
|
49
63
|
}
|
|
50
64
|
/**
|
|
51
|
-
* Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the
|
|
65
|
+
* Test seam: puts `configureHooks` back to the defaults, so one test's budget or preface is not the
|
|
66
|
+
* next's.
|
|
52
67
|
* `resetAll` calls it.
|
|
53
68
|
*/
|
|
54
69
|
export const resetHooks = () => {
|
|
55
|
-
|
|
70
|
+
hookSettings = { ...HOOK_DEFAULTS };
|
|
56
71
|
};
|
|
57
72
|
/**
|
|
58
73
|
* The budget a call is held to: its own when it gave a usable one, the process's otherwise. The
|
|
59
74
|
* same rule `configureHooks` applies, so a `0` threaded through for "no opinion" does not quietly
|
|
60
75
|
* turn every hook's context off.
|
|
61
76
|
*/
|
|
62
|
-
const budget = (given) => typeof given === "number" && given > 0 ? given :
|
|
63
|
-
/**
|
|
64
|
-
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
65
|
-
* Names no host; `withContext` takes another for one that wants to.
|
|
66
|
-
*/
|
|
67
|
-
export const HOOK_PREFACE = "The <context> blocks below were added for this message by the host's hooks. They are " +
|
|
68
|
-
"background the user did not write and may not be relevant. The user's message follows them.";
|
|
77
|
+
const budget = (given) => typeof given === "number" && given > 0 ? given : hookSettings.contextTokens;
|
|
69
78
|
const attribute = (text) => text.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
70
79
|
/**
|
|
71
80
|
* Builds the context a set of outcomes adds and the notes that go with it.
|
|
@@ -124,14 +133,15 @@ export function assembleContext(outcomes, maxTokens) {
|
|
|
124
133
|
* session once a compaction has folded the head into a summary. Anything but a user message there
|
|
125
134
|
* leaves the request as it was.
|
|
126
135
|
* @param context What `assembleContext` built. Empty returns `history` itself.
|
|
127
|
-
* @param preface Said above the blocks.
|
|
136
|
+
* @param preface Said above the blocks. Absent is what `configureHooks` last set — `HOOK_PREFACE`
|
|
137
|
+
* unless something moved it. Empty says nothing, rather than leaving a blank line where it was.
|
|
128
138
|
* @returns `history` when there was nothing to add or nowhere to add it, otherwise a new array.
|
|
129
139
|
*/
|
|
130
|
-
export function withContext(history, index, context, preface =
|
|
140
|
+
export function withContext(history, index, context, preface = hookSettings.preface) {
|
|
131
141
|
const message = history[index];
|
|
132
142
|
if (!context || message?.role !== "user")
|
|
133
143
|
return history;
|
|
134
|
-
const lead = `${preface}\n\n${context}\n\n`;
|
|
144
|
+
const lead = preface ? `${preface}\n\n${context}\n\n` : `${context}\n\n`;
|
|
135
145
|
const content = typeof message.content === "string"
|
|
136
146
|
? `${lead}${message.content}`
|
|
137
147
|
: [{ type: "text", text: lead }, ...message.content];
|
package/dist/index.d.ts
CHANGED
|
@@ -28,4 +28,4 @@ export { type Produced, type StreamTurnOptions, streamTurn, type Turn, type Turn
|
|
|
28
28
|
export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, type Split, stripThinking, THINK_FENCE, } from "./thinking.ts";
|
|
29
29
|
export { estimateTokens } from "./tokens.ts";
|
|
30
30
|
export { parseToolArguments, recoverToolCalls, ToolArgumentsError, type ToolCall, } from "./tool-calls.ts";
|
|
31
|
-
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.ts";
|
|
31
|
+
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.ts";
|
package/dist/index.js
CHANGED
|
@@ -26,4 +26,4 @@ export { streamTurn, } from "./stream.js";
|
|
|
26
26
|
export { ALL_FENCES, DEFAULT_FENCES, FenceSplitter, stripThinking, THINK_FENCE, } from "./thinking.js";
|
|
27
27
|
export { estimateTokens } from "./tokens.js";
|
|
28
28
|
export { parseToolArguments, recoverToolCalls, ToolArgumentsError, } from "./tool-calls.js";
|
|
29
|
-
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.js";
|
|
29
|
+
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.js";
|
package/dist/reset.d.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
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, the event bus, and the hooks' configured budget. `resetClients`,
|
|
8
|
-
* `resetHints`, `resetEvents` and `resetHooks` stay exported, because a
|
|
9
|
-
* one thing should say so.
|
|
7
|
+
* hints, the event bus, and the hooks' configured budget and preface. `resetClients`,
|
|
8
|
+
* `resetCapabilities`, `resetHints`, `resetEvents` and `resetHooks` stay exported, because a
|
|
9
|
+
* test that means to clear one thing should say so.
|
|
10
10
|
*
|
|
11
11
|
* This is for the other case, which is every teardown. What they hold is *latched
|
|
12
12
|
* refusals* — a fact one test taught the process about an endpoint, still true as far as the
|
package/dist/reset.js
CHANGED
|
@@ -9,9 +9,9 @@ import { resetHints } from "./side-task.js";
|
|
|
9
9
|
* Five modules here keep state for the life of the process, each for a good reason and each
|
|
10
10
|
* with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
11
11
|
* out not to take `stream_options` or a grammar, the models that refused the no-thinking
|
|
12
|
-
* hints, the event bus, and the hooks' configured budget. `resetClients`,
|
|
13
|
-
* `resetHints`, `resetEvents` and `resetHooks` stay exported, because a
|
|
14
|
-
* one thing should say so.
|
|
12
|
+
* hints, the event bus, and the hooks' configured budget and preface. `resetClients`,
|
|
13
|
+
* `resetCapabilities`, `resetHints`, `resetEvents` and `resetHooks` stay exported, because a
|
|
14
|
+
* test that means to clear one thing should say so.
|
|
15
15
|
*
|
|
16
16
|
* This is for the other case, which is every teardown. What they hold is *latched
|
|
17
17
|
* refusals* — a fact one test taught the process about an endpoint, still true as far as the
|
package/dist/tool-loading.d.ts
CHANGED
|
@@ -23,28 +23,49 @@ export declare const LOAD_TOOLS = "load_tools";
|
|
|
23
23
|
*/
|
|
24
24
|
export declare const LOAD_TOOLS_DEFINITION: OpenAI.ChatCompletionTool;
|
|
25
25
|
/**
|
|
26
|
-
* The catalogue as a plain grouped listing of names, loaded ones marked.
|
|
26
|
+
* The catalogue as a plain grouped listing of names, loaded ones marked if asked.
|
|
27
27
|
*
|
|
28
28
|
* A server with no tools is dropped rather than titled: a pool hands one over whenever a
|
|
29
29
|
* server is connected but has nothing to offer, and a label with nothing under it reads as a
|
|
30
30
|
* listing that got cut off.
|
|
31
31
|
*
|
|
32
32
|
* @param catalog The connected servers. Ones with no tools are dropped.
|
|
33
|
-
* @param loaded Names
|
|
33
|
+
* @param loaded Names to mark `(loaded)` rather than remove. Absent marks nothing, which keeps the
|
|
34
|
+
* listing the same text for the whole run.
|
|
34
35
|
*/
|
|
35
36
|
export declare function catalogList(catalog: CatalogServer[], loaded?: ReadonlySet<string>): string;
|
|
36
37
|
/**
|
|
37
38
|
* The catalogue block appended to the system prompt. Names only — descriptions arrive on load.
|
|
38
39
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* the
|
|
40
|
+
* `runAgentLoop` passes no `loaded`, so the block is the same text on every step. The system
|
|
41
|
+
* prompt is the head of the request, and marking each load there threw away the prompt cache for
|
|
42
|
+
* the whole transcript on every `load_tools` call. What is loaded is said where it does not move
|
|
43
|
+
* the prefix instead: in the tool array, appended in load order (`loadedTools`), and in the
|
|
44
|
+
* `load_tools` result, which answers a repeat load with "already loaded" (`loadResult`).
|
|
45
|
+
*
|
|
46
|
+
* Loaded tools are never removed from the list. That reads as the tool having vanished the moment
|
|
47
|
+
* it was loaded, and the model loads again to get it back; hoisting them into a separate "already
|
|
48
|
+
* loaded" section splits a server's tools apart, and the model picks a sibling from the longer
|
|
49
|
+
* list instead.
|
|
43
50
|
*
|
|
44
51
|
* @param catalog The connected servers. A catalogue with no tools in it produces an empty string.
|
|
45
|
-
* @param loaded Names
|
|
52
|
+
* @param loaded Names to mark `(loaded)`, for a caller that rebuilds its prompt per load and does
|
|
53
|
+
* not mind the cache. Absent marks nothing.
|
|
46
54
|
*/
|
|
47
55
|
export declare function catalogPrompt(catalog: CatalogServer[], loaded?: ReadonlySet<string>): string;
|
|
56
|
+
/**
|
|
57
|
+
* A tool array with newly loaded definitions appended, in the order they were loaded.
|
|
58
|
+
*
|
|
59
|
+
* Never re-sorted and never rebuilt from a set. A template renders the tool array into the
|
|
60
|
+
* prompt near its head, and a load that moved an earlier definition moved everything after it,
|
|
61
|
+
* so the cache was lost from there on every load; appended, the definitions already sent stay
|
|
62
|
+
* a prefix of the new array.
|
|
63
|
+
*
|
|
64
|
+
* @param previous What the last request declared, `load_tools` included. Not written to.
|
|
65
|
+
* @param matched The definitions to add. Ones whose name is already declared, here or earlier in
|
|
66
|
+
* this list, are skipped rather than moved.
|
|
67
|
+
*/
|
|
68
|
+
export declare function loadedTools(previous: readonly OpenAI.ChatCompletionTool[], matched: readonly OpenAI.ChatCompletionTool[]): OpenAI.ChatCompletionTool[];
|
|
48
69
|
/**
|
|
49
70
|
* The most a single `load_tools` call may pull in.
|
|
50
71
|
*
|
|
@@ -103,10 +124,15 @@ export declare function expandNames(requested: string[], catalog: CatalogServer[
|
|
|
103
124
|
/**
|
|
104
125
|
* What `load_tools` reports back: the descriptions, now that they are worth their tokens.
|
|
105
126
|
*
|
|
127
|
+
* A name that was loaded before this call is reported as already loaded rather than loaded
|
|
128
|
+
* again. The catalogue no longer marks what is loaded — see `catalogPrompt` — so this is where a
|
|
129
|
+
* model that asks twice finds out it need not have, and is told to call the tool instead.
|
|
130
|
+
*
|
|
106
131
|
* @param expanded What `expandNames` resolved: the matches, the misses, and the over-broad asks.
|
|
107
132
|
* @param catalog The servers, read for the descriptions now worth their tokens.
|
|
133
|
+
* @param loaded What was loaded before this call. Absent reports every match as newly loaded.
|
|
108
134
|
*/
|
|
109
|
-
export declare function loadResult({ matched, unknown, overBroad, deferred, maxPerLoad }: ReturnType<typeof expandNames>, catalog: CatalogServer[]): string;
|
|
135
|
+
export declare function loadResult({ matched, unknown, overBroad, deferred, maxPerLoad }: ReturnType<typeof expandNames>, catalog: CatalogServer[], loaded?: ReadonlySet<string>): string;
|
|
110
136
|
/**
|
|
111
137
|
* Whether the catalogue holds a tool by this name.
|
|
112
138
|
*
|
package/dist/tool-loading.js
CHANGED
|
@@ -49,14 +49,15 @@ export const LOAD_TOOLS_DEFINITION = deepFreeze({
|
|
|
49
49
|
},
|
|
50
50
|
});
|
|
51
51
|
/**
|
|
52
|
-
* The catalogue as a plain grouped listing of names, loaded ones marked.
|
|
52
|
+
* The catalogue as a plain grouped listing of names, loaded ones marked if asked.
|
|
53
53
|
*
|
|
54
54
|
* A server with no tools is dropped rather than titled: a pool hands one over whenever a
|
|
55
55
|
* server is connected but has nothing to offer, and a label with nothing under it reads as a
|
|
56
56
|
* listing that got cut off.
|
|
57
57
|
*
|
|
58
58
|
* @param catalog The connected servers. Ones with no tools are dropped.
|
|
59
|
-
* @param loaded Names
|
|
59
|
+
* @param loaded Names to mark `(loaded)` rather than remove. Absent marks nothing, which keeps the
|
|
60
|
+
* listing the same text for the whole run.
|
|
60
61
|
*/
|
|
61
62
|
export function catalogList(catalog, loaded) {
|
|
62
63
|
return catalog
|
|
@@ -70,13 +71,20 @@ export function catalogList(catalog, loaded) {
|
|
|
70
71
|
/**
|
|
71
72
|
* The catalogue block appended to the system prompt. Names only — descriptions arrive on load.
|
|
72
73
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* the
|
|
74
|
+
* `runAgentLoop` passes no `loaded`, so the block is the same text on every step. The system
|
|
75
|
+
* prompt is the head of the request, and marking each load there threw away the prompt cache for
|
|
76
|
+
* the whole transcript on every `load_tools` call. What is loaded is said where it does not move
|
|
77
|
+
* the prefix instead: in the tool array, appended in load order (`loadedTools`), and in the
|
|
78
|
+
* `load_tools` result, which answers a repeat load with "already loaded" (`loadResult`).
|
|
79
|
+
*
|
|
80
|
+
* Loaded tools are never removed from the list. That reads as the tool having vanished the moment
|
|
81
|
+
* it was loaded, and the model loads again to get it back; hoisting them into a separate "already
|
|
82
|
+
* loaded" section splits a server's tools apart, and the model picks a sibling from the longer
|
|
83
|
+
* list instead.
|
|
77
84
|
*
|
|
78
85
|
* @param catalog The connected servers. A catalogue with no tools in it produces an empty string.
|
|
79
|
-
* @param loaded Names
|
|
86
|
+
* @param loaded Names to mark `(loaded)`, for a caller that rebuilds its prompt per load and does
|
|
87
|
+
* not mind the cache. Absent marks nothing.
|
|
80
88
|
*/
|
|
81
89
|
export function catalogPrompt(catalog, loaded) {
|
|
82
90
|
const list = catalogList(catalog, loaded);
|
|
@@ -88,14 +96,39 @@ export function catalogPrompt(catalog, loaded) {
|
|
|
88
96
|
"# Tool catalogue",
|
|
89
97
|
"",
|
|
90
98
|
"These tools exist but are not loaded. Call `load_tools` with the names you need, then call",
|
|
91
|
-
"them on the step after. Names are descriptive; load a tool to see its parameters. A
|
|
92
|
-
"
|
|
93
|
-
"
|
|
99
|
+
"them on the step after. Names are descriptive; load a tool to see its parameters. A tool",
|
|
100
|
+
"already in your tool list is loaded — call it directly, do not load it again. Do not load",
|
|
101
|
+
"tools the task does not need, and do not mention this mechanism in your answer.",
|
|
94
102
|
"",
|
|
95
103
|
list,
|
|
96
104
|
].join("\n");
|
|
97
105
|
}
|
|
98
106
|
const flatten = (catalog) => catalog.flatMap((server) => server.tools);
|
|
107
|
+
/**
|
|
108
|
+
* A tool array with newly loaded definitions appended, in the order they were loaded.
|
|
109
|
+
*
|
|
110
|
+
* Never re-sorted and never rebuilt from a set. A template renders the tool array into the
|
|
111
|
+
* prompt near its head, and a load that moved an earlier definition moved everything after it,
|
|
112
|
+
* so the cache was lost from there on every load; appended, the definitions already sent stay
|
|
113
|
+
* a prefix of the new array.
|
|
114
|
+
*
|
|
115
|
+
* @param previous What the last request declared, `load_tools` included. Not written to.
|
|
116
|
+
* @param matched The definitions to add. Ones whose name is already declared, here or earlier in
|
|
117
|
+
* this list, are skipped rather than moved.
|
|
118
|
+
*/
|
|
119
|
+
export function loadedTools(previous, matched) {
|
|
120
|
+
const nameOf = (tool) => tool.type === "function" ? tool.function.name : undefined;
|
|
121
|
+
const declared = new Set(previous.map(nameOf));
|
|
122
|
+
const tools = [...previous];
|
|
123
|
+
for (const tool of matched) {
|
|
124
|
+
const name = nameOf(tool);
|
|
125
|
+
if (name !== undefined && declared.has(name))
|
|
126
|
+
continue;
|
|
127
|
+
declared.add(name);
|
|
128
|
+
tools.push(tool);
|
|
129
|
+
}
|
|
130
|
+
return tools;
|
|
131
|
+
}
|
|
99
132
|
/**
|
|
100
133
|
* The most a single `load_tools` call may pull in.
|
|
101
134
|
*
|
|
@@ -201,17 +234,29 @@ export function expandNames(requested, catalog, maxPerLoad = MAX_PER_LOAD) {
|
|
|
201
234
|
/**
|
|
202
235
|
* What `load_tools` reports back: the descriptions, now that they are worth their tokens.
|
|
203
236
|
*
|
|
237
|
+
* A name that was loaded before this call is reported as already loaded rather than loaded
|
|
238
|
+
* again. The catalogue no longer marks what is loaded — see `catalogPrompt` — so this is where a
|
|
239
|
+
* model that asks twice finds out it need not have, and is told to call the tool instead.
|
|
240
|
+
*
|
|
204
241
|
* @param expanded What `expandNames` resolved: the matches, the misses, and the over-broad asks.
|
|
205
242
|
* @param catalog The servers, read for the descriptions now worth their tokens.
|
|
243
|
+
* @param loaded What was loaded before this call. Absent reports every match as newly loaded.
|
|
206
244
|
*/
|
|
207
|
-
export function loadResult({ matched, unknown, overBroad, deferred, maxPerLoad }, catalog) {
|
|
245
|
+
export function loadResult({ matched, unknown, overBroad, deferred, maxPerLoad }, catalog, loaded) {
|
|
208
246
|
const byName = new Map(flatten(catalog).map((tool) => [tool.name, tool.description]));
|
|
209
247
|
const lines = [];
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
248
|
+
const fresh = matched.filter((name) => !loaded?.has(name));
|
|
249
|
+
const again = matched.filter((name) => loaded?.has(name));
|
|
250
|
+
if (fresh.length) {
|
|
251
|
+
lines.push(`Loaded ${fresh.length} tool(s); they are callable on your next step.`, "");
|
|
252
|
+
for (const name of fresh)
|
|
213
253
|
lines.push(`${name}: ${byName.get(name) ?? ""}`.trim());
|
|
214
254
|
}
|
|
255
|
+
if (again.length) {
|
|
256
|
+
if (lines.length)
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push(`Already loaded and in your tool list: ${again.join(", ")}. Call them directly; do not load them again.`);
|
|
259
|
+
}
|
|
215
260
|
for (const { name, hits } of overBroad) {
|
|
216
261
|
if (lines.length)
|
|
217
262
|
lines.push("");
|
package/llms.txt
CHANGED
|
@@ -108,7 +108,7 @@ What a run is doing, while it is doing it.
|
|
|
108
108
|
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.
|
|
109
109
|
|
|
110
110
|
- `assembleContext` — Builds the context a set of outcomes adds and the notes that go with it.
|
|
111
|
-
- `configureHooks` — Changes what hooks are held to, for a process whose windows are not the size these defaults were chosen for.
|
|
111
|
+
- `configureHooks` — Changes what hooks are held to, for a process whose windows are not the size these defaults were chosen for, or whose host wants its own name above the context.
|
|
112
112
|
- `Gathered` (type) — The context a set of outcomes adds to a request, and a note for each hook worth mentioning.
|
|
113
113
|
- `gather` — Runs the hooks ahead of a request and builds what they add to it.
|
|
114
114
|
- `HOOK_CONTEXT_TOKENS` — The most context all of a request's hooks add between them by default, in estimated tokens.
|
|
@@ -123,7 +123,7 @@ Lifecycle hooks, from the host's side: what a session looks like to them, where
|
|
|
123
123
|
- `HookRunner` (type) — Runs one event's hooks.
|
|
124
124
|
- `INJECT_EVENTS` — The events whose hooks run before a request, and so the only ones whose output can reach it.
|
|
125
125
|
- `notify` — Runs the hooks for an event that reads what happened and adds nothing to a request.
|
|
126
|
-
- `resetHooks` — Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the next's.
|
|
126
|
+
- `resetHooks` — Test seam: puts `configureHooks` back to the defaults, so one test's budget or preface is not the next's.
|
|
127
127
|
- `turnIndex` — Which turn of a session begins at a point, from 0: the user messages ahead of it.
|
|
128
128
|
- `turnMessages` — A stretch of a transcript as a hook reads it: what the user and the assistant said, and nothing else.
|
|
129
129
|
- `UNTRUSTED_PREFACE` — One sentence for a system prompt, saying what an `untrusted` block is.
|
|
@@ -231,12 +231,13 @@ Reading what a model meant by a tool call when it did not write one cleanly.
|
|
|
231
231
|
### tool-loading
|
|
232
232
|
|
|
233
233
|
- `carryOver` — The tools to start the next turn with: recently used, newest last, capped.
|
|
234
|
-
- `catalogList` — The catalogue as a plain grouped listing of names, loaded ones marked.
|
|
234
|
+
- `catalogList` — The catalogue as a plain grouped listing of names, loaded ones marked if asked.
|
|
235
235
|
- `catalogPrompt` — The catalogue block appended to the system prompt.
|
|
236
236
|
- `expandNames` — Resolves requested names against the catalogue, expanding trailing `*` wildcards.
|
|
237
237
|
- `inCatalog` — Whether the catalogue holds a tool by this name.
|
|
238
238
|
- `LOAD_TOOLS` — On-demand tool loading.
|
|
239
239
|
- `LOAD_TOOLS_DEFINITION` — One object for the life of the process — the agent loop asks for it on every iteration.
|
|
240
|
+
- `loadedTools` — A tool array with newly loaded definitions appended, in the order they were loaded.
|
|
240
241
|
- `loadResult` — What `load_tools` reports back: the descriptions, now that they are worth their tokens.
|
|
241
242
|
- `MAX_CARRIED` — The most a conversation carries between turns.
|
|
242
243
|
- `MAX_PER_LOAD` — The most a single `load_tools` call may pull in.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.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",
|