@cubicecho/agent-core 4.13.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 +46 -9
- package/dist/core/config.d.ts +4 -1
- package/dist/run/agent-loop.d.ts +61 -17
- package/dist/run/agent-loop.js +48 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -768,8 +768,8 @@ distinctive than a word the catalogue shares has to have matched, and the tools
|
|
|
768
768
|
have to score well below the ones it kept — a hit just under the line scoring nearly as much as
|
|
769
769
|
one just over it means the ranking chose arbitrarily, which is the case a model is worth spending
|
|
770
770
|
on. Matching nothing is not confident either, since the words cannot tell a request that needs no
|
|
771
|
-
tools from one whose words are not in the catalogue. An empty
|
|
772
|
-
|
|
771
|
+
tools from one whose words are not in the catalogue. An empty model still means no preselection
|
|
772
|
+
at all, words included. `preselect` also takes a `temperature` and a
|
|
773
773
|
`reasoningEffort` and an `extraBody`, read as `ask` reads them, and `runAgentLoop({ preselect: true })` makes the call
|
|
774
774
|
itself — see "Its side tasks" below.
|
|
775
775
|
|
|
@@ -884,10 +884,32 @@ as the host passed them, this one as typed — which is why the snippet stores o
|
|
|
884
884
|
without waiting; a host that stores those notes with the turn, or would otherwise exit before the
|
|
885
885
|
turn is remembered, awaits it once the answer is out.
|
|
886
886
|
|
|
887
|
-
The loop gathers before its first request,
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
(see [Hooks](#hooks)).
|
|
887
|
+
The loop gathers before its first request, beside its own preselection when `preselect` asked for
|
|
888
|
+
one, and nothing else of the host's can run beside those. A host with pre-turn work to overlap with
|
|
889
|
+
the hooks — a compaction — calls `gather` itself, applies `withContext` to the current question as
|
|
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.
|
|
891
913
|
|
|
892
914
|
A host that stores its transcript — a row per message, so a crash or a stop mid-run still leaves
|
|
893
915
|
readable history — gets each message as the loop appends it, and the run as it stood when the loop
|
|
@@ -1488,16 +1510,31 @@ with `tasks.compaction` writing the summary and no falling back to the main mode
|
|
|
1488
1510
|
are off by default: a host that already calls `preselect` or compacts in `beforeStep` is not sent
|
|
1489
1511
|
a second request.
|
|
1490
1512
|
|
|
1513
|
+
The preselector falls back to the main model. With neither `tasks.toolSelect` nor
|
|
1514
|
+
`toolSelectModel` naming one, `preselect: true` asks `config.model` on the agent's own endpoint at
|
|
1515
|
+
`preselect`'s defaults: a host that opted in has asked for a preselection, and an empty name is a
|
|
1516
|
+
setting nobody filled in. Compaction has no such fallback. A host that wants no preselection passes
|
|
1517
|
+
no `preselect`. A spec layer that declines the task with `model: ""` leaves it absent from the
|
|
1518
|
+
resolved agent, as one nobody configured is, so under `preselect: true` the main model is asked
|
|
1519
|
+
there too.
|
|
1520
|
+
|
|
1521
|
+
What it asks about is the question as written: the last user message of `messages`, which the hooks'
|
|
1522
|
+
context never reaches because the loop puts that on the request. A host that put context on the
|
|
1523
|
+
question itself passes `preselectQuery`. The preselection and the hooks' `gather` run side by side,
|
|
1524
|
+
since neither reads the other, so asking for both costs the first request one round trip rather
|
|
1525
|
+
than two.
|
|
1526
|
+
|
|
1491
1527
|
What the loop picked it says twice. A `notice` event names the tools before the first `turn` event
|
|
1492
1528
|
— `PRESELECTED_NOTICE` and then the names, comma-separated — and the result's `preselected` is the
|
|
1493
1529
|
shortlist the run opened with, the host's own where it gave one and empty where nothing was picked.
|
|
1494
1530
|
A run that fails carries it too, beside `loaded` and `used`.
|
|
1495
1531
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1532
|
+
Called directly, `preselect` reads an empty model as no preselection and says nothing about it; it
|
|
1533
|
+
is only the loop that falls back. A host calling it itself whose settings read `""` as "use the
|
|
1534
|
+
run's model" says so in the call:
|
|
1498
1535
|
|
|
1499
1536
|
```ts
|
|
1500
|
-
|
|
1537
|
+
await preselect(config, settings.toolSelectModel || settings.model, catalog, prompt, options);
|
|
1501
1538
|
```
|
|
1502
1539
|
|
|
1503
1540
|
```ts
|
package/dist/core/config.d.ts
CHANGED
|
@@ -148,7 +148,10 @@ export type ToolDiscovery = (typeof ToolDiscovery)[keyof typeof ToolDiscovery];
|
|
|
148
148
|
export interface ToolPolicy {
|
|
149
149
|
/** How tool definitions reach the model. */
|
|
150
150
|
toolDiscovery: ToolDiscovery;
|
|
151
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* The model that does the preselection pass. Empty means don't preselect, to `preselect` itself;
|
|
153
|
+
* `runAgentLoop`, asked to preselect, reads empty as the run's own `model`.
|
|
154
|
+
*/
|
|
152
155
|
toolSelectModel: string;
|
|
153
156
|
/** Hard stop on runaway tool loops. */
|
|
154
157
|
maxToolIterations: number;
|
package/dist/run/agent-loop.d.ts
CHANGED
|
@@ -43,18 +43,41 @@ export interface AgentLoopRequest {
|
|
|
43
43
|
* The hooks a loop runs around one question. See `hooks.ts`.
|
|
44
44
|
*
|
|
45
45
|
* @remarks
|
|
46
|
-
* The loop gathers before its first request
|
|
47
|
-
* pre-turn work of its own to overlap with
|
|
48
|
-
* itself, puts the context on with `withContext`, calls
|
|
46
|
+
* The loop gathers before its first request, beside its own preselection where it was asked for
|
|
47
|
+
* one, and nothing else runs beside the two. A host with pre-turn work of its own to overlap with
|
|
48
|
+
* the hooks — a compaction — calls `gather` itself, puts the context on with `withContext`, calls
|
|
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.
|
|
49
52
|
*/
|
|
50
53
|
export interface AgentLoopHooks {
|
|
51
54
|
run: HookRunner;
|
|
52
55
|
/** What the hooks are told. `reply` and `turn` are filled in for `afterTurn`. */
|
|
53
56
|
context: HookContext;
|
|
54
57
|
/**
|
|
55
|
-
*
|
|
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.
|
|
56
77
|
*
|
|
57
78
|
* @defaultValue `[HookEvent.BeforeTurn]`
|
|
79
|
+
* @deprecated List them in `fire` instead, with `afterTurn` and `beforeCompact` where the loop
|
|
80
|
+
* should fire those too.
|
|
58
81
|
*/
|
|
59
82
|
events?: readonly HookEvent[];
|
|
60
83
|
/**
|
|
@@ -69,7 +92,7 @@ export interface AgentLoopHooks {
|
|
|
69
92
|
* @defaultValue The preface `configureHooks` holds.
|
|
70
93
|
*/
|
|
71
94
|
preface?: string;
|
|
72
|
-
/** Hears each note, from before the request and from `afterTurn`. */
|
|
95
|
+
/** Hears each note, from before the request, from `beforeCompact` and from `afterTurn`. */
|
|
73
96
|
onNote?: OnNote;
|
|
74
97
|
/**
|
|
75
98
|
* What the run's first message is numbered as in the uuids hooks are told — the host's stored
|
|
@@ -109,7 +132,8 @@ export interface AgentLoopOptions {
|
|
|
109
132
|
* @remarks
|
|
110
133
|
* `tasks` is a resolved agent's, and is read only for the side tasks the loop was asked to run:
|
|
111
134
|
* `tasks.toolSelect` under `preselect` and `tasks.compaction` under `compact`. `toolSelectModel`
|
|
112
|
-
* is the flattened spelling of the first, for a host that is not on the spec.
|
|
135
|
+
* is the flattened spelling of the first, for a host that is not on the spec. With neither
|
|
136
|
+
* naming a model, `preselect` asks `model` itself; `compact` never does.
|
|
113
137
|
*/
|
|
114
138
|
config: Endpoint & ModelParams & Pick<ToolPolicy, 'maxToolIterations'> & Partial<Pick<ToolPolicy, 'toolDiscovery' | 'toolSelectModel'>> & Partial<RetryPolicy> & {
|
|
115
139
|
contextLength?: number;
|
|
@@ -200,20 +224,37 @@ export interface AgentLoopOptions {
|
|
|
200
224
|
* The preselector is `config.tasks.toolSelect` with everything it states — its endpoint, model,
|
|
201
225
|
* ceiling, temperature and reasoning effort, keyed as `taskCall` keys a task against `config` —
|
|
202
226
|
* and otherwise `config.toolSelectModel` on the config's own endpoint at `preselect`'s defaults.
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
227
|
+
* With neither naming a model it is `config.model` itself, on the same terms as
|
|
228
|
+
* `toolSelectModel`: a host that opted in has asked for a preselection, and an empty name is a
|
|
229
|
+
* setting nobody filled in rather than a refusal. A host that wants none passes no `preselect`.
|
|
230
|
+
*
|
|
231
|
+
* What it is asked about is `preselectQuery`, and otherwise the text of the last user message as
|
|
232
|
+
* the host gave it — the hooks' context is put on the request, never on `messages`, so the
|
|
233
|
+
* tools are picked on what was asked rather than on what was recalled. Nothing is asked in eager
|
|
234
|
+
* mode, with no question, or when `preselected` is given — an empty one included, which is how
|
|
235
|
+
* a host says it has already decided.
|
|
206
236
|
*
|
|
207
237
|
* `true` takes `preselect`'s defaults; an object passes `keywords` and `maxPerLoad` through.
|
|
208
238
|
* Its notices arrive as `notice` events, and a preselection that fails picks nothing rather
|
|
209
239
|
* than ending the run. What it picked is a notice too, `PRESELECTED_NOTICE` and the names,
|
|
210
|
-
* before the first `turn` event, and is the result's `preselected`. It runs
|
|
211
|
-
* gather,
|
|
240
|
+
* before the first `turn` event, and is the result's `preselected`. It runs beside the hooks'
|
|
241
|
+
* `gather` rather than before it, since neither reads what the other returns, and the first
|
|
242
|
+
* request waits for both.
|
|
212
243
|
*/
|
|
213
244
|
preselect?: boolean | {
|
|
214
245
|
keywords?: boolean | KeywordPreselectOptions;
|
|
215
246
|
maxPerLoad?: number;
|
|
216
247
|
};
|
|
248
|
+
/**
|
|
249
|
+
* What the loop's own preselection is asked about, in place of the last user message's text.
|
|
250
|
+
* Read only under `preselect`.
|
|
251
|
+
*
|
|
252
|
+
* @remarks
|
|
253
|
+
* For a host that gathered its hooks itself and sent the question with `withContext` already on
|
|
254
|
+
* it: the loop cannot tell the context from the question, and a preselector shown both picks
|
|
255
|
+
* tools for whatever memory recalled. Blank asks nothing, as a blank question does.
|
|
256
|
+
*/
|
|
257
|
+
preselectQuery?: string;
|
|
217
258
|
/**
|
|
218
259
|
* Has the loop fold the transcript itself before a step, once `planCompaction` says the window
|
|
219
260
|
* is filling, with the summary written by `config.tasks.compaction` on its own settings. Off by
|
|
@@ -307,7 +348,8 @@ export interface AgentLoopOptions {
|
|
|
307
348
|
*/
|
|
308
349
|
onToolResult?: (result: ToolCallResult) => void;
|
|
309
350
|
/**
|
|
310
|
-
* 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.
|
|
311
353
|
*
|
|
312
354
|
* @remarks
|
|
313
355
|
* Only this question is touched — the last user message of `messages` — and the context is on
|
|
@@ -447,12 +489,13 @@ export interface AgentLoopResult {
|
|
|
447
489
|
* preselection picked it. Empty in eager mode, and where nothing was picked.
|
|
448
490
|
*/
|
|
449
491
|
preselected: string[];
|
|
450
|
-
/** The hooks' notes from before the first request. */
|
|
492
|
+
/** The hooks' notes from before the first request. Empty where `fire` lists no injecting event. */
|
|
451
493
|
notes: HookNote[];
|
|
452
494
|
/**
|
|
453
495
|
* The `<context>` blocks the hooks added to this question, as `withContext` takes them. Empty
|
|
454
|
-
* when no hook added any,
|
|
455
|
-
* 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`.
|
|
456
499
|
*/
|
|
457
500
|
context: string;
|
|
458
501
|
/**
|
|
@@ -467,8 +510,9 @@ export interface AgentLoopResult {
|
|
|
467
510
|
* The `afterTurn` hooks' notes, once they have run — failures only, as `notify` returns them.
|
|
468
511
|
* The loop does not wait for it, so the answer is never held for a hook; a host that stores the
|
|
469
512
|
* notes with the turn, or must not exit before the turn is remembered, awaits it. Each note
|
|
470
|
-
* reaches `onNote` as well. Already resolved and empty without `hooks`, and
|
|
471
|
-
* `
|
|
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.
|
|
472
516
|
*/
|
|
473
517
|
afterTurn: Promise<HookNote[]>;
|
|
474
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';
|
|
@@ -218,17 +218,18 @@ const userText = (message) => {
|
|
|
218
218
|
* name on its own endpoint where it does not.
|
|
219
219
|
*
|
|
220
220
|
* @param options - The run's options. Read for the config, the catalogue, the signal, what
|
|
221
|
-
* `preselect` passes through, and the last user message, whose text is
|
|
221
|
+
* `preselect` passes through, and `preselectQuery` or else the last user message, whose text is
|
|
222
|
+
* what is asked about.
|
|
222
223
|
* @param notice - Hears the preselector's notices, a failure among them.
|
|
223
|
-
* @returns The names picked. Empty where there is no user message
|
|
224
|
+
* @returns The names picked. Empty where the query is blank or there is no user message — nothing
|
|
224
225
|
* is asked — and where the preselection picked nothing or failed. An aborted request still rejects.
|
|
225
226
|
*
|
|
226
227
|
* @remarks
|
|
227
|
-
*
|
|
228
|
+
* With no preselector named the main model is asked, on the config's own endpoint.
|
|
228
229
|
*/
|
|
229
230
|
async function chooseTools(options, notice) {
|
|
230
231
|
const { config, catalog = [], signal } = options;
|
|
231
|
-
const prompt = userText(options.messages.findLast((message) => message.role === Role.User));
|
|
232
|
+
const prompt = options.preselectQuery ?? userText(options.messages.findLast((message) => message.role === Role.User));
|
|
232
233
|
if (!prompt.trim()) {
|
|
233
234
|
return [];
|
|
234
235
|
}
|
|
@@ -239,7 +240,7 @@ async function chooseTools(options, notice) {
|
|
|
239
240
|
};
|
|
240
241
|
const selector = config.tasks?.toolSelect;
|
|
241
242
|
if (!selector?.model) {
|
|
242
|
-
return preselect(config, config.toolSelectModel
|
|
243
|
+
return preselect(config, config.toolSelectModel || config.model, catalog, prompt, asked);
|
|
243
244
|
}
|
|
244
245
|
const call = taskCall(selector, asked, config);
|
|
245
246
|
return preselect(call.endpoint, call.model, catalog, prompt, call.options);
|
|
@@ -280,6 +281,7 @@ function ownSummariser(options, notice) {
|
|
|
280
281
|
*/
|
|
281
282
|
async function foldBeforeStep(transcript, reported, summarise, { options, charsPerToken, notice }) {
|
|
282
283
|
const { config, hooks } = options;
|
|
284
|
+
const told = hooks && firing(hooks).beforeCompact ? hooks : undefined;
|
|
283
285
|
const compactor = config.tasks?.compaction;
|
|
284
286
|
const plan = planCompaction(transcript, {
|
|
285
287
|
...(typeof options.compact === 'object' ? options.compact : {}),
|
|
@@ -292,13 +294,37 @@ async function foldBeforeStep(transcript, reported, summarise, { options, charsP
|
|
|
292
294
|
return transcript;
|
|
293
295
|
}
|
|
294
296
|
const folded = await compactTranscript(transcript, plan, summarise, {
|
|
295
|
-
...(
|
|
297
|
+
...(told ? { hooks: { run: told.run, context: told.context, onNote: told.onNote, offset: told.offset } } : {}),
|
|
296
298
|
});
|
|
297
299
|
if (folded !== transcript) {
|
|
298
300
|
notice(`compacted ${plan.toSummarise.length} messages into a summary`);
|
|
299
301
|
}
|
|
300
302
|
return folded;
|
|
301
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
|
+
}
|
|
302
328
|
/**
|
|
303
329
|
* Tells the `afterTurn` hooks what was answered, with the turn it closed where the question is
|
|
304
330
|
* still in the transcript.
|
|
@@ -494,6 +520,19 @@ async function runSteps(options, standing, pin) {
|
|
|
494
520
|
// The host's own fields win over the pin's, so an explicit `cache_prompt: false` stays false.
|
|
495
521
|
const extraBody = pin ? { ...pin.fields, ...config.extraBody } : config.extraBody;
|
|
496
522
|
const sending = { ...config, extraBody };
|
|
523
|
+
// Started before the preselection rather than after it: neither reads the other, and in a row
|
|
524
|
+
// they cost the first request two round trips where one will do. A failing runner is a note, but
|
|
525
|
+
// a host's `onNote` that throws rejects the gather, perhaps while the preselection is still out:
|
|
526
|
+
// handled at once so that is no unhandled rejection, and still thrown below, where it always was.
|
|
527
|
+
const fired = hooks && firing(hooks);
|
|
528
|
+
const gathering = hooks
|
|
529
|
+
? gather(hooks.run, fired?.before ?? [], hooks.context, {
|
|
530
|
+
signal,
|
|
531
|
+
onNote: hooks.onNote,
|
|
532
|
+
maxTokens: hooks.maxTokens,
|
|
533
|
+
})
|
|
534
|
+
: Promise.resolve({ context: '', notes: [] });
|
|
535
|
+
gathering.catch(() => undefined);
|
|
497
536
|
const { proxied, onDemand, loaded, preselected } = await discovery(options, notice);
|
|
498
537
|
const instructions = serverInstructions === false
|
|
499
538
|
? ''
|
|
@@ -531,13 +570,7 @@ async function runSteps(options, standing, pin) {
|
|
|
531
570
|
// hooks' context with it, which is right.
|
|
532
571
|
const question = messages.findLast((message) => message.role === Role.User);
|
|
533
572
|
const questionAt = () => (question ? messages.indexOf(question) : -1);
|
|
534
|
-
const gathered =
|
|
535
|
-
? await gather(hooks.run, hooks.events ?? [HookEvent.BeforeTurn], hooks.context, {
|
|
536
|
-
signal,
|
|
537
|
-
onNote: hooks.onNote,
|
|
538
|
-
maxTokens: hooks.maxTokens,
|
|
539
|
-
})
|
|
540
|
-
: { context: '', notes: [] };
|
|
573
|
+
const gathered = await gathering;
|
|
541
574
|
// Read once, so what the result hands back is what every step of this run said.
|
|
542
575
|
const preface = hooks?.preface ?? configureHooks().preface;
|
|
543
576
|
const loads = { toolsLoaded: 0, redundantLoads: 0, unknownToolNames: 0 };
|
|
@@ -722,7 +755,7 @@ async function runSteps(options, standing, pin) {
|
|
|
722
755
|
if (isAnswer || ended) {
|
|
723
756
|
// Not awaited: the answer is ready, and remembering it is not something to hold it for.
|
|
724
757
|
// Handed back instead, for a host that wants its notes.
|
|
725
|
-
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([]);
|
|
726
759
|
const metrics = runMetrics(recorded, { contextLength: config.contextLength });
|
|
727
760
|
let outcome = RunOutcome.Answered;
|
|
728
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",
|