@cubicecho/agent-core 2.19.0 → 2.21.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 +98 -5
- package/dist/agent-loop.d.ts +73 -11
- package/dist/agent-loop.js +252 -49
- package/dist/compaction.d.ts +3 -1
- package/dist/compaction.js +8 -1
- package/dist/config.d.ts +4 -2
- package/dist/errors.d.ts +89 -0
- package/dist/errors.js +77 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/spec.d.ts +5 -1
- package/dist/spec.js +4 -2
- package/dist/tool-loading.d.ts +98 -0
- package/dist/tool-loading.js +215 -0
- package/llms.txt +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ only, Node >=22.
|
|
|
23
23
|
| Module | What it does |
|
|
24
24
|
| --- | --- |
|
|
25
25
|
| `schema-compat` | Makes an MCP tool schema something a strict or grammar-constrained server will accept. `sanitizeTools`, `relaxTools`, `isGrammarError`. |
|
|
26
|
-
| `tool-loading` | On-demand tool discovery: a name-only catalogue plus a `load_tools` meta-tool, so a run pays for the schemas it asks for instead of all of them. Plus `preselectByKeywords`, which picks from it without a model. |
|
|
26
|
+
| `tool-loading` | On-demand tool discovery: a name-only catalogue plus a `load_tools` meta-tool, so a run pays for the schemas it asks for instead of all of them. Plus `preselectByKeywords`, which picks from it without a model, and the proxied form of the same thing — `PROXY_TOOLS`, `proxyLoadResult`, `proxiedCall` — for a server whose prompt cache a growing tool array throws away. |
|
|
27
27
|
| `stream` | Reads one streamed turn back into a message: token callbacks, tool-call reassembly, fenced reasoning taken out of the answer, and the idle watchdog that turns a silent endpoint into `EndpointSilent`. |
|
|
28
28
|
| `capabilities` | What an endpoint turned out not to support — and, under it, what one model on that endpoint did not — plus the loop that answers either when it says so. `capabilitiesFor`, `modelCapabilitiesFor`, `negotiate`. |
|
|
29
29
|
| `thinking` | Tells a scratchpad fenced inside `content` from the answer: `FenceSplitter` for a stream, `stripThinking` for a whole reply, and the fence tables both read. |
|
|
@@ -44,7 +44,7 @@ only, Node >=22.
|
|
|
44
44
|
| `spec` | `parseSpec`, `resolveAgentSpec` and `exportSpec`: an agent as a versioned JSON document, layered into the flat config the loop takes. Imports nothing but types, and is published separately at `@cubicecho/agent-core/spec`. |
|
|
45
45
|
| `reset` | `resetAll`: drops every cache and latch in one call, so a teardown cannot forget one. |
|
|
46
46
|
| `tokens` | `estimateTokens`: characters over four, deliberately low, for everything here that has to guess at a window. |
|
|
47
|
-
| `errors` | `errorMessage`: a caught `unknown` turned into something a run row can hold. |
|
|
47
|
+
| `errors` | `errorMessage`: a caught `unknown` turned into something a run row can hold. And what `runAgentLoop` throws, with the run as it stood: `AgentLoopError`, `ToolIterationLimit`, `AgentLoopOverflow`, and `failedRun` to read it off any of them. |
|
|
48
48
|
| `catalog` | `CatalogServer`: the name-only shape `tool-loading` reads a connected server as. |
|
|
49
49
|
|
|
50
50
|
What is **not** here is the work: orchestration, prompts, and whatever the run is about. That
|
|
@@ -385,10 +385,10 @@ import { runAgentLoop, emit } from "@cubicecho/agent-core";
|
|
|
385
385
|
|
|
386
386
|
const { turn, messages, usage, loaded } = await runAgentLoop({
|
|
387
387
|
config, // Endpoint & ModelParams & { maxToolIterations, toolDiscovery?, maxRetries?, loadingTimeoutSeconds?, contextLength? }
|
|
388
|
-
system, // sent as the first message; on-demand
|
|
388
|
+
system, // sent as the first message; on-demand and proxied modes append the catalogue
|
|
389
389
|
messages: history, // ending in the question; not written to
|
|
390
390
|
tools, // every tool the run may reach
|
|
391
|
-
catalog, // the same, name-only, for on-demand loading
|
|
391
|
+
catalog, // the same, name-only, for on-demand and proxied loading
|
|
392
392
|
preselected, // from `preselect`, if a small model chose
|
|
393
393
|
dispatch: ({ name, args }, signal) => pool.call(name, args, signal),
|
|
394
394
|
hooks: { run, context: { session: { id } } },
|
|
@@ -483,6 +483,53 @@ tool array stays a strict prefix of the new one. With name order, the default, a
|
|
|
483
483
|
is sorted in and moves every definition after it — the first step still matches the second, but
|
|
484
484
|
the last turn's cache holds only as far as the first new name.
|
|
485
485
|
|
|
486
|
+
`toolDiscovery: "proxy"` is the same catalogue behind a tool array that never changes. A chat
|
|
487
|
+
template renders the tool array in the system turn, so on a server that caches by prefix —
|
|
488
|
+
llama.cpp, and anything built on it — a `load_tools` that appends a definition changes the head of
|
|
489
|
+
the prompt and the whole transcript is prefilled again, on the very step that was meant to save
|
|
490
|
+
tokens. Proxied, every step declares `PROXY_TOOLS` and nothing else: `load_tools`, and `call_tool`,
|
|
491
|
+
which takes `{ name, arguments }`. A load answers with each new tool's name, description and
|
|
492
|
+
parameters as a line of JSON, since no request will ever declare them, and with a pointer back for
|
|
493
|
+
one whose definition is already in the conversation. A `call_tool` is refused for a name outside
|
|
494
|
+
the catalogue, takes `arguments` as an object or as JSON in a string, and reaches `dispatch` as the
|
|
495
|
+
tool it names — the inner name and arguments, under the call's own id — so a dispatcher, a watcher
|
|
496
|
+
and `used` see the same thing in every mode. The transcript keeps the `call_tool` the model wrote.
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
const { messages, used } = await runAgentLoop({
|
|
500
|
+
config: { ...config, toolDiscovery: "proxy" },
|
|
501
|
+
messages: history,
|
|
502
|
+
tools, // never declared; a load answers with them
|
|
503
|
+
catalog,
|
|
504
|
+
preselected, // written into the transcript, not into the tool array
|
|
505
|
+
dispatch: ({ name, args }, signal) => pool.call(name, args, signal),
|
|
506
|
+
});
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
A preselection has no tool array to go into there, and no step of its own. It is written into the
|
|
510
|
+
transcript after the question as a `load_tools` call the model did not make, and the result that
|
|
511
|
+
call would have had:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
{ role: "assistant", content: null, tool_calls: [{ id: "preselect-3", type: "function",
|
|
515
|
+
function: { name: "load_tools", arguments: '{"names":["files__read_file"]}' } }] },
|
|
516
|
+
{ role: "tool", tool_call_id: "preselect-3", content: "Loaded 1 tool(s). Run them with `call_tool`.\n\n{...}" },
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
The pair is in the `messages` the loop hands back, to be stored like any other, and is reported as
|
|
520
|
+
a `tool-call` and a `tool-result` event, told to `onToolCall` and `onToolResult`, and counted in
|
|
521
|
+
`toolCalls` and in the `load_tools` metrics. The
|
|
522
|
+
id is numbered by where the call lands in the transcript, so two questions do not share one. A name
|
|
523
|
+
with no definition in `tools`, or outside the catalogue, is left out, and none left is no exchange.
|
|
524
|
+
|
|
525
|
+
What is loaded there is what the history holds, so `loaded` comes back empty and the `loaded`
|
|
526
|
+
option is not read: there is nothing to carry, and a definition a compaction folded away has to be
|
|
527
|
+
loadable again. For the same reason a `beforeStep` that returns a different array makes the loop
|
|
528
|
+
forget what it loaded, which costs a definition sent twice at worst. `call_tool` is honoured in
|
|
529
|
+
`ondemand` mode as well — a session switched out of proxied mode still has it in its history, and
|
|
530
|
+
a model copies what it has seen work — unless the host has a tool of that name, which is then the
|
|
531
|
+
host's. Eager mode does not know it.
|
|
532
|
+
|
|
486
533
|
That preselection costs a round trip to a model, which on a local box is a few seconds before the
|
|
487
534
|
run has started, spent on a model doing term matching. `preselect(..., { keywords: true })` does
|
|
488
535
|
the matching directly and spends the model only on what the words cannot settle:
|
|
@@ -626,6 +673,50 @@ with pre-turn work to overlap with the hooks — compaction, a preselection —
|
|
|
626
673
|
applies `withContext` to the current question as well, calls `notify` after, and passes no `hooks`
|
|
627
674
|
(see [Hooks](#hooks)).
|
|
628
675
|
|
|
676
|
+
A host that stores its transcript — a row per message, so a crash or a stop mid-run still leaves
|
|
677
|
+
readable history — gets each message as the loop appends it, and the run as it stood when the loop
|
|
678
|
+
throws:
|
|
679
|
+
|
|
680
|
+
```ts
|
|
681
|
+
try {
|
|
682
|
+
await runAgentLoop({
|
|
683
|
+
...options,
|
|
684
|
+
// Awaited: the row is written before the tools run, and before the next request is sent.
|
|
685
|
+
onMessage: (message, step, turn) =>
|
|
686
|
+
store.add(sessionId, turn?.reasoning ? { ...message, reasoning_content: turn.reasoning } : message),
|
|
687
|
+
});
|
|
688
|
+
} catch (error) {
|
|
689
|
+
const run = failedRun(error); // messages, usage, toolCalls, loaded, used — or undefined
|
|
690
|
+
if (run) await store.keep(sessionId, { usage: run.usage, loaded: run.loaded });
|
|
691
|
+
if (signal.aborted) return; // a stop, told by the signal as it always was
|
|
692
|
+
if (error instanceof ContextOverflow) return compactAndRetry(run?.messages);
|
|
693
|
+
if (error instanceof ToolIterationLimit) return outOfSteps();
|
|
694
|
+
throw error; // an AgentLoopError: the message is the cause's, and the cause is on `cause`
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
`onMessage` hears the assistant message as it will be replayed — recovered calls folded in, and
|
|
699
|
+
arguments repaired, which the turn `onTurn` is handed does not show — together with that `Turn`,
|
|
700
|
+
and then each tool result, in transcript order. `onTurn` is not awaited and never sees a tool
|
|
701
|
+
result; this is the one to store from. What it returns is ignored, since `beforeStep` is the one way to rewrite
|
|
702
|
+
the transcript, and what it throws ends the run.
|
|
703
|
+
|
|
704
|
+
Everything the loop throws carries the run. A spent `maxToolIterations` is a `ToolIterationLimit`,
|
|
705
|
+
with the message it always had. A request too big for the window is an `AgentLoopOverflow`, which
|
|
706
|
+
is a `ContextOverflow`, so a `catch` written for one still catches it. Anything else — a stop, a
|
|
707
|
+
refused request, a dropped stream — is an `AgentLoopError` whose `cause` is what was caught and
|
|
708
|
+
whose message is the cause's own. It is a wrapper and not fields on the error that was caught
|
|
709
|
+
because a stop throws the signal's reason, one object for every run under that signal. A stop is
|
|
710
|
+
still recognised by the signal the host aborted.
|
|
711
|
+
|
|
712
|
+
A failure while a step's tools are running leaves a transcript that can be sent again. The results
|
|
713
|
+
that came back are kept, a call the run was stopped during is answered `Stopped before this call
|
|
714
|
+
finished.`, and one the loop never reached `Not run: the run stopped first.` — a call with no
|
|
715
|
+
result is a transcript an endpoint refuses. Those are announced through `onMessage` like any other
|
|
716
|
+
result. The reply of a request that was in flight when the run stopped is not kept: the host has
|
|
717
|
+
the deltas from its `output` and `thinking` events, and whether half an answer belongs in the
|
|
718
|
+
history is its call.
|
|
719
|
+
|
|
629
720
|
`resolveApiKey` is exported and not applied, because which key an endpoint gets is a rule a
|
|
630
721
|
consumer states and a library guessing it could send one where it was not meant to go. The rule
|
|
631
722
|
it encodes is the conservative one: an endpoint's own key wins; one that names a base URL of its
|
|
@@ -663,7 +754,9 @@ Two ways to make a transcript smaller, cheap first.
|
|
|
663
754
|
`pruneToolResults(messages, { keepLast: 5, maxChars: 256 })` replaces every tool result but the
|
|
664
755
|
latest five with a stub — `[result cleared, 10,412 chars]`. A 40k-character `read_file` is 10k
|
|
665
756
|
tokens on every turn after it, and by then the model has usually taken what it wanted; the stub
|
|
666
|
-
keeps the call answered and says how much was there.
|
|
757
|
+
keeps the call answered and says how much was there. A proxied `load_tools` result that carries
|
|
758
|
+
definitions is never cleared, however old: it is the only copy of the schema the model has, and
|
|
759
|
+
`holdsDefinitions` is the test for a host with a prune of its own.
|
|
667
760
|
|
|
668
761
|
`planCompaction(messages, { limit, used })` says where to fold the oldest stretch into a summary,
|
|
669
762
|
once `used` (the last turn's prompt tokens, or the estimate) is past three quarters of the window.
|
package/dist/agent-loop.d.ts
CHANGED
|
@@ -98,7 +98,12 @@ export declare function preselect(config: Endpoint, model: string, catalog: Cata
|
|
|
98
98
|
*/
|
|
99
99
|
keywords?: boolean | KeywordPreselectOptions;
|
|
100
100
|
}): Promise<string[]>;
|
|
101
|
-
/**
|
|
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
|
+
*/
|
|
102
107
|
export interface ToolCallRequest {
|
|
103
108
|
id: string;
|
|
104
109
|
name: string;
|
|
@@ -107,7 +112,10 @@ export interface ToolCallRequest {
|
|
|
107
112
|
* read, which only `onToolCall` is ever handed.
|
|
108
113
|
*/
|
|
109
114
|
args: Record<string, unknown>;
|
|
110
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* The arguments as the model wrote them, before any repair. Through `call_tool`, the inner
|
|
117
|
+
* arguments as JSON text.
|
|
118
|
+
*/
|
|
111
119
|
raw: string;
|
|
112
120
|
}
|
|
113
121
|
/** What one tool call did, in the order the model asked. */
|
|
@@ -119,6 +127,7 @@ export interface ToolCallOutcome {
|
|
|
119
127
|
* sets it; optional so an outcome built before it existed still compiles.
|
|
120
128
|
*/
|
|
121
129
|
id?: string;
|
|
130
|
+
/** The tool that was called. For a `call_tool`, the tool it named. */
|
|
122
131
|
name: string;
|
|
123
132
|
/** False when the arguments did not parse, the tool threw, or `load_tools` loaded nothing. */
|
|
124
133
|
ok: boolean;
|
|
@@ -192,20 +201,27 @@ export interface AgentLoopOptions {
|
|
|
192
201
|
* The endpoint, what to ask the model for, and how long it may keep calling tools.
|
|
193
202
|
* `toolDiscovery` absent is eager, `maxRetries` absent is none, and `contextLength` is handed
|
|
194
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`.
|
|
195
205
|
*/
|
|
196
206
|
config: Endpoint & ModelParams & Pick<ToolPolicy, "maxToolIterations"> & Partial<Pick<ToolPolicy, "toolDiscovery">> & Partial<RetryPolicy> & {
|
|
197
207
|
contextLength?: number;
|
|
198
208
|
};
|
|
199
|
-
/**
|
|
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
|
+
*/
|
|
200
213
|
system?: string;
|
|
201
214
|
/** The transcript so far, ending in the question. Not written to; see the result's `messages`. */
|
|
202
215
|
messages: OpenAI.ChatCompletionMessageParam[];
|
|
203
216
|
/**
|
|
204
217
|
* Every tool this run may reach. Eager mode sends all of them; on-demand mode sends the ones
|
|
205
|
-
* loaded so far, by name.
|
|
218
|
+
* loaded so far, by name; proxied mode declares none, and answers a load with them instead.
|
|
206
219
|
*/
|
|
207
220
|
tools?: OpenAI.ChatCompletionTool[];
|
|
208
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* The same tools as a name-only catalogue. On-demand and proxied modes need it, and are eager
|
|
223
|
+
* without it.
|
|
224
|
+
*/
|
|
209
225
|
catalog?: CatalogServer[];
|
|
210
226
|
/**
|
|
211
227
|
* How the declared tools are ordered before each request. By name unless told otherwise, so a
|
|
@@ -218,6 +234,13 @@ export interface AgentLoopOptions {
|
|
|
218
234
|
* `load_tools` — because a model with the menu still in front of it shops: it reloads what it
|
|
219
235
|
* has or picks a sibling. Everything comes back on the step after. `preselectRouting` trades
|
|
220
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.
|
|
221
244
|
*/
|
|
222
245
|
preselected?: readonly string[];
|
|
223
246
|
/**
|
|
@@ -233,11 +256,14 @@ export interface AgentLoopOptions {
|
|
|
233
256
|
* Appended is before `toolOrder` has its say: sorted, a preselected tool lands at its name's
|
|
234
257
|
* place and moves every definition after it, so only `toolOrder: false` keeps a carried array
|
|
235
258
|
* a strict prefix. Either way the first step's system prompt is the second's, and so is its
|
|
236
|
-
* tool array until something else is loaded. Nothing to do without a preselection,
|
|
237
|
-
* mode.
|
|
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.
|
|
238
261
|
*/
|
|
239
262
|
preselectRouting?: "exclusive" | "append";
|
|
240
|
-
/**
|
|
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
|
+
*/
|
|
241
267
|
loaded?: Iterable<string>;
|
|
242
268
|
/** Runs one tool call and returns what the model reads. What it throws, the model reads too. */
|
|
243
269
|
dispatch: (call: ToolCallRequest, signal?: AbortSignal) => Promise<string>;
|
|
@@ -328,6 +354,19 @@ export interface AgentLoopOptions {
|
|
|
328
354
|
onRequest?: (request: AgentLoopRequest) => void;
|
|
329
355
|
/** Each turn as it comes back, before its tools run. Recovered calls are in it as calls. */
|
|
330
356
|
onTurn?: (turn: Turn, step: number) => void;
|
|
357
|
+
/**
|
|
358
|
+
* Each message as the loop appends it to the transcript, in transcript order, and awaited before
|
|
359
|
+
* the loop goes on — so a host that stores its transcript has written the assistant message
|
|
360
|
+
* before its tools run, and every tool result before the next request is sent.
|
|
361
|
+
*
|
|
362
|
+
* The assistant message is the one replayed on later requests — arguments repaired, recovered
|
|
363
|
+
* calls folded in — and comes with its `turn`, as `onTurn` was handed it, for the `reasoning` a
|
|
364
|
+
* host keeps beside the message. A tool result comes without one, and so do the two messages
|
|
365
|
+
* a proxied run writes for its `preselected` shortlist, told as step zero's before any request.
|
|
366
|
+
* What it returns is ignored: `beforeStep` is the one way to rewrite the transcript. What it
|
|
367
|
+
* throws ends the run, and it is not called again for that run.
|
|
368
|
+
*/
|
|
369
|
+
onMessage?: (message: OpenAI.ChatCompletionMessageParam, step: number, turn?: Turn) => void | Promise<void>;
|
|
331
370
|
/**
|
|
332
371
|
* How many times an answer cut off at `maxTokens` is continued, zero — the default — for never.
|
|
333
372
|
* Opt-in because it spends another request, and on a server that does not continue a trailing
|
|
@@ -343,11 +382,18 @@ export interface AgentLoopResult {
|
|
|
343
382
|
messages: OpenAI.ChatCompletionMessageParam[];
|
|
344
383
|
/** Summed over every turn of the run. */
|
|
345
384
|
usage: TurnUsage;
|
|
346
|
-
/**
|
|
385
|
+
/**
|
|
386
|
+
* Every call, `load_tools` included, in the order they were made, each under its call id — a
|
|
387
|
+
* proxied preselection's among them, first.
|
|
388
|
+
*/
|
|
347
389
|
toolCalls: ToolCallOutcome[];
|
|
348
|
-
/**
|
|
390
|
+
/**
|
|
391
|
+
* What is loaded at the end, for `carryOver`. Empty in eager mode, and in proxied mode: a
|
|
392
|
+
* definition loaded there is already in the history, and one a compaction folded away has to be
|
|
393
|
+
* loadable again rather than answered "already loaded".
|
|
394
|
+
*/
|
|
349
395
|
loaded: string[];
|
|
350
|
-
/** The tools the model actually called, `load_tools` excluded. */
|
|
396
|
+
/** The tools the model actually called, `load_tools` excluded and `call_tool` looked through. */
|
|
351
397
|
used: string[];
|
|
352
398
|
/** The hooks' notes from before the first request. */
|
|
353
399
|
notes: HookNote[];
|
|
@@ -390,6 +436,15 @@ export interface AgentLoopResult {
|
|
|
390
436
|
* until a turn asks for none. Throws when `maxToolIterations` is spent, when stopped, and on
|
|
391
437
|
* whatever `runTurn` throws — `ContextOverflow` among them, however it was found out.
|
|
392
438
|
*
|
|
439
|
+
* What it throws carries the run as it stood — `messages`, `usage`, `toolCalls`, `loaded` and
|
|
440
|
+
* `used` — because the steps before a failure were real and a host that stores its transcript
|
|
441
|
+
* has nothing else to store them from. A spent budget is a `ToolIterationLimit`, an overflow is
|
|
442
|
+
* an `AgentLoopOverflow`, which is still a `ContextOverflow`, and anything else is an
|
|
443
|
+
* `AgentLoopError` with what was caught as its `cause` and its message as its own; `failedRun`
|
|
444
|
+
* reads the run off any of the three. A failure while a step's tools run leaves the transcript
|
|
445
|
+
* well-formed: the results that came back are kept, and each call without one is answered with a
|
|
446
|
+
* line saying it was stopped or never run, since a call with no result is refused on replay.
|
|
447
|
+
*
|
|
393
448
|
* On-demand loading is handled here, `load_tools` and all: the catalogue rides on the system
|
|
394
449
|
* prompt unchanged from step to step, a load adds to the tool array — which every request sends
|
|
395
450
|
* in the stable order `toolOrder` asks for — and
|
|
@@ -407,6 +462,13 @@ export interface AgentLoopResult {
|
|
|
407
462
|
* continuation, and a step that reported none, or reported a cache count above its prompt, is not
|
|
408
463
|
* recorded. The ledger comes back in the result and on every `usage` event; see `tokensBetween`.
|
|
409
464
|
*
|
|
465
|
+
* Proxied discovery is the same catalogue behind a tool array that never moves: every step
|
|
466
|
+
* declares `PROXY_TOOLS` and nothing else, a load answers with the definitions themselves, and a
|
|
467
|
+
* `call_tool` is dispatched, counted and reported as the tool it names. A preselection has no
|
|
468
|
+
* tool array to go into, so it is written into the transcript as a `load_tools` exchange after
|
|
469
|
+
* the question. `call_tool` is honoured on demand too, unless the host has a tool of that name —
|
|
470
|
+
* a session switched out of proxied mode still has it in its history, and the model copies it.
|
|
471
|
+
*
|
|
410
472
|
* @param options The config, transcript, tools and dispatcher, plus the optional hooks, events
|
|
411
473
|
* and cancellation. See `AgentLoopOptions`.
|
|
412
474
|
*/
|