@cubicecho/agent-core 4.14.0 → 4.15.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 +23 -3
- package/dist/run/agent-loop.d.ts +34 -9
- package/dist/run/agent-loop.js +30 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -887,9 +887,29 @@ turn is remembered, awaits it once the answer is out.
|
|
|
887
887
|
The loop gathers before its first request, beside its own preselection when `preselect` asked for
|
|
888
888
|
one, and nothing else of the host's can run beside those. A host with pre-turn work to overlap with
|
|
889
889
|
the hooks — a compaction — calls `gather` itself, applies `withContext` to the current question as
|
|
890
|
-
well, calls `notify` after
|
|
891
|
-
|
|
892
|
-
|
|
890
|
+
well, and calls `notify` after (see [Hooks](#hooks)). Such a host passes the question as it was
|
|
891
|
+
typed as `preselectQuery` too, or the preselector is shown the context in front of it and picks
|
|
892
|
+
tools for what was recalled rather than what was asked.
|
|
893
|
+
|
|
894
|
+
`hooks.fire` says which events the loop fires itself, and it fires no others: `sessionStart` and
|
|
895
|
+
`beforeTurn` gathered before the first request, `beforeCompact` beside each fold `compact` makes,
|
|
896
|
+
`afterTurn` once answered. Left out, it is all of them — `events` (`beforeTurn` by default), then
|
|
897
|
+
`afterTurn` and `beforeCompact` — which is what the loop always did. A host that fires the turn's
|
|
898
|
+
events around a step of its own still hands the loop its hooks for the fold:
|
|
899
|
+
|
|
900
|
+
```ts
|
|
901
|
+
await runAgentLoop({
|
|
902
|
+
...options,
|
|
903
|
+
compact: true,
|
|
904
|
+
hooks: { run, context, fire: [HookEvent.BeforeCompact] }, // beforeTurn and afterTurn are the host's
|
|
905
|
+
});
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
An event left out of `fire` is one the result has nothing for: `afterTurn` is resolved and empty
|
|
909
|
+
without `afterTurn`, and `notes` and `context` are empty without an injecting event, since the
|
|
910
|
+
host's own `gather` and `notify` hold those. `sessionEnd` and `sessionDelete` have no point in a
|
|
911
|
+
loop and are ignored there. `events` is deprecated: it is the injecting half of `fire`, and is
|
|
912
|
+
ignored once `fire` is given.
|
|
893
913
|
|
|
894
914
|
A host that stores its transcript — a row per message, so a crash or a stop mid-run still leaves
|
|
895
915
|
readable history — gets each message as the loop appends it, and the run as it stood when the loop
|
package/dist/run/agent-loop.d.ts
CHANGED
|
@@ -46,16 +46,38 @@ export interface AgentLoopRequest {
|
|
|
46
46
|
* The loop gathers before its first request, beside its own preselection where it was asked for
|
|
47
47
|
* one, and nothing else runs beside the two. A host with pre-turn work of its own to overlap with
|
|
48
48
|
* the hooks — a compaction — calls `gather` itself, puts the context on with `withContext`, calls
|
|
49
|
-
* `notify` after,
|
|
49
|
+
* `notify` after, and hands `preselectQuery` the question as it was written. It still passes
|
|
50
|
+
* `hooks` when the loop folds for it, with `fire` naming only `beforeCompact`, so what it fires
|
|
51
|
+
* itself is not fired a second time.
|
|
50
52
|
*/
|
|
51
53
|
export interface AgentLoopHooks {
|
|
52
54
|
run: HookRunner;
|
|
53
55
|
/** What the hooks are told. `reply` and `turn` are filled in for `afterTurn`. */
|
|
54
56
|
context: HookContext;
|
|
55
57
|
/**
|
|
56
|
-
*
|
|
58
|
+
* The events the loop fires, and the only ones: `sessionStart` and `beforeTurn` gathered before
|
|
59
|
+
* the first request, in the order listed; `beforeCompact` beside each fold `compact` makes; and
|
|
60
|
+
* `afterTurn` once the run has its answer. `sessionEnd` and `sessionDelete` are the host's, since
|
|
61
|
+
* the loop has no point that is either, and are ignored here.
|
|
62
|
+
*
|
|
63
|
+
* @defaultValue `events`, then `afterTurn` and `beforeCompact` — everything the loop has a point
|
|
64
|
+
* for, as it fired before this existed.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* For a host that fires some of them around the loop rather than inside it: one that tells
|
|
68
|
+
* `beforeTurn` and `afterTurn` around a whole step of its own, and wants only the fold's
|
|
69
|
+
* `beforeCompact` from the loop, passes `[HookEvent.BeforeCompact]`. With `afterTurn` left out the
|
|
70
|
+
* result's `afterTurn` is resolved and empty, and `notes` and `context` are empty with no
|
|
71
|
+
* injecting event listed — the host's own `gather` and `notify` hold those instead.
|
|
72
|
+
*/
|
|
73
|
+
fire?: readonly HookEvent[];
|
|
74
|
+
/**
|
|
75
|
+
* Run before the first request, when `fire` is not given. Ignored beside `fire`, which says the
|
|
76
|
+
* same thing with the other events in it.
|
|
57
77
|
*
|
|
58
78
|
* @defaultValue `[HookEvent.BeforeTurn]`
|
|
79
|
+
* @deprecated List them in `fire` instead, with `afterTurn` and `beforeCompact` where the loop
|
|
80
|
+
* should fire those too.
|
|
59
81
|
*/
|
|
60
82
|
events?: readonly HookEvent[];
|
|
61
83
|
/**
|
|
@@ -70,7 +92,7 @@ export interface AgentLoopHooks {
|
|
|
70
92
|
* @defaultValue The preface `configureHooks` holds.
|
|
71
93
|
*/
|
|
72
94
|
preface?: string;
|
|
73
|
-
/** Hears each note, from before the request and from `afterTurn`. */
|
|
95
|
+
/** Hears each note, from before the request, from `beforeCompact` and from `afterTurn`. */
|
|
74
96
|
onNote?: OnNote;
|
|
75
97
|
/**
|
|
76
98
|
* What the run's first message is numbered as in the uuids hooks are told — the host's stored
|
|
@@ -326,7 +348,8 @@ export interface AgentLoopOptions {
|
|
|
326
348
|
*/
|
|
327
349
|
onToolResult?: (result: ToolCallResult) => void;
|
|
328
350
|
/**
|
|
329
|
-
* Hooks gathered onto the question before the first request, and told the reply after
|
|
351
|
+
* Hooks gathered onto the question before the first request, and told the reply after — or
|
|
352
|
+
* whichever of those `fire` names.
|
|
330
353
|
*
|
|
331
354
|
* @remarks
|
|
332
355
|
* Only this question is touched — the last user message of `messages` — and the context is on
|
|
@@ -466,12 +489,13 @@ export interface AgentLoopResult {
|
|
|
466
489
|
* preselection picked it. Empty in eager mode, and where nothing was picked.
|
|
467
490
|
*/
|
|
468
491
|
preselected: string[];
|
|
469
|
-
/** The hooks' notes from before the first request. */
|
|
492
|
+
/** The hooks' notes from before the first request. Empty where `fire` lists no injecting event. */
|
|
470
493
|
notes: HookNote[];
|
|
471
494
|
/**
|
|
472
495
|
* The `<context>` blocks the hooks added to this question, as `withContext` takes them. Empty
|
|
473
|
-
* when no hook added any,
|
|
474
|
-
* turn's request to begin with this one's stores it
|
|
496
|
+
* when no hook added any, without `hooks`, and where `fire` lists no injecting event. Not in
|
|
497
|
+
* `messages`, so a host that wants the next turn's request to begin with this one's stores it
|
|
498
|
+
* beside the question; see `hooks`.
|
|
475
499
|
*/
|
|
476
500
|
context: string;
|
|
477
501
|
/**
|
|
@@ -486,8 +510,9 @@ export interface AgentLoopResult {
|
|
|
486
510
|
* The `afterTurn` hooks' notes, once they have run — failures only, as `notify` returns them.
|
|
487
511
|
* The loop does not wait for it, so the answer is never held for a hook; a host that stores the
|
|
488
512
|
* notes with the turn, or must not exit before the turn is remembered, awaits it. Each note
|
|
489
|
-
* reaches `onNote` as well. Already resolved and empty without `hooks`, and
|
|
490
|
-
* `
|
|
513
|
+
* reaches `onNote` as well. Already resolved and empty without `hooks`, and where `fire` leaves
|
|
514
|
+
* `afterTurn` out — a host that tells it itself has the notes from its own `notify`. Rejects only
|
|
515
|
+
* if `onNote` throws.
|
|
491
516
|
*/
|
|
492
517
|
afterTurn: Promise<HookNote[]>;
|
|
493
518
|
/**
|
package/dist/run/agent-loop.js
CHANGED
|
@@ -6,7 +6,7 @@ import { charsPerTokenFor } from '../endpoint/calibration.js';
|
|
|
6
6
|
import { capabilitiesFor, modelCapabilitiesFor, } from '../endpoint/capabilities.js';
|
|
7
7
|
import { firstTokenMs, getClient, loadingMs, servedSlots, timeoutMs } from '../endpoint/client.js';
|
|
8
8
|
import { CACHE_PROMPT_FIELD, SESSION_HEADER, SLOT_FIELD, slotAllocator } from '../endpoint/slots.js';
|
|
9
|
-
import { HookEvent } from '../hooks/hook-events.js';
|
|
9
|
+
import { HookEvent, INJECT_EVENTS } from '../hooks/hook-events.js';
|
|
10
10
|
import { configureHooks, gather, notify, turnIndex, turnMessages, withContext, } from '../hooks/hooks.js';
|
|
11
11
|
import { serverInstructionsPrompt } from '../tools/instructions.js';
|
|
12
12
|
import { preselect } from '../tools/preselect.js';
|
|
@@ -281,6 +281,7 @@ function ownSummariser(options, notice) {
|
|
|
281
281
|
*/
|
|
282
282
|
async function foldBeforeStep(transcript, reported, summarise, { options, charsPerToken, notice }) {
|
|
283
283
|
const { config, hooks } = options;
|
|
284
|
+
const told = hooks && firing(hooks).beforeCompact ? hooks : undefined;
|
|
284
285
|
const compactor = config.tasks?.compaction;
|
|
285
286
|
const plan = planCompaction(transcript, {
|
|
286
287
|
...(typeof options.compact === 'object' ? options.compact : {}),
|
|
@@ -293,13 +294,37 @@ async function foldBeforeStep(transcript, reported, summarise, { options, charsP
|
|
|
293
294
|
return transcript;
|
|
294
295
|
}
|
|
295
296
|
const folded = await compactTranscript(transcript, plan, summarise, {
|
|
296
|
-
...(
|
|
297
|
+
...(told ? { hooks: { run: told.run, context: told.context, onNote: told.onNote, offset: told.offset } } : {}),
|
|
297
298
|
});
|
|
298
299
|
if (folded !== transcript) {
|
|
299
300
|
notice(`compacted ${plan.toSummarise.length} messages into a summary`);
|
|
300
301
|
}
|
|
301
302
|
return folded;
|
|
302
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Which events the loop fires for these hooks, read from `fire`, or from `events` without it.
|
|
306
|
+
*
|
|
307
|
+
* @param hooks - The run's hooks.
|
|
308
|
+
* @returns The events to gather before the first request, and whether `afterTurn` and
|
|
309
|
+
* `beforeCompact` are the loop's to fire.
|
|
310
|
+
*
|
|
311
|
+
* @remarks
|
|
312
|
+
* Without `fire`, `events` goes to `gather` as it was given, unfiltered, so a host that had put
|
|
313
|
+
* something odd in it sees what it saw before. With `fire`, only the injecting events are gathered:
|
|
314
|
+
* a list that names every event is the whole of the loop's firing, and `afterTurn` gathered before
|
|
315
|
+
* the request would be the hooks told a reply that does not exist yet.
|
|
316
|
+
*/
|
|
317
|
+
function firing(hooks) {
|
|
318
|
+
if (!hooks.fire) {
|
|
319
|
+
return { before: hooks.events ?? [HookEvent.BeforeTurn], afterTurn: true, beforeCompact: true };
|
|
320
|
+
}
|
|
321
|
+
const fire = new Set(hooks.fire);
|
|
322
|
+
return {
|
|
323
|
+
before: [...fire].filter((event) => INJECT_EVENTS.has(event)),
|
|
324
|
+
afterTurn: fire.has(HookEvent.AfterTurn),
|
|
325
|
+
beforeCompact: fire.has(HookEvent.BeforeCompact),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
303
328
|
/**
|
|
304
329
|
* Tells the `afterTurn` hooks what was answered, with the turn it closed where the question is
|
|
305
330
|
* still in the transcript.
|
|
@@ -499,8 +524,9 @@ async function runSteps(options, standing, pin) {
|
|
|
499
524
|
// they cost the first request two round trips where one will do. A failing runner is a note, but
|
|
500
525
|
// a host's `onNote` that throws rejects the gather, perhaps while the preselection is still out:
|
|
501
526
|
// handled at once so that is no unhandled rejection, and still thrown below, where it always was.
|
|
527
|
+
const fired = hooks && firing(hooks);
|
|
502
528
|
const gathering = hooks
|
|
503
|
-
? gather(hooks.run,
|
|
529
|
+
? gather(hooks.run, fired?.before ?? [], hooks.context, {
|
|
504
530
|
signal,
|
|
505
531
|
onNote: hooks.onNote,
|
|
506
532
|
maxTokens: hooks.maxTokens,
|
|
@@ -729,7 +755,7 @@ async function runSteps(options, standing, pin) {
|
|
|
729
755
|
if (isAnswer || ended) {
|
|
730
756
|
// Not awaited: the answer is ready, and remembering it is not something to hold it for.
|
|
731
757
|
// Handed back instead, for a host that wants its notes.
|
|
732
|
-
const afterTurn = hooks ? tellAfterTurn(hooks, messages, questionAt(), turn.content) : Promise.resolve([]);
|
|
758
|
+
const afterTurn = hooks && fired?.afterTurn ? tellAfterTurn(hooks, messages, questionAt(), turn.content) : Promise.resolve([]);
|
|
733
759
|
const metrics = runMetrics(recorded, { contextLength: config.contextLength });
|
|
734
760
|
let outcome = RunOutcome.Answered;
|
|
735
761
|
if (ended) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.15.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",
|