@cubicecho/agent-core 2.18.1 → 2.20.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 +230 -7
- package/dist/agent-loop.d.ts +218 -18
- package/dist/agent-loop.js +180 -39
- package/dist/compaction.d.ts +30 -2
- package/dist/compaction.js +53 -13
- package/dist/config.d.ts +4 -2
- package/dist/events.d.ts +17 -4
- package/dist/hooks.d.ts +8 -2
- package/dist/hooks.js +8 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/ledger.d.ts +132 -0
- package/dist/ledger.js +166 -0
- package/dist/retry.d.ts +6 -0
- package/dist/retry.js +6 -2
- package/dist/spec.d.ts +5 -1
- package/dist/spec.js +4 -2
- package/dist/stream.d.ts +11 -0
- package/dist/tool-loading.d.ts +98 -0
- package/dist/tool-loading.js +215 -0
- package/llms.txt +24 -1
- package/package.json +1 -1
package/dist/agent-loop.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { CatalogServer } from "./catalog.ts";
|
|
|
4
4
|
import type { Endpoint, ModelParams, RetryPolicy, ToolPolicy } from "./config.ts";
|
|
5
5
|
import { type RunEventInput, type RunMetrics } from "./events.ts";
|
|
6
6
|
import { type HookContext, type HookEvent, type HookNote, type HookRunner } from "./hooks.ts";
|
|
7
|
+
import { type TokenLedger } from "./ledger.ts";
|
|
7
8
|
import type { Turn, TurnUsage } from "./stream.ts";
|
|
8
9
|
import { type KeywordPreselectOptions, type ToolOrder } from "./tool-loading.ts";
|
|
9
10
|
/**
|
|
@@ -97,22 +98,76 @@ export declare function preselect(config: Endpoint, model: string, catalog: Cata
|
|
|
97
98
|
*/
|
|
98
99
|
keywords?: boolean | KeywordPreselectOptions;
|
|
99
100
|
}): Promise<string[]>;
|
|
100
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* One call the model made, as `dispatch` and `onToolCall` are handed it.
|
|
103
|
+
*
|
|
104
|
+
* A `call_tool` arrives as the tool it names — that tool's name and arguments, under the
|
|
105
|
+
* `call_tool`'s id — so a dispatcher is the same in every discovery mode.
|
|
106
|
+
*/
|
|
101
107
|
export interface ToolCallRequest {
|
|
102
108
|
id: string;
|
|
103
109
|
name: string;
|
|
104
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Parsed by `parseToolArguments`, repairs and all. Empty for a call whose arguments could not be
|
|
112
|
+
* read, which only `onToolCall` is ever handed.
|
|
113
|
+
*/
|
|
105
114
|
args: Record<string, unknown>;
|
|
106
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* The arguments as the model wrote them, before any repair. Through `call_tool`, the inner
|
|
117
|
+
* arguments as JSON text.
|
|
118
|
+
*/
|
|
107
119
|
raw: string;
|
|
108
120
|
}
|
|
109
121
|
/** What one tool call did, in the order the model asked. */
|
|
110
122
|
export interface ToolCallOutcome {
|
|
123
|
+
/**
|
|
124
|
+
* The id the model's reply gave the call, which is what tells two calls to one tool apart.
|
|
125
|
+
* Distinct within a step — `streamTurn` mints one where the server sent none or repeated one —
|
|
126
|
+
* but nothing stops a server using the same id again in a later step. `runAgentLoop` always
|
|
127
|
+
* sets it; optional so an outcome built before it existed still compiles.
|
|
128
|
+
*/
|
|
129
|
+
id?: string;
|
|
130
|
+
/** The tool that was called. For a `call_tool`, the tool it named. */
|
|
111
131
|
name: string;
|
|
112
132
|
/** False when the arguments did not parse, the tool threw, or `load_tools` loaded nothing. */
|
|
113
133
|
ok: boolean;
|
|
114
134
|
}
|
|
115
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* One step's request as `onRequest` is handed it: what the model is about to read.
|
|
137
|
+
*
|
|
138
|
+
* The loop assembles this and nothing else sees it — the system prompt with the catalogue on it,
|
|
139
|
+
* the hooks' context on the question, the tool array ordered and sanitised as it is sent — so a
|
|
140
|
+
* host that wants to say what is filling the window has nothing to measure without it.
|
|
141
|
+
*/
|
|
142
|
+
export interface AgentLoopRequest {
|
|
143
|
+
/** The messages as sent, system prompt first. The request's own array, not a copy. */
|
|
144
|
+
messages: readonly OpenAI.ChatCompletionMessageParam[];
|
|
145
|
+
/** The tools as sent: ordered, sanitised, relaxed where the endpoint needs it. Empty for none. */
|
|
146
|
+
tools: readonly OpenAI.ChatCompletionTool[];
|
|
147
|
+
/** Which step of the run this is, from zero, as `beforeStep` and `onTurn` count them. */
|
|
148
|
+
step: number;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* One call's whole answer, as `onToolResult` is handed it.
|
|
152
|
+
*
|
|
153
|
+
* The `tool-result` event carries the same answer cut by `preview`, which is right for a readout
|
|
154
|
+
* and wrong for a host that renders or stores the result: this is the text the model reads.
|
|
155
|
+
*/
|
|
156
|
+
export interface ToolCallResult extends ToolCallOutcome {
|
|
157
|
+
id: string;
|
|
158
|
+
/**
|
|
159
|
+
* What went into the transcript for this call, uncut: what the tool returned, what it threw, the
|
|
160
|
+
* `load_tools` answer, or why the arguments could not be read.
|
|
161
|
+
*/
|
|
162
|
+
content: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The hooks a loop runs around one question. See `hooks.ts`.
|
|
166
|
+
*
|
|
167
|
+
* The loop gathers before its first request and nothing else can run beside that, so a host with
|
|
168
|
+
* pre-turn work of its own to overlap with the hooks — compaction, a preselection — calls `gather`
|
|
169
|
+
* itself, puts the context on with `withContext`, calls `notify` after, and passes no `hooks`.
|
|
170
|
+
*/
|
|
116
171
|
export interface AgentLoopHooks {
|
|
117
172
|
run: HookRunner;
|
|
118
173
|
/** What the hooks are told. `reply` and `turn` are filled in for `afterTurn`. */
|
|
@@ -126,26 +181,47 @@ export interface AgentLoopHooks {
|
|
|
126
181
|
/** Hears each note, from before the request and from `afterTurn`. */
|
|
127
182
|
onNote?: (note: HookNote) => void;
|
|
128
183
|
}
|
|
184
|
+
/** How full the window is as a step is about to be sent, as `beforeStep` is told it. */
|
|
185
|
+
export interface StepWindow {
|
|
186
|
+
/**
|
|
187
|
+
* The prompt tokens this run's last request reported. Absent before the first step — an earlier
|
|
188
|
+
* run's figure is the host's to vouch for, since the transcript may have been folded since —
|
|
189
|
+
* and where that request reported none. It does not count the reply and tool results added
|
|
190
|
+
* since.
|
|
191
|
+
*/
|
|
192
|
+
used?: number;
|
|
193
|
+
/** The model's window, `config.contextLength`. Zero where the config names none. */
|
|
194
|
+
limit: number;
|
|
195
|
+
/** The run's ledger so far, indexed into the transcript `beforeStep` is handed beside it. */
|
|
196
|
+
ledger: TokenLedger;
|
|
197
|
+
}
|
|
129
198
|
/** What `runAgentLoop` takes. */
|
|
130
199
|
export interface AgentLoopOptions {
|
|
131
200
|
/**
|
|
132
201
|
* The endpoint, what to ask the model for, and how long it may keep calling tools.
|
|
133
202
|
* `toolDiscovery` absent is eager, `maxRetries` absent is none, and `contextLength` is handed
|
|
134
203
|
* to `runTurn` as `contextLimit`, which sizes each request against the window before sending.
|
|
204
|
+
* `"proxy"` is on-demand loading behind a tool array that never changes; see `PROXY_TOOLS`.
|
|
135
205
|
*/
|
|
136
206
|
config: Endpoint & ModelParams & Pick<ToolPolicy, "maxToolIterations"> & Partial<Pick<ToolPolicy, "toolDiscovery">> & Partial<RetryPolicy> & {
|
|
137
207
|
contextLength?: number;
|
|
138
208
|
};
|
|
139
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* The standing instruction, sent as the first message. On-demand and proxied modes append the
|
|
211
|
+
* catalogue, each in its own wording.
|
|
212
|
+
*/
|
|
140
213
|
system?: string;
|
|
141
214
|
/** The transcript so far, ending in the question. Not written to; see the result's `messages`. */
|
|
142
215
|
messages: OpenAI.ChatCompletionMessageParam[];
|
|
143
216
|
/**
|
|
144
217
|
* Every tool this run may reach. Eager mode sends all of them; on-demand mode sends the ones
|
|
145
|
-
* loaded so far, by name.
|
|
218
|
+
* loaded so far, by name; proxied mode declares none, and answers a load with them instead.
|
|
146
219
|
*/
|
|
147
220
|
tools?: OpenAI.ChatCompletionTool[];
|
|
148
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* The same tools as a name-only catalogue. On-demand and proxied modes need it, and are eager
|
|
223
|
+
* without it.
|
|
224
|
+
*/
|
|
149
225
|
catalog?: CatalogServer[];
|
|
150
226
|
/**
|
|
151
227
|
* How the declared tools are ordered before each request. By name unless told otherwise, so a
|
|
@@ -156,10 +232,38 @@ export interface AgentLoopOptions {
|
|
|
156
232
|
/**
|
|
157
233
|
* What `preselect` picked. The first step is sent these and nothing else — no catalogue, no
|
|
158
234
|
* `load_tools` — because a model with the menu still in front of it shops: it reloads what it
|
|
159
|
-
* has or picks a sibling. Everything comes back on the step after.
|
|
235
|
+
* has or picks a sibling. Everything comes back on the step after. `preselectRouting` trades
|
|
236
|
+
* that for a first step shaped like the rest.
|
|
237
|
+
*
|
|
238
|
+
* Proxied, the tool array is fixed and there is no such step. The shortlist is written into the
|
|
239
|
+
* transcript instead, after the question, as a `load_tools` call the model did not make and the
|
|
240
|
+
* result that call would have had — an assistant message and a tool message, handed back in
|
|
241
|
+
* `messages` like any other, reported as a `tool-call` and a `tool-result` event and told to
|
|
242
|
+
* `onToolCall` and `onToolResult`. Names with no definition in `tools`, or outside the
|
|
243
|
+
* catalogue, are left out, and none left is no exchange.
|
|
160
244
|
*/
|
|
161
245
|
preselected?: readonly string[];
|
|
162
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* What a preselection does to the first step. `"exclusive"`, the default, is the shortlist alone
|
|
248
|
+
* as `preselected` describes — and a system prompt and tool array unlike the last request's and
|
|
249
|
+
* unlike the next step's, so a prompt cache misses the whole transcript on the first step and
|
|
250
|
+
* again on the second. `"append"` loads the shortlist as a `load_tools` call would have and
|
|
251
|
+
* sends the first step like any other: the catalogue, `load_tools`, what `loaded` carried, then
|
|
252
|
+
* the shortlist after it, with a name already carried left where it is. The price is the menu
|
|
253
|
+
* back in front of the model. Which costs more has not been measured, which is why the default
|
|
254
|
+
* has not moved.
|
|
255
|
+
*
|
|
256
|
+
* Appended is before `toolOrder` has its say: sorted, a preselected tool lands at its name's
|
|
257
|
+
* place and moves every definition after it, so only `toolOrder: false` keeps a carried array
|
|
258
|
+
* a strict prefix. Either way the first step's system prompt is the second's, and so is its
|
|
259
|
+
* tool array until something else is loaded. Nothing to do without a preselection, in eager
|
|
260
|
+
* mode, or proxied, where no step is routed either way.
|
|
261
|
+
*/
|
|
262
|
+
preselectRouting?: "exclusive" | "append";
|
|
263
|
+
/**
|
|
264
|
+
* Tools already loaded, carried from an earlier question. See `carryOver`. Not read in proxied
|
|
265
|
+
* mode, where a tool is loaded only while its definition is in the history.
|
|
266
|
+
*/
|
|
163
267
|
loaded?: Iterable<string>;
|
|
164
268
|
/** Runs one tool call and returns what the model reads. What it throws, the model reads too. */
|
|
165
269
|
dispatch: (call: ToolCallRequest, signal?: AbortSignal) => Promise<string>;
|
|
@@ -184,13 +288,49 @@ export interface AgentLoopOptions {
|
|
|
184
288
|
* `idempotentHint` annotations can answer it; nothing in an OpenAI tool definition can.
|
|
185
289
|
*/
|
|
186
290
|
dedupeToolCalls?: boolean | ((call: ToolCallRequest) => boolean);
|
|
187
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* Told of every call the model made, as it starts — the ones `dispatch` never sees included:
|
|
293
|
+
* `load_tools`, an identical repeat answered from the first, and a call whose arguments could
|
|
294
|
+
* not be read, which arrives with empty `args` and the model's own text in `raw`.
|
|
295
|
+
*
|
|
296
|
+
* For a host that shows each call as it is made and fills it in when its result lands, which
|
|
297
|
+
* `dispatch` alone cannot do for those three. Not awaited: a promise it returns is dropped.
|
|
298
|
+
*/
|
|
299
|
+
onToolCall?: (call: ToolCallRequest) => void;
|
|
300
|
+
/**
|
|
301
|
+
* Told of every call's answer as it lands, whole, under the id `onToolCall` announced it by.
|
|
302
|
+
*
|
|
303
|
+
* One per call the model made: a deduplicated repeat gets its own, carrying the answer it
|
|
304
|
+
* shares with the first. With `parallel` they arrive as the calls finish, which need not be
|
|
305
|
+
* the order they were made in; the transcript and the result's `toolCalls` keep that order.
|
|
306
|
+
* Not awaited: a promise it returns is dropped.
|
|
307
|
+
*/
|
|
308
|
+
onToolResult?: (result: ToolCallResult) => void;
|
|
309
|
+
/**
|
|
310
|
+
* Hooks gathered onto the question before the first request, and told the reply after.
|
|
311
|
+
*
|
|
312
|
+
* Only this question is touched — the last user message of `messages` — and the context is on
|
|
313
|
+
* the request, never in the transcript handed back. An earlier question's context is therefore
|
|
314
|
+
* the host's to send again: keep the result's `context` and `preface` beside the question, and
|
|
315
|
+
* put them back with `withContext` on every later call. A question sent bare that was first
|
|
316
|
+
* sent with context is a different prompt from that message on, and the server's prefix cache
|
|
317
|
+
* is lost behind it on every turn.
|
|
318
|
+
*/
|
|
188
319
|
hooks?: AgentLoopHooks;
|
|
189
320
|
/**
|
|
190
321
|
* Called before each step with the transcript, and what it returns replaces it — the point to
|
|
191
|
-
* compact or prune a run that has grown into its window. Returning nothing keeps it.
|
|
322
|
+
* compact or prune a run that has grown into its window. Returning nothing keeps it. `window`
|
|
323
|
+
* is what `planCompaction` wants to know, so a host need not track usage itself to compact
|
|
324
|
+
* mid-run.
|
|
325
|
+
*/
|
|
326
|
+
beforeStep?: (messages: readonly OpenAI.ChatCompletionMessageParam[], step: number, window: StepWindow) => OpenAI.ChatCompletionMessageParam[] | undefined | Promise<OpenAI.ChatCompletionMessageParam[] | undefined>;
|
|
327
|
+
/**
|
|
328
|
+
* The ledger an earlier run handed back for this transcript, to be continued. Its indexes have
|
|
329
|
+
* to be into `messages` as given here — see `rebaseLedger` for a transcript rewritten since.
|
|
330
|
+
* This run's requests start a new epoch, because the loop cannot see whether the request before
|
|
331
|
+
* its first one had the same system prompt and tools. Absent starts an empty one.
|
|
192
332
|
*/
|
|
193
|
-
|
|
333
|
+
ledger?: TokenLedger;
|
|
194
334
|
/** Stops the run: the request in flight, and between steps and calls. */
|
|
195
335
|
signal?: AbortSignal;
|
|
196
336
|
/** Told what the run is doing, as the events a watcher reads. */
|
|
@@ -202,6 +342,16 @@ export interface AgentLoopOptions {
|
|
|
202
342
|
* the answer. See `recoverToolCalls`.
|
|
203
343
|
*/
|
|
204
344
|
recoverToolCalls?: boolean;
|
|
345
|
+
/**
|
|
346
|
+
* Each step's request as first built, before it goes out — for measuring what was sent, not for
|
|
347
|
+
* changing it, which is `beforeStep`'s.
|
|
348
|
+
*
|
|
349
|
+
* Called once a step, synchronously, and what it throws ends the run. It is not called again
|
|
350
|
+
* for what `runTurn` sends after that: a retry sends the same request, but a refusal is answered
|
|
351
|
+
* with a lesser body — relaxed schemas, a field dropped — and a continuation with the reply so
|
|
352
|
+
* far on the end, and neither is reported.
|
|
353
|
+
*/
|
|
354
|
+
onRequest?: (request: AgentLoopRequest) => void;
|
|
205
355
|
/** Each turn as it comes back, before its tools run. Recovered calls are in it as calls. */
|
|
206
356
|
onTurn?: (turn: Turn, step: number) => void;
|
|
207
357
|
/**
|
|
@@ -219,19 +369,54 @@ export interface AgentLoopResult {
|
|
|
219
369
|
messages: OpenAI.ChatCompletionMessageParam[];
|
|
220
370
|
/** Summed over every turn of the run. */
|
|
221
371
|
usage: TurnUsage;
|
|
222
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Every call, `load_tools` included, in the order they were made, each under its call id — a
|
|
374
|
+
* proxied preselection's among them, first.
|
|
375
|
+
*/
|
|
223
376
|
toolCalls: ToolCallOutcome[];
|
|
224
|
-
/**
|
|
377
|
+
/**
|
|
378
|
+
* What is loaded at the end, for `carryOver`. Empty in eager mode, and in proxied mode: a
|
|
379
|
+
* definition loaded there is already in the history, and one a compaction folded away has to be
|
|
380
|
+
* loadable again rather than answered "already loaded".
|
|
381
|
+
*/
|
|
225
382
|
loaded: string[];
|
|
226
|
-
/** The tools the model actually called, `load_tools` excluded. */
|
|
383
|
+
/** The tools the model actually called, `load_tools` excluded and `call_tool` looked through. */
|
|
227
384
|
used: string[];
|
|
228
385
|
/** The hooks' notes from before the first request. */
|
|
229
386
|
notes: HookNote[];
|
|
387
|
+
/**
|
|
388
|
+
* The `<context>` blocks the hooks added to this question, as `withContext` takes them. Empty
|
|
389
|
+
* when no hook added any, and without `hooks`. Not in `messages`, so a host that wants the next
|
|
390
|
+
* turn's request to begin with this one's stores it beside the question; see `hooks`.
|
|
391
|
+
*/
|
|
392
|
+
context: string;
|
|
393
|
+
/**
|
|
394
|
+
* What was said above `context`: the hooks' own `preface`, or what `configureHooks` had set when
|
|
395
|
+
* the run began. Handed back because the second is not the host's to know later — with it,
|
|
396
|
+
* `withContext(messages, at, context, preface)` is the question as it was sent, whatever the
|
|
397
|
+
* process's preface has since become. Empty when the preface was turned off; with no `context`
|
|
398
|
+
* nothing was said either way, and `withContext` adds nothing.
|
|
399
|
+
*/
|
|
400
|
+
preface: string;
|
|
401
|
+
/**
|
|
402
|
+
* The `afterTurn` hooks' notes, once they have run — failures only, as `notify` returns them.
|
|
403
|
+
* The loop does not wait for it, so the answer is never held for a hook; a host that stores the
|
|
404
|
+
* notes with the turn, or must not exit before the turn is remembered, awaits it. Each note
|
|
405
|
+
* reaches `onNote` as well. Already resolved and empty without `hooks`, and rejects only if
|
|
406
|
+
* `onNote` throws.
|
|
407
|
+
*/
|
|
408
|
+
afterTurn: Promise<HookNote[]>;
|
|
230
409
|
/**
|
|
231
410
|
* The run summed and derived: what `runMetrics` makes of the events this loop emitted, plus the
|
|
232
411
|
* `load_tools` findings only the loop sees, `wallMs` from the call to the return, and `outcome`.
|
|
233
412
|
*/
|
|
234
413
|
metrics: RunMetrics;
|
|
414
|
+
/**
|
|
415
|
+
* What each step's request reported its prompt as, indexed into `messages` above, for
|
|
416
|
+
* `tokensBetween` and `estimateFrom`. The entries of a `ledger` passed in come first. A step
|
|
417
|
+
* whose prompt was not reported has no entry.
|
|
418
|
+
*/
|
|
419
|
+
ledger: TokenLedger;
|
|
235
420
|
}
|
|
236
421
|
/**
|
|
237
422
|
* Runs a question to its answer: one `runTurn` per step, the tools it asks for between them,
|
|
@@ -242,10 +427,25 @@ export interface AgentLoopResult {
|
|
|
242
427
|
* prompt unchanged from step to step, a load adds to the tool array — which every request sends
|
|
243
428
|
* in the stable order `toolOrder` asks for — and
|
|
244
429
|
* a catalogued tool called without being loaded is loaded and run rather than refused, and a
|
|
245
|
-
* preselection shapes the first step
|
|
246
|
-
* because it otherwise reads exactly like a finished one — or, given `maxContinuations`, is
|
|
430
|
+
* preselection shapes the first step unless `preselectRouting` says to append it. A turn cut off
|
|
431
|
+
* at `maxTokens` is said so as a notice, because it otherwise reads exactly like a finished one — or, given `maxContinuations`, is
|
|
247
432
|
* continued first. Every turn ends in a `usage` event carrying the turn's own report, with the
|
|
248
|
-
* cache compared against the request before it
|
|
433
|
+
* cache compared against the request before it and the request broken down by what filled it; see
|
|
434
|
+
* `TurnUsage` and `runMetrics`.
|
|
435
|
+
*
|
|
436
|
+
* Each step's reported prompt goes on a token ledger, in the same epoch as the step before where
|
|
437
|
+
* the request only appended to it and in a new one where it did not — a `beforeStep` that rewrote
|
|
438
|
+
* the transcript, a tool array a load grew — so that what the messages between two steps cost can
|
|
439
|
+
* be read back as a subtraction. The prompt recorded is the first request's, before any
|
|
440
|
+
* continuation, and a step that reported none, or reported a cache count above its prompt, is not
|
|
441
|
+
* recorded. The ledger comes back in the result and on every `usage` event; see `tokensBetween`.
|
|
442
|
+
*
|
|
443
|
+
* Proxied discovery is the same catalogue behind a tool array that never moves: every step
|
|
444
|
+
* declares `PROXY_TOOLS` and nothing else, a load answers with the definitions themselves, and a
|
|
445
|
+
* `call_tool` is dispatched, counted and reported as the tool it names. A preselection has no
|
|
446
|
+
* tool array to go into, so it is written into the transcript as a `load_tools` exchange after
|
|
447
|
+
* the question. `call_tool` is honoured on demand too, unless the host has a tool of that name —
|
|
448
|
+
* a session switched out of proxied mode still has it in its history, and the model copies it.
|
|
249
449
|
*
|
|
250
450
|
* @param options The config, transcript, tools and dispatcher, plus the optional hooks, events
|
|
251
451
|
* and cancellation. See `AgentLoopOptions`.
|