@cubicecho/agent-core 2.20.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 +45 -1
- package/dist/agent-loop.d.ts +22 -0
- package/dist/agent-loop.js +150 -30
- package/dist/errors.d.ts +89 -0
- package/dist/errors.js +77 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/llms.txt +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
|
@@ -673,6 +673,50 @@ with pre-turn work to overlap with the hooks — compaction, a preselection —
|
|
|
673
673
|
applies `withContext` to the current question as well, calls `notify` after, and passes no `hooks`
|
|
674
674
|
(see [Hooks](#hooks)).
|
|
675
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
|
+
|
|
676
720
|
`resolveApiKey` is exported and not applied, because which key an endpoint gets is a rule a
|
|
677
721
|
consumer states and a library guessing it could send one where it was not meant to go. The rule
|
|
678
722
|
it encodes is the conservative one: an endpoint's own key wins; one that names a base URL of its
|
package/dist/agent-loop.d.ts
CHANGED
|
@@ -354,6 +354,19 @@ export interface AgentLoopOptions {
|
|
|
354
354
|
onRequest?: (request: AgentLoopRequest) => void;
|
|
355
355
|
/** Each turn as it comes back, before its tools run. Recovered calls are in it as calls. */
|
|
356
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>;
|
|
357
370
|
/**
|
|
358
371
|
* How many times an answer cut off at `maxTokens` is continued, zero — the default — for never.
|
|
359
372
|
* Opt-in because it spends another request, and on a server that does not continue a trailing
|
|
@@ -423,6 +436,15 @@ export interface AgentLoopResult {
|
|
|
423
436
|
* until a turn asks for none. Throws when `maxToolIterations` is spent, when stopped, and on
|
|
424
437
|
* whatever `runTurn` throws — `ContextOverflow` among them, however it was found out.
|
|
425
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
|
+
*
|
|
426
448
|
* On-demand loading is handled here, `load_tools` and all: the catalogue rides on the system
|
|
427
449
|
* prompt unchanged from step to step, a load adds to the tool array — which every request sends
|
|
428
450
|
* in the stable order `toolOrder` asks for — and
|
package/dist/agent-loop.js
CHANGED
|
@@ -2,11 +2,11 @@ import { charsPerTokenFor } from "./calibration.js";
|
|
|
2
2
|
import { capabilitiesFor, effortFor, modelCapabilitiesFor, } from "./capabilities.js";
|
|
3
3
|
import { firstTokenMs, getClient, NO_KEY, timeoutMs } from "./client.js";
|
|
4
4
|
import { continueTurn } from "./continuation.js";
|
|
5
|
-
import { errorMessage } from "./errors.js";
|
|
5
|
+
import { AgentLoopError, AgentLoopOverflow, errorMessage, ToolIterationLimit, } from "./errors.js";
|
|
6
6
|
import { runMetrics } from "./events.js";
|
|
7
7
|
import { configureHooks, gather, notify, turnIndex, turnMessages, withContext, } from "./hooks.js";
|
|
8
8
|
import { rebaseLedger, recordRequest } from "./ledger.js";
|
|
9
|
-
import { contextTokens, toolsChars } from "./retry.js";
|
|
9
|
+
import { ContextOverflow, contextTokens, toolsChars } from "./retry.js";
|
|
10
10
|
import { runTurn } from "./run-turn.js";
|
|
11
11
|
import { relaxTools, sanitizeTools } from "./schema-compat.js";
|
|
12
12
|
import { askJson, tryAsk } from "./side-task.js";
|
|
@@ -198,11 +198,24 @@ function cacheDiagnosis(previous, messages, tools, usage) {
|
|
|
198
198
|
found.cacheBreakReason = breakReason(previous, messages, tools);
|
|
199
199
|
return found;
|
|
200
200
|
}
|
|
201
|
+
/** What a tool call the run was stopped during is answered with, in place of a result. */
|
|
202
|
+
const STOPPED_CALL = "Stopped before this call finished.";
|
|
203
|
+
/** The same for a call of that step the loop never got to. */
|
|
204
|
+
const UNRUN_CALL = "Not run: the run stopped first.";
|
|
201
205
|
/**
|
|
202
206
|
* Runs a question to its answer: one `runTurn` per step, the tools it asks for between them,
|
|
203
207
|
* until a turn asks for none. Throws when `maxToolIterations` is spent, when stopped, and on
|
|
204
208
|
* whatever `runTurn` throws — `ContextOverflow` among them, however it was found out.
|
|
205
209
|
*
|
|
210
|
+
* What it throws carries the run as it stood — `messages`, `usage`, `toolCalls`, `loaded` and
|
|
211
|
+
* `used` — because the steps before a failure were real and a host that stores its transcript
|
|
212
|
+
* has nothing else to store them from. A spent budget is a `ToolIterationLimit`, an overflow is
|
|
213
|
+
* an `AgentLoopOverflow`, which is still a `ContextOverflow`, and anything else is an
|
|
214
|
+
* `AgentLoopError` with what was caught as its `cause` and its message as its own; `failedRun`
|
|
215
|
+
* reads the run off any of the three. A failure while a step's tools run leaves the transcript
|
|
216
|
+
* well-formed: the results that came back are kept, and each call without one is answered with a
|
|
217
|
+
* line saying it was stopped or never run, since a call with no result is refused on replay.
|
|
218
|
+
*
|
|
206
219
|
* On-demand loading is handled here, `load_tools` and all: the catalogue rides on the system
|
|
207
220
|
* prompt unchanged from step to step, a load adds to the tool array — which every request sends
|
|
208
221
|
* in the stable order `toolOrder` asks for — and
|
|
@@ -231,8 +244,39 @@ function cacheDiagnosis(previous, messages, tools, usage) {
|
|
|
231
244
|
* and cancellation. See `AgentLoopOptions`.
|
|
232
245
|
*/
|
|
233
246
|
export async function runAgentLoop(options) {
|
|
247
|
+
// Until the steps have set themselves up there is only what the caller handed over.
|
|
248
|
+
const standing = {
|
|
249
|
+
read: () => ({
|
|
250
|
+
messages: [...options.messages],
|
|
251
|
+
usage: { prompt: 0, completion: 0, total: 0, cached: 0 },
|
|
252
|
+
toolCalls: [],
|
|
253
|
+
loaded: [],
|
|
254
|
+
used: [],
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
let result;
|
|
258
|
+
try {
|
|
259
|
+
result = await runSteps(options, standing);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
// Wrapped rather than annotated: an abort's reason is one object shared by every run under
|
|
263
|
+
// the signal, so the run cannot be hung on the error itself. An overflow keeps its class,
|
|
264
|
+
// because `instanceof ContextOverflow` is how a caller knows to compact and try again.
|
|
265
|
+
throw error instanceof ContextOverflow
|
|
266
|
+
? new AgentLoopOverflow(error.message, standing.read(), { cause: error })
|
|
267
|
+
: new AgentLoopError(errorMessage(error), standing.read(), { cause: error });
|
|
268
|
+
}
|
|
269
|
+
if (result)
|
|
270
|
+
return result;
|
|
271
|
+
throw new ToolIterationLimit(`Stopped after ${options.config.maxToolIterations} tool iterations.`, standing.read());
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* The steps of `runAgentLoop`, throwing what they caught as it was caught. Resolves to nothing
|
|
275
|
+
* when `maxToolIterations` is spent, and tells `standing` how to read the run either way.
|
|
276
|
+
*/
|
|
277
|
+
async function runSteps(options, standing) {
|
|
234
278
|
const { config, system = "", tools = [], catalog = [], dispatch, hooks, signal } = options;
|
|
235
|
-
const { onRequest, onTurn, onToolCall, onToolResult, beforeStep, parallel = false, recoverToolCalls: recover = true, maxContinuations = 0, toolOrder = true, dedupeToolCalls = true, preselectRouting = "exclusive", } = options;
|
|
279
|
+
const { onRequest, onTurn, onMessage, onToolCall, onToolResult, beforeStep, parallel = false, recoverToolCalls: recover = true, maxContinuations = 0, toolOrder = true, dedupeToolCalls = true, preselectRouting = "exclusive", } = options;
|
|
236
280
|
const dedupable = typeof dedupeToolCalls === "function" ? dedupeToolCalls : () => dedupeToolCalls;
|
|
237
281
|
const started = Date.now();
|
|
238
282
|
// What the loop emitted, less the token deltas, for `runMetrics` at the end. Stamped here rather
|
|
@@ -284,6 +328,30 @@ export async function runAgentLoop(options) {
|
|
|
284
328
|
// in the order things were added, so a load appends and never reshuffles what went before.
|
|
285
329
|
const byName = (names) => [...names].flatMap((name) => definitions.get(name) ?? []);
|
|
286
330
|
let messages = [...options.messages];
|
|
331
|
+
const usage = { prompt: 0, completion: 0, total: 0, cached: 0 };
|
|
332
|
+
const toolCalls = [];
|
|
333
|
+
standing.read = () => ({
|
|
334
|
+
messages,
|
|
335
|
+
usage,
|
|
336
|
+
toolCalls,
|
|
337
|
+
// As the result would have said it: proxied, nothing is loaded that the history does not hold.
|
|
338
|
+
loaded: proxied ? [] : [...loaded],
|
|
339
|
+
used: [...used],
|
|
340
|
+
});
|
|
341
|
+
// Once `onMessage` has thrown the run is ending on that, and the results still to be written
|
|
342
|
+
// into the transcript are not offered to a host that has just failed to take one.
|
|
343
|
+
let heard = true;
|
|
344
|
+
const announce = async (message, step, turn) => {
|
|
345
|
+
if (!onMessage || !heard)
|
|
346
|
+
return;
|
|
347
|
+
try {
|
|
348
|
+
await onMessage(message, step, turn);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
heard = false;
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
287
355
|
// Held by reference rather than by index, so a `beforeStep` that folds the head into a summary
|
|
288
356
|
// moves the question without losing it — and one that summarises the question away takes the
|
|
289
357
|
// hooks' context with it, which is right.
|
|
@@ -297,8 +365,6 @@ export async function runAgentLoop(options) {
|
|
|
297
365
|
: { context: "", notes: [] };
|
|
298
366
|
// Read once, so what the result hands back is what every step of this run said.
|
|
299
367
|
const preface = hooks?.preface ?? configureHooks().preface;
|
|
300
|
-
const usage = { prompt: 0, completion: 0, total: 0, cached: 0 };
|
|
301
|
-
const toolCalls = [];
|
|
302
368
|
const loads = { toolsLoaded: 0, redundantLoads: 0, unknownToolNames: 0 };
|
|
303
369
|
let previous;
|
|
304
370
|
let ledger = options.ledger ?? [];
|
|
@@ -328,11 +394,19 @@ export async function runAgentLoop(options) {
|
|
|
328
394
|
loads.toolsLoaded += names.length;
|
|
329
395
|
for (const name of names)
|
|
330
396
|
loaded.add(name);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
397
|
+
const exchange = [
|
|
398
|
+
{
|
|
399
|
+
role: "assistant",
|
|
400
|
+
content: null,
|
|
401
|
+
tool_calls: [{ id, type: "function", function: { name: LOAD_TOOLS, arguments: args } }],
|
|
402
|
+
},
|
|
403
|
+
{ role: "tool", tool_call_id: id, content },
|
|
404
|
+
];
|
|
405
|
+
// Both written before either is announced, so a host that throws on the first leaves a call
|
|
406
|
+
// with its result. Told as step zero's, with no turn: no request was made for them.
|
|
407
|
+
messages.push(...exchange);
|
|
408
|
+
for (const message of exchange)
|
|
409
|
+
await announce(message, 0);
|
|
336
410
|
}
|
|
337
411
|
for (let step = 0; step < config.maxToolIterations; step++) {
|
|
338
412
|
// A stop aborts the request in flight, but a tool call already handed off runs to its own
|
|
@@ -476,7 +550,7 @@ export async function runAgentLoop(options) {
|
|
|
476
550
|
return { call, error, normal: "{}" };
|
|
477
551
|
}
|
|
478
552
|
});
|
|
479
|
-
|
|
553
|
+
const assistant = {
|
|
480
554
|
role: "assistant",
|
|
481
555
|
content: content || null,
|
|
482
556
|
...(parsed.length
|
|
@@ -487,7 +561,9 @@ export async function runAgentLoop(options) {
|
|
|
487
561
|
})),
|
|
488
562
|
}
|
|
489
563
|
: {}),
|
|
490
|
-
}
|
|
564
|
+
};
|
|
565
|
+
messages.push(assistant);
|
|
566
|
+
await announce(assistant, step, shown);
|
|
491
567
|
if (!calls.length) {
|
|
492
568
|
let afterTurn = Promise.resolve([]);
|
|
493
569
|
if (hooks) {
|
|
@@ -531,13 +607,15 @@ export async function runAgentLoop(options) {
|
|
|
531
607
|
// Per step, not per run: the answer to a call made two steps ago was true before the tools in
|
|
532
608
|
// between ran, and the file the model read may be the file it has since written.
|
|
533
609
|
const answered = new Map();
|
|
534
|
-
|
|
610
|
+
// A `call_tool` is shown, counted and dispatched as the tool it names. Read from the repaired
|
|
611
|
+
// arguments where there are any, since the model's own may be almost-JSON.
|
|
612
|
+
const named = ({ call, args, normal }) => proxies && call.function.name === CALL_TOOL
|
|
613
|
+
? shownCall(CALL_TOOL, args ? normal : call.function.arguments)
|
|
614
|
+
: { name: call.function.name, input: call.function.arguments };
|
|
615
|
+
const run = async (entry) => {
|
|
616
|
+
const { call, args, error: unreadable, normal } = entry;
|
|
535
617
|
const proxy = proxies && call.function.name === CALL_TOOL;
|
|
536
|
-
|
|
537
|
-
// arguments where there are any, since the model's own may be almost-JSON.
|
|
538
|
-
const { name, input: raw } = proxy
|
|
539
|
-
? shownCall(CALL_TOOL, args ? normal : call.function.arguments)
|
|
540
|
-
: { name: call.function.name, input: call.function.arguments };
|
|
618
|
+
const { name, input: raw } = named(entry);
|
|
541
619
|
// Looked through before the call is announced, so a host is told of the tool it names with
|
|
542
620
|
// that tool's arguments. A name outside the catalogue is refused here, so `call_tool` reaches
|
|
543
621
|
// nothing a load could not, and the refusal is the call's answer below.
|
|
@@ -608,23 +686,65 @@ export async function runAgentLoop(options) {
|
|
|
608
686
|
onToolResult?.(result);
|
|
609
687
|
return result;
|
|
610
688
|
};
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
689
|
+
// By position, so a call that came back while another was being stopped is still found.
|
|
690
|
+
const outcomes = parsed.map(() => undefined);
|
|
691
|
+
// How many of the step's calls were handed off, and how many have their result in the
|
|
692
|
+
// transcript. Both count from the front, since results are written in the order asked.
|
|
693
|
+
let begun = 0;
|
|
694
|
+
let kept = 0;
|
|
695
|
+
const keep = async (id, content) => {
|
|
696
|
+
const result = {
|
|
697
|
+
role: "tool",
|
|
698
|
+
tool_call_id: id,
|
|
699
|
+
content,
|
|
700
|
+
};
|
|
701
|
+
messages.push(result);
|
|
702
|
+
kept++;
|
|
703
|
+
await announce(result, step);
|
|
704
|
+
};
|
|
705
|
+
try {
|
|
706
|
+
if (parallel) {
|
|
618
707
|
signal?.throwIfAborted();
|
|
619
|
-
|
|
708
|
+
begun = parsed.length;
|
|
709
|
+
await Promise.all(parsed.map(async (call, at) => {
|
|
710
|
+
outcomes[at] = await run(call);
|
|
711
|
+
}));
|
|
712
|
+
for (const { id, name, ok, content } of outcomes.flatMap((outcome) => outcome ?? [])) {
|
|
713
|
+
toolCalls.push({ id, name, ok });
|
|
714
|
+
await keep(id, content);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
for (const [at, call] of parsed.entries()) {
|
|
719
|
+
signal?.throwIfAborted();
|
|
720
|
+
begun = at + 1;
|
|
721
|
+
const { id, name, ok, content } = await run(call);
|
|
722
|
+
toolCalls.push({ id, name, ok });
|
|
723
|
+
await keep(id, content);
|
|
724
|
+
}
|
|
620
725
|
}
|
|
621
726
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
727
|
+
catch (error) {
|
|
728
|
+
// A call with no result is a transcript no endpoint takes back, so every call of the step
|
|
729
|
+
// is answered before the throw: with what it returned where it had, and otherwise with a
|
|
730
|
+
// line saying it was stopped, or never run. Only a call that was made counts in `toolCalls`.
|
|
731
|
+
for (const [at, entry] of parsed.entries()) {
|
|
732
|
+
const { call } = entry;
|
|
733
|
+
if (at < kept)
|
|
734
|
+
continue;
|
|
735
|
+
const outcome = outcomes[at];
|
|
736
|
+
// Under the name it was announced by, which for a `call_tool` is the tool it names.
|
|
737
|
+
if (at < begun) {
|
|
738
|
+
toolCalls.push({ id: call.id, name: named(entry).name, ok: outcome?.ok ?? false });
|
|
739
|
+
}
|
|
740
|
+
// The run is already ending on `error`; a host that cannot take this result does not
|
|
741
|
+
// get to replace it.
|
|
742
|
+
await keep(call.id, outcome?.content ?? (at < begun ? STOPPED_CALL : UNRUN_CALL)).catch(() => { });
|
|
743
|
+
}
|
|
744
|
+
throw error;
|
|
625
745
|
}
|
|
626
746
|
}
|
|
627
|
-
|
|
747
|
+
return undefined;
|
|
628
748
|
}
|
|
629
749
|
/**
|
|
630
750
|
* Makes a call at most once per key, sharing the in-flight promise so two identical calls in one
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
import type { ToolCallOutcome } from "./agent-loop.ts";
|
|
3
|
+
import { ContextOverflow } from "./retry.ts";
|
|
4
|
+
import type { TurnUsage } from "./stream.ts";
|
|
1
5
|
/**
|
|
2
6
|
* What went wrong, as a string.
|
|
3
7
|
*
|
|
@@ -8,3 +12,88 @@
|
|
|
8
12
|
* @param error Whatever a `catch` bound. Anything that is not an `Error` is stringified.
|
|
9
13
|
*/
|
|
10
14
|
export declare const errorMessage: (error: unknown) => string;
|
|
15
|
+
/**
|
|
16
|
+
* A run as it stood when `runAgentLoop` gave up on it.
|
|
17
|
+
*
|
|
18
|
+
* The loop works on its own copy of the transcript and hands it back only on success, so without
|
|
19
|
+
* this a spent budget, a stop or a refused request took every step before it along — steps whose
|
|
20
|
+
* assistant turns and tool results were real, and that a host storing its transcript has to keep.
|
|
21
|
+
*/
|
|
22
|
+
export interface AgentLoopFailure {
|
|
23
|
+
/**
|
|
24
|
+
* The transcript, as the result's `messages` would have been: every assistant turn and tool
|
|
25
|
+
* result the run added, and no system prompt. Well-formed — a call the run was stopped during
|
|
26
|
+
* has a result saying so. The reply of a request that was in flight is not in it.
|
|
27
|
+
*/
|
|
28
|
+
messages: OpenAI.ChatCompletionMessageParam[];
|
|
29
|
+
/** Summed over the turns that came back. A request that failed or was stopped adds nothing. */
|
|
30
|
+
usage: TurnUsage;
|
|
31
|
+
/** Every call that was made, in order. One the run was stopped during is `ok: false`. */
|
|
32
|
+
toolCalls: ToolCallOutcome[];
|
|
33
|
+
/** What was loaded when the run failed, for `carryOver`. Empty in eager and proxied modes. */
|
|
34
|
+
loaded: string[];
|
|
35
|
+
/** The tools the model had called, `load_tools` excluded. */
|
|
36
|
+
used: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What `runAgentLoop` throws, carrying the run as it stood and what was caught as `cause`.
|
|
40
|
+
*
|
|
41
|
+
* Its own class rather than fields hung on the error that was caught, because a stop throws the
|
|
42
|
+
* signal's reason and that is one object for every run under the signal. The message is the
|
|
43
|
+
* cause's own, so a host that logs `errorMessage(error)` logs what it always did. A stop is told
|
|
44
|
+
* the way it was before — by the signal the host aborted, not by the class of what came out.
|
|
45
|
+
*/
|
|
46
|
+
export declare class AgentLoopError extends Error implements AgentLoopFailure {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly messages: OpenAI.ChatCompletionMessageParam[];
|
|
49
|
+
readonly usage: TurnUsage;
|
|
50
|
+
readonly toolCalls: ToolCallOutcome[];
|
|
51
|
+
readonly loaded: string[];
|
|
52
|
+
readonly used: string[];
|
|
53
|
+
/**
|
|
54
|
+
* @param message What went wrong — the cause's own message, where there is a cause.
|
|
55
|
+
* @param run The run as it stood. Held, not copied.
|
|
56
|
+
* @param options The standard `cause`: what the loop caught, absent where the loop itself gave up.
|
|
57
|
+
*/
|
|
58
|
+
constructor(message: string, run: AgentLoopFailure, options?: ErrorOptions);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `maxToolIterations` was spent with the model still asking for tools.
|
|
62
|
+
*
|
|
63
|
+
* Named so a host can tell it from a failure without matching the message, which is the same
|
|
64
|
+
* sentence it has always been. There is no `cause`: nothing was caught, the loop stopped itself.
|
|
65
|
+
*/
|
|
66
|
+
export declare class ToolIterationLimit extends AgentLoopError {
|
|
67
|
+
readonly name: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A `ContextOverflow` out of `runAgentLoop`, carrying the run as it stood.
|
|
71
|
+
*
|
|
72
|
+
* A subclass of `ContextOverflow` rather than an `AgentLoopError` around one, because
|
|
73
|
+
* `instanceof ContextOverflow` is how a caller knows to compact and send again, and wrapping
|
|
74
|
+
* would have made that test quietly false. The overflow the loop caught is the `cause`, and the
|
|
75
|
+
* endpoint's own error, where there was one, is that one's `cause` in turn.
|
|
76
|
+
*/
|
|
77
|
+
export declare class AgentLoopOverflow extends ContextOverflow implements AgentLoopFailure {
|
|
78
|
+
readonly messages: OpenAI.ChatCompletionMessageParam[];
|
|
79
|
+
readonly usage: TurnUsage;
|
|
80
|
+
readonly toolCalls: ToolCallOutcome[];
|
|
81
|
+
readonly loaded: string[];
|
|
82
|
+
readonly used: string[];
|
|
83
|
+
/**
|
|
84
|
+
* @param message The overflow's own message.
|
|
85
|
+
* @param run The run as it stood. Held, not copied.
|
|
86
|
+
* @param options The standard `cause`: the `ContextOverflow` the loop caught.
|
|
87
|
+
*/
|
|
88
|
+
constructor(message: string, run: AgentLoopFailure, options?: ErrorOptions);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The run a failed `runAgentLoop` left behind, read off whatever it threw, or nothing.
|
|
92
|
+
*
|
|
93
|
+
* Two classes carry it — an overflow has to stay a `ContextOverflow` and so cannot also be an
|
|
94
|
+
* `AgentLoopError` — and a `catch` that only wants the transcript should not have to name both.
|
|
95
|
+
*
|
|
96
|
+
* @param error Whatever a `catch` around `runAgentLoop` bound. Anything the loop did not throw
|
|
97
|
+
* answers `undefined`.
|
|
98
|
+
*/
|
|
99
|
+
export declare const failedRun: (error: unknown) => AgentLoopFailure | undefined;
|
package/dist/errors.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ContextOverflow } from "./retry.js";
|
|
1
2
|
/**
|
|
2
3
|
* What went wrong, as a string.
|
|
3
4
|
*
|
|
@@ -8,3 +9,79 @@
|
|
|
8
9
|
* @param error Whatever a `catch` bound. Anything that is not an `Error` is stringified.
|
|
9
10
|
*/
|
|
10
11
|
export const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
12
|
+
/**
|
|
13
|
+
* What `runAgentLoop` throws, carrying the run as it stood and what was caught as `cause`.
|
|
14
|
+
*
|
|
15
|
+
* Its own class rather than fields hung on the error that was caught, because a stop throws the
|
|
16
|
+
* signal's reason and that is one object for every run under the signal. The message is the
|
|
17
|
+
* cause's own, so a host that logs `errorMessage(error)` logs what it always did. A stop is told
|
|
18
|
+
* the way it was before — by the signal the host aborted, not by the class of what came out.
|
|
19
|
+
*/
|
|
20
|
+
export class AgentLoopError extends Error {
|
|
21
|
+
name = "AgentLoopError";
|
|
22
|
+
messages;
|
|
23
|
+
usage;
|
|
24
|
+
toolCalls;
|
|
25
|
+
loaded;
|
|
26
|
+
used;
|
|
27
|
+
/**
|
|
28
|
+
* @param message What went wrong — the cause's own message, where there is a cause.
|
|
29
|
+
* @param run The run as it stood. Held, not copied.
|
|
30
|
+
* @param options The standard `cause`: what the loop caught, absent where the loop itself gave up.
|
|
31
|
+
*/
|
|
32
|
+
constructor(message, run, options) {
|
|
33
|
+
super(message, options);
|
|
34
|
+
this.messages = run.messages;
|
|
35
|
+
this.usage = run.usage;
|
|
36
|
+
this.toolCalls = run.toolCalls;
|
|
37
|
+
this.loaded = run.loaded;
|
|
38
|
+
this.used = run.used;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `maxToolIterations` was spent with the model still asking for tools.
|
|
43
|
+
*
|
|
44
|
+
* Named so a host can tell it from a failure without matching the message, which is the same
|
|
45
|
+
* sentence it has always been. There is no `cause`: nothing was caught, the loop stopped itself.
|
|
46
|
+
*/
|
|
47
|
+
export class ToolIterationLimit extends AgentLoopError {
|
|
48
|
+
name = "ToolIterationLimit";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A `ContextOverflow` out of `runAgentLoop`, carrying the run as it stood.
|
|
52
|
+
*
|
|
53
|
+
* A subclass of `ContextOverflow` rather than an `AgentLoopError` around one, because
|
|
54
|
+
* `instanceof ContextOverflow` is how a caller knows to compact and send again, and wrapping
|
|
55
|
+
* would have made that test quietly false. The overflow the loop caught is the `cause`, and the
|
|
56
|
+
* endpoint's own error, where there was one, is that one's `cause` in turn.
|
|
57
|
+
*/
|
|
58
|
+
export class AgentLoopOverflow extends ContextOverflow {
|
|
59
|
+
messages;
|
|
60
|
+
usage;
|
|
61
|
+
toolCalls;
|
|
62
|
+
loaded;
|
|
63
|
+
used;
|
|
64
|
+
/**
|
|
65
|
+
* @param message The overflow's own message.
|
|
66
|
+
* @param run The run as it stood. Held, not copied.
|
|
67
|
+
* @param options The standard `cause`: the `ContextOverflow` the loop caught.
|
|
68
|
+
*/
|
|
69
|
+
constructor(message, run, options) {
|
|
70
|
+
super(message, options);
|
|
71
|
+
this.messages = run.messages;
|
|
72
|
+
this.usage = run.usage;
|
|
73
|
+
this.toolCalls = run.toolCalls;
|
|
74
|
+
this.loaded = run.loaded;
|
|
75
|
+
this.used = run.used;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The run a failed `runAgentLoop` left behind, read off whatever it threw, or nothing.
|
|
80
|
+
*
|
|
81
|
+
* Two classes carry it — an overflow has to stay a `ContextOverflow` and so cannot also be an
|
|
82
|
+
* `AgentLoopError` — and a `catch` that only wants the transcript should not have to name both.
|
|
83
|
+
*
|
|
84
|
+
* @param error Whatever a `catch` around `runAgentLoop` bound. Anything the loop did not throw
|
|
85
|
+
* answers `undefined`.
|
|
86
|
+
*/
|
|
87
|
+
export const failedRun = (error) => error instanceof AgentLoopError || error instanceof AgentLoopOverflow ? error : undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { type ClientPoolOptions, configureClients, contextLimitFor, endpointId,
|
|
|
17
17
|
export { applyCompaction, COMPACT_AT, type CompactionOptions, type CompactionPlan, type CompactionRecord, type CompactionRunOptions, compactTranscript, KEEP_RATIO, type PruneOptions, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.ts";
|
|
18
18
|
export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } from "./config.ts";
|
|
19
19
|
export { type ContinueTurnOptions, continueTurn, isContinuable, } from "./continuation.ts";
|
|
20
|
-
export { errorMessage } from "./errors.ts";
|
|
20
|
+
export { AgentLoopError, type AgentLoopFailure, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from "./errors.ts";
|
|
21
21
|
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunMetrics, type RunMetricsOptions, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from "./events.ts";
|
|
22
22
|
export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, type HookContext, type HookEvent, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.ts";
|
|
23
23
|
export { estimateFrom, type LedgerEntry, type LedgerEstimateOptions, type LedgerRequest, rebaseLedger, recordRequest, type TokenLedger, tokensBetween, } from "./ledger.ts";
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { capabilitiesFor, EFFORT_LADDER, effortFor, expireCapabilities, modelCap
|
|
|
15
15
|
export { configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.js";
|
|
16
16
|
export { applyCompaction, COMPACT_AT, compactTranscript, KEEP_RATIO, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.js";
|
|
17
17
|
export { continueTurn, isContinuable, } from "./continuation.js";
|
|
18
|
-
export { errorMessage } from "./errors.js";
|
|
18
|
+
export { AgentLoopError, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from "./errors.js";
|
|
19
19
|
export { configureEvents, emit, endRun, fold, history, resetEvents, runMetrics, watch, } from "./events.js";
|
|
20
20
|
export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.js";
|
|
21
21
|
export { estimateFrom, rebaseLedger, recordRequest, tokensBetween, } from "./ledger.js";
|
package/llms.txt
CHANGED
|
@@ -115,7 +115,12 @@ Picking up an answer the token ceiling cut off, instead of keeping half of it.
|
|
|
115
115
|
|
|
116
116
|
### errors
|
|
117
117
|
|
|
118
|
+
- `AgentLoopError` — What `runAgentLoop` throws, carrying the run as it stood and what was caught as `cause`.
|
|
119
|
+
- `AgentLoopFailure` (type) — A run as it stood when `runAgentLoop` gave up on it.
|
|
120
|
+
- `AgentLoopOverflow` — A `ContextOverflow` out of `runAgentLoop`, carrying the run as it stood.
|
|
118
121
|
- `errorMessage` — What went wrong, as a string.
|
|
122
|
+
- `failedRun` — The run a failed `runAgentLoop` left behind, read off whatever it threw, or nothing.
|
|
123
|
+
- `ToolIterationLimit` — `maxToolIterations` was spent with the model still asking for tools.
|
|
119
124
|
|
|
120
125
|
### events
|
|
121
126
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.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",
|