@cubicecho/agent-core 4.4.0 → 4.6.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 +39 -5
- package/dist/context/compaction.d.ts +38 -7
- package/dist/context/compaction.js +142 -41
- package/dist/index.d.ts +2 -2
- package/dist/run/agent-loop.d.ts +23 -2
- package/dist/run/agent-loop.js +8 -3
- package/dist/run/events.d.ts +16 -2
- package/dist/run/events.js +10 -0
- package/dist/run/run-calls.d.ts +8 -3
- package/dist/run/run-calls.js +52 -7
- package/dist/wire/errors.d.ts +9 -1
- package/dist/wire/errors.js +4 -0
- package/dist/wire/tool-calls.d.ts +64 -0
- package/llms.txt +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -481,6 +481,34 @@ await runAgentLoop({
|
|
|
481
481
|
});
|
|
482
482
|
```
|
|
483
483
|
|
|
484
|
+
### What the calls made
|
|
485
|
+
|
|
486
|
+
A tool that wrote a document or saved a file can say so, and `dispatch` can pass that on: it
|
|
487
|
+
resolves to a `ToolAnswer`, `{ text, links }`, in place of the string. `text` is the whole of
|
|
488
|
+
what goes into the transcript, so the request, the prompt cache, pruning and compaction never see
|
|
489
|
+
the links. `links` are `ResourceLink`s, MCP's own shape declared structurally, so what
|
|
490
|
+
agent-mcp-pool's `call` answers with `links: true` is a dispatcher's answer as it stands.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
const { artifacts } = await runAgentLoop({
|
|
494
|
+
// ...
|
|
495
|
+
dispatch: ({ name, args }, signal) => pool.call(name, args, { signal, links: true }),
|
|
496
|
+
});
|
|
497
|
+
// [{ link: { uri, name, title?, ... }, callId, tool, step, count }]
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
The loop collects them into the run's `artifacts`: one per uri in the order first seen, each with
|
|
501
|
+
the call, tool and step that first linked to it and the number of calls that did. A link is what
|
|
502
|
+
one call answered with; an artifact is what the run made. `onToolResult` hears a call's `links`
|
|
503
|
+
beside its `content`, an `artifact` event goes out the first time each uri appears — `name` the
|
|
504
|
+
tool, `text` the title or else the uri, and `link` the link — and `runMetrics` counts those events
|
|
505
|
+
as `artifacts`. A deduplicated repeat shares the first call's answer and links but adds nothing to
|
|
506
|
+
the count, since nothing was made twice. A call that threw made nothing. A failed run carries what
|
|
507
|
+
its calls made on the error too, read off with `failedRun`.
|
|
508
|
+
|
|
509
|
+
A link meant for the model alone — one whose MCP `audience` leaves out `"user"` — is the
|
|
510
|
+
dispatcher's to drop, since that is MCP's rule and this package knows nothing of MCP.
|
|
511
|
+
|
|
484
512
|
Arguments go through `parseToolArguments`, which is lenient where the model's meaning is plain:
|
|
485
513
|
JSON held in a string is opened, and the almost-JSON local models write — single quotes, Python's
|
|
486
514
|
`True` and `None`, bare keys, a trailing comma — is repaired, without touching what is inside a
|
|
@@ -815,9 +843,15 @@ once `used` (the last turn's prompt tokens, or the estimate) is past three quart
|
|
|
815
843
|
The kept tail fills at most 35% of it and starts on a user message, since a transcript resuming
|
|
816
844
|
mid-exchange is one servers refuse; leading system prompts are never folded, and an earlier
|
|
817
845
|
summary is continued rather than summarised. `compactTranscript` writes the summary and tells
|
|
818
|
-
`beforeCompact` hooks what is going while it does.
|
|
819
|
-
|
|
820
|
-
|
|
846
|
+
`beforeCompact` hooks what is going while it does.
|
|
847
|
+
|
|
848
|
+
A run that is one question and a long tool loop has no user message to cut at, so there the plan
|
|
849
|
+
cuts inside the loop instead: the question is kept whole (the plan's `question`), the calls and
|
|
850
|
+
results after it are folded up to an assistant message, and the request becomes the summary, the
|
|
851
|
+
question, then the rest of the loop — every tool result still after its call. A transcript with a
|
|
852
|
+
user message to cut at is planned as before. `runAgentLoop({ compact: true })` folds such a run on
|
|
853
|
+
its own; a host that numbers messages across steps passes `hooks.offset`, the stored index of the
|
|
854
|
+
run's first message, so the ids `beforeCompact` is told match its own.
|
|
821
855
|
|
|
822
856
|
```ts
|
|
823
857
|
beforeStep: async (messages, step) => {
|
|
@@ -950,8 +984,8 @@ leading `system` messages, and reads an earlier summary back out of a `SUMMARY_L
|
|
|
950
984
|
them. A host whose system prompt is a separate argument and whose summary is a column has neither
|
|
951
985
|
in the array, and knows both exactly. Given them, nothing is scanned.
|
|
952
986
|
|
|
953
|
-
`applyCompaction(messages, fold)` is the way back — the summary as a `system` message, then
|
|
954
|
-
everything from `through` — and it writes the same `SUMMARY_LEAD` `planCompaction` looks for, so
|
|
987
|
+
`applyCompaction(messages, fold)` is the way back — the summary as a `system` message, then the
|
|
988
|
+
fold's `question` if it was made inside one, then everything from `through` — and it writes the same `SUMMARY_LEAD` `planCompaction` looks for, so
|
|
955
989
|
the next fold continues those notes rather than summarising them a second time. No fold yet hands
|
|
956
990
|
back the messages themselves.
|
|
957
991
|
|
|
@@ -125,9 +125,17 @@ export interface CompactionOptions {
|
|
|
125
125
|
export interface CompactionPlan {
|
|
126
126
|
/** The first message folded away. Everything before it is a system prompt and stays. */
|
|
127
127
|
from: number;
|
|
128
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* The first message kept whole: a user message, or an assistant message on a fold made inside
|
|
130
|
+
* one question's tool loop (see `question`).
|
|
131
|
+
*/
|
|
129
132
|
cut: number;
|
|
130
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* The user message kept whole between the summary and `cut`, on a fold made inside its tool
|
|
135
|
+
* loop. Absent on a fold that cuts at a user message.
|
|
136
|
+
*/
|
|
137
|
+
question?: number;
|
|
138
|
+
/** The messages from `from` to `cut`, `question` left out: the ones the summary replaces. */
|
|
131
139
|
toSummarise: Message[];
|
|
132
140
|
/** The summary an earlier compaction left, which this one continues. */
|
|
133
141
|
previous?: string;
|
|
@@ -155,6 +163,13 @@ export interface CompactionPlan {
|
|
|
155
163
|
* than summarised as if it were conversation. No plan comes back when the window is not full
|
|
156
164
|
* enough, or when the only legal cut folds too little to pay for the summary.
|
|
157
165
|
*
|
|
166
|
+
* A transcript that is one question and a long tool loop has no user message to cut at, so where
|
|
167
|
+
* no user cut folds anything the plan cuts inside the last question's loop instead: the question
|
|
168
|
+
* is kept whole as `question`, the assistant and tool messages after it are folded up to an
|
|
169
|
+
* assistant message, and the tail resumes there — every tool result in it still follows its call.
|
|
170
|
+
* `applyCompaction` sends the question after the summary. A transcript with a user cut is planned
|
|
171
|
+
* exactly as it was.
|
|
172
|
+
*
|
|
158
173
|
* Both of those are recovered by reading the transcript, which is what a host whose array holds
|
|
159
174
|
* everything it sends has to do. A host that keeps its fold as a record beside an append-only
|
|
160
175
|
* transcript — its system prompt a separate argument, no summary message in the array at all —
|
|
@@ -206,6 +221,11 @@ export interface CompactionRecord {
|
|
|
206
221
|
summary: string;
|
|
207
222
|
/** Index into the transcript the plan was made for: the first message still sent whole. */
|
|
208
223
|
through: number;
|
|
224
|
+
/**
|
|
225
|
+
* The user message sent whole after the summary and ahead of `through`, on a fold made inside
|
|
226
|
+
* that question's tool loop. Absent on a fold that cut at a user message.
|
|
227
|
+
*/
|
|
228
|
+
question?: number;
|
|
209
229
|
/** ISO 8601, so the chat can show where history was folded and how stale the notes are. */
|
|
210
230
|
at: string;
|
|
211
231
|
}
|
|
@@ -213,7 +233,8 @@ export interface CompactionRecord {
|
|
|
213
233
|
export interface CompactionRunOptions {
|
|
214
234
|
/**
|
|
215
235
|
* Hooks to tell. `context` is extended with `compacting` and `range`, whose indexes are the
|
|
216
|
-
* plan's — and so the host's own, for a plan made over a stored transcript. `
|
|
236
|
+
* plan's — and so the host's own, for a plan made over a stored transcript. `compacting` leaves
|
|
237
|
+
* out a `question` the fold keeps. `honourVeto` waits
|
|
217
238
|
* for the hooks and lets one stop the compaction; without it they add no latency.
|
|
218
239
|
*/
|
|
219
240
|
hooks?: {
|
|
@@ -221,6 +242,14 @@ export interface CompactionRunOptions {
|
|
|
221
242
|
context: HookContext;
|
|
222
243
|
onNote?: OnNote;
|
|
223
244
|
honourVeto?: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* What the transcript's first message is numbered as in the `compacting` uuids: the host's
|
|
247
|
+
* stored index of `messages[0]`, for a host that numbers messages across steps or requests.
|
|
248
|
+
* See `turnMessages`.
|
|
249
|
+
*
|
|
250
|
+
* @defaultValue `0`
|
|
251
|
+
*/
|
|
252
|
+
offset?: number;
|
|
224
253
|
};
|
|
225
254
|
/**
|
|
226
255
|
* The window is already exceeded — the caller caught a `ContextOverflow`, or is compacting to
|
|
@@ -256,7 +285,8 @@ export declare function runCompaction(messages: Message[], plan: CompactionPlan,
|
|
|
256
285
|
* Absent, the leading `system` messages are found by scanning, and zero of them is the ordinary
|
|
257
286
|
* case for a host whose system prompt is a separate argument.
|
|
258
287
|
* @returns `messages` itself when there is no record or its summary is blank, otherwise a new
|
|
259
|
-
* array: the kept head, one summary message,
|
|
288
|
+
* array: the kept head, one summary message, the record's `question` when it has one and the head
|
|
289
|
+
* does not already hold it, then everything from the record's `through` on.
|
|
260
290
|
*
|
|
261
291
|
* @remarks
|
|
262
292
|
* The inverse of storing a `CompactionRecord`, and the shape `planCompaction` expects to meet
|
|
@@ -264,7 +294,7 @@ export declare function runCompaction(messages: Message[], plan: CompactionPlan,
|
|
|
264
294
|
* continues these notes rather than summarising them a second time. Any earlier summary message in
|
|
265
295
|
* the kept head is dropped, since the record's already contains it.
|
|
266
296
|
*/
|
|
267
|
-
export declare function applyCompaction(messages: Message[], record?: Pick<CompactionRecord, 'summary' | 'through'>, { from }?: {
|
|
297
|
+
export declare function applyCompaction(messages: Message[], record?: Pick<CompactionRecord, 'summary' | 'through' | 'question'>, { from }?: {
|
|
268
298
|
from?: number;
|
|
269
299
|
}): Message[];
|
|
270
300
|
/**
|
|
@@ -278,7 +308,8 @@ export declare function applyCompaction(messages: Message[], record?: Pick<Compa
|
|
|
278
308
|
* prompts, when the host keeps them in the array. Zero is the stored-fold case,
|
|
279
309
|
* where the summary is the request's first message.
|
|
280
310
|
* @returns The index in the request. One before the record's `through` gives `head`, where the
|
|
281
|
-
* summary sits — an index among the kept head messages themselves included
|
|
311
|
+
* summary sits — an index among the kept head messages themselves included — except the record's
|
|
312
|
+
* `question`, which is sent whole right after the summary.
|
|
282
313
|
*
|
|
283
314
|
* @remarks
|
|
284
315
|
* A transcript that stays append-only and a request that does not are two numberings of the same
|
|
@@ -286,7 +317,7 @@ export declare function applyCompaction(messages: Message[], record?: Pick<Compa
|
|
|
286
317
|
* hook — has to say which it is in. An index inside the folded stretch answers with the summary
|
|
287
318
|
* message that now stands for it.
|
|
288
319
|
*/
|
|
289
|
-
export declare const requestIndex: (index: number, record?: Pick<CompactionRecord, 'through'>, head?: number) => number;
|
|
320
|
+
export declare const requestIndex: (index: number, record?: Pick<CompactionRecord, 'through' | 'question'>, head?: number) => number;
|
|
290
321
|
/**
|
|
291
322
|
* The transcript with the plan's stretch replaced by one system message holding its summary.
|
|
292
323
|
*
|
|
@@ -142,6 +142,56 @@ export function pruneToolResults(messages, { keepLast = 5, maxChars = 256 } = {}
|
|
|
142
142
|
* @returns `index` where it lies from zero to `most`, otherwise the nearer of the two.
|
|
143
143
|
*/
|
|
144
144
|
const clamp = (index, most) => Math.min(Math.max(index, 0), most);
|
|
145
|
+
/**
|
|
146
|
+
* Where to cut, by `target` when there is one and by `keepRatio` otherwise, among the indexes
|
|
147
|
+
* `isCut` allows.
|
|
148
|
+
*
|
|
149
|
+
* @param messages - The transcript. Not written to.
|
|
150
|
+
* @param search - The weights, the bounds and which indexes are legal cuts. See `CutSearch`.
|
|
151
|
+
* @returns `cut`, the length of `messages` when no legal cut was found, and on a plan made to a
|
|
152
|
+
* `target` the cost it leaves as `after`.
|
|
153
|
+
*/
|
|
154
|
+
const findCut = (messages, { from, cost, limit, keepRatio, target, summaryTokens, estimate, isCut, kept }) => {
|
|
155
|
+
let cut = messages.length;
|
|
156
|
+
let after;
|
|
157
|
+
if (target !== undefined && target > 0) {
|
|
158
|
+
// Forward from the head, so the first cut that reaches the target is the one that folds least.
|
|
159
|
+
let folded = 0;
|
|
160
|
+
for (let at = from; at < messages.length; at++) {
|
|
161
|
+
if (isCut(at)) {
|
|
162
|
+
cut = at;
|
|
163
|
+
after = cost - folded + summaryTokens;
|
|
164
|
+
if (after <= limit * target) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (at !== kept) {
|
|
169
|
+
folded += estimate(messages[at]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { cut, after };
|
|
173
|
+
}
|
|
174
|
+
const budget = limit * keepRatio;
|
|
175
|
+
let tail = 0;
|
|
176
|
+
for (let at = messages.length - 1; at > from; at--) {
|
|
177
|
+
tail += estimate(messages[at]);
|
|
178
|
+
if (tail > budget) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
cut = at;
|
|
182
|
+
}
|
|
183
|
+
while (cut < messages.length && isCut(cut) === false) {
|
|
184
|
+
cut++;
|
|
185
|
+
}
|
|
186
|
+
return { cut };
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* The index of the last user message in a transcript.
|
|
190
|
+
*
|
|
191
|
+
* @param messages - The transcript.
|
|
192
|
+
* @returns The index, or `-1` when there is none.
|
|
193
|
+
*/
|
|
194
|
+
const lastUser = (messages) => messages.findLastIndex((message) => message.role === Role.User);
|
|
145
195
|
/**
|
|
146
196
|
* Where to fold a transcript that has grown into its window, or `undefined` when it should not be.
|
|
147
197
|
*
|
|
@@ -159,6 +209,13 @@ const clamp = (index, most) => Math.min(Math.max(index, 0), most);
|
|
|
159
209
|
* than summarised as if it were conversation. No plan comes back when the window is not full
|
|
160
210
|
* enough, or when the only legal cut folds too little to pay for the summary.
|
|
161
211
|
*
|
|
212
|
+
* A transcript that is one question and a long tool loop has no user message to cut at, so where
|
|
213
|
+
* no user cut folds anything the plan cuts inside the last question's loop instead: the question
|
|
214
|
+
* is kept whole as `question`, the assistant and tool messages after it are folded up to an
|
|
215
|
+
* assistant message, and the tail resumes there — every tool result in it still follows its call.
|
|
216
|
+
* `applyCompaction` sends the question after the summary. A transcript with a user cut is planned
|
|
217
|
+
* exactly as it was.
|
|
218
|
+
*
|
|
162
219
|
* Both of those are recovered by reading the transcript, which is what a host whose array holds
|
|
163
220
|
* everything it sends has to do. A host that keeps its fold as a record beside an append-only
|
|
164
221
|
* transcript — its system prompt a separate argument, no summary message in the array at all —
|
|
@@ -184,47 +241,53 @@ export function planCompaction(messages, { limit, used, compactAt = COMPACT_AT,
|
|
|
184
241
|
const head = systemHead(messages);
|
|
185
242
|
const from = clamp(givenFrom ?? head.from, messages.length);
|
|
186
243
|
const previous = givenPrevious ?? head.previous;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
for (let at = from; at < messages.length; at++) {
|
|
193
|
-
if (messages[at].role === Role.User) {
|
|
194
|
-
cut = at;
|
|
195
|
-
after = cost - folded + summaryTokens;
|
|
196
|
-
if (after <= limit * target) {
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
folded += estimate(messages[at]);
|
|
201
|
-
}
|
|
244
|
+
const search = { from, cost, limit, keepRatio, target, summaryTokens, estimate };
|
|
245
|
+
const atUser = findCut(messages, { ...search, isCut: (at) => messages[at].role === Role.User });
|
|
246
|
+
const foldsAtUser = atUser.cut < messages.length && atUser.cut - from >= FEWEST_FOLDED;
|
|
247
|
+
if (foldsAtUser) {
|
|
248
|
+
return planFor(messages, from, atUser.cut, previous, atUser.after);
|
|
202
249
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
kept += estimate(messages[at]);
|
|
208
|
-
if (kept > budget) {
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
cut = at;
|
|
212
|
-
}
|
|
213
|
-
while (cut < messages.length && messages[cut].role !== Role.User) {
|
|
214
|
-
cut++;
|
|
215
|
-
}
|
|
250
|
+
// A cut that folds too little but already reaches the target means there is nothing to fold.
|
|
251
|
+
const reachesTarget = target !== undefined && atUser.after !== undefined && atUser.after <= limit * target;
|
|
252
|
+
if (reachesTarget) {
|
|
253
|
+
return undefined;
|
|
216
254
|
}
|
|
217
|
-
|
|
255
|
+
// No user message to cut at: fold inside the last question's tool loop, keeping the question.
|
|
256
|
+
const question = lastUser(messages);
|
|
257
|
+
if (question < 0) {
|
|
218
258
|
return undefined;
|
|
219
259
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
260
|
+
const inLoop = findCut(messages, {
|
|
261
|
+
...search,
|
|
262
|
+
isCut: (at) => at > question && messages[at].role === Role.Assistant,
|
|
263
|
+
kept: question,
|
|
264
|
+
});
|
|
265
|
+
if (inLoop.cut >= messages.length) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
const folded = planFor(messages, from, inLoop.cut, previous, inLoop.after, question);
|
|
269
|
+
return folded.toSummarise.length >= FEWEST_FOLDED ? folded : undefined;
|
|
227
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* A plan for a cut `planCompaction` has chosen.
|
|
273
|
+
*
|
|
274
|
+
* @param messages - The transcript. Not written to.
|
|
275
|
+
* @param from - The first message folded.
|
|
276
|
+
* @param cut - The first message kept whole.
|
|
277
|
+
* @param [previous] - The summary this one continues.
|
|
278
|
+
* @param [after] - What the request is expected to cost once folded, on a plan made to a target.
|
|
279
|
+
* @param [question] - The user message kept whole ahead of `cut`, on a fold inside its tool loop.
|
|
280
|
+
* Left out of `toSummarise` where it lies between `from` and `cut`.
|
|
281
|
+
* @returns The plan, `toSummarise` a new array of the same message objects.
|
|
282
|
+
*/
|
|
283
|
+
const planFor = (messages, from, cut, previous, after, question) => ({
|
|
284
|
+
from,
|
|
285
|
+
cut,
|
|
286
|
+
...(question === undefined ? {} : { question }),
|
|
287
|
+
toSummarise: messages.slice(from, cut).filter((_, at) => at + from !== question),
|
|
288
|
+
...(previous ? { previous } : {}),
|
|
289
|
+
...(after === undefined ? {} : { after: Math.round(after) }),
|
|
290
|
+
});
|
|
228
291
|
/**
|
|
229
292
|
* What the summariser is handed for a plan: the earlier summary if there was one, then each
|
|
230
293
|
* message as its role and at most 4000 characters of its text.
|
|
@@ -256,6 +319,22 @@ export function summaryInput(plan) {
|
|
|
256
319
|
* resolves with an empty string where the model wrote nothing.
|
|
257
320
|
*/
|
|
258
321
|
export const summariser = (config, model, { system = SUMMARY_PROMPT, maxTokens = SUMMARY_TOKENS, ...options } = {}) => (text) => ask(config, model, system, text, { maxTokens, ...options });
|
|
322
|
+
/**
|
|
323
|
+
* What `beforeCompact` hooks are told is going: the plan's stretch as `turnMessages` reads it,
|
|
324
|
+
* less the question the fold keeps.
|
|
325
|
+
*
|
|
326
|
+
* @param sessionId - Prefixes every uuid.
|
|
327
|
+
* @param messages - The transcript the plan was made for.
|
|
328
|
+
* @param plan - What `planCompaction` returned for it.
|
|
329
|
+
* @param [offset] - What `messages[0]` is numbered as in the uuids.
|
|
330
|
+
* @returns The user and assistant text from `plan.from` to `plan.cut`, `plan.question` left out.
|
|
331
|
+
*/
|
|
332
|
+
const foldedMessages = (sessionId, messages, plan, offset = 0) => {
|
|
333
|
+
const { from, cut, question } = plan;
|
|
334
|
+
const read = (start, end) => turnMessages(sessionId, messages, start, end, { offset });
|
|
335
|
+
const splitsAtQuestion = question !== undefined && question >= from && question < cut;
|
|
336
|
+
return splitsAtQuestion ? [...read(from, question), ...read(question + 1, cut)] : read(from, cut);
|
|
337
|
+
};
|
|
259
338
|
/**
|
|
260
339
|
* The hooks and the summariser for a plan, as a record to store rather than a transcript to send.
|
|
261
340
|
*
|
|
@@ -275,7 +354,7 @@ export const summariser = (config, model, { system = SUMMARY_PROMPT, maxTokens =
|
|
|
275
354
|
export async function runCompaction(messages, plan, summarise, { hooks, forced = false } = {}) {
|
|
276
355
|
const context = hooks && {
|
|
277
356
|
...hooks.context,
|
|
278
|
-
compacting:
|
|
357
|
+
compacting: foldedMessages(hooks.context.session.id, messages, plan, hooks.offset),
|
|
279
358
|
range: { from: plan.from, through: plan.cut },
|
|
280
359
|
};
|
|
281
360
|
let summary;
|
|
@@ -295,7 +374,12 @@ export async function runCompaction(messages, plan, summarise, { hooks, forced =
|
|
|
295
374
|
if (!summary.trim()) {
|
|
296
375
|
return undefined;
|
|
297
376
|
}
|
|
298
|
-
return {
|
|
377
|
+
return {
|
|
378
|
+
summary: summary.trim(),
|
|
379
|
+
through: plan.cut,
|
|
380
|
+
...(plan.question === undefined ? {} : { question: plan.question }),
|
|
381
|
+
at: new Date().toISOString(),
|
|
382
|
+
};
|
|
299
383
|
}
|
|
300
384
|
/**
|
|
301
385
|
* The transcript as the server should see it: the folded head replaced by its summary.
|
|
@@ -308,7 +392,8 @@ export async function runCompaction(messages, plan, summarise, { hooks, forced =
|
|
|
308
392
|
* Absent, the leading `system` messages are found by scanning, and zero of them is the ordinary
|
|
309
393
|
* case for a host whose system prompt is a separate argument.
|
|
310
394
|
* @returns `messages` itself when there is no record or its summary is blank, otherwise a new
|
|
311
|
-
* array: the kept head, one summary message,
|
|
395
|
+
* array: the kept head, one summary message, the record's `question` when it has one and the head
|
|
396
|
+
* does not already hold it, then everything from the record's `through` on.
|
|
312
397
|
*
|
|
313
398
|
* @remarks
|
|
314
399
|
* The inverse of storing a `CompactionRecord`, and the shape `planCompaction` expects to meet
|
|
@@ -321,12 +406,22 @@ export function applyCompaction(messages, record, { from } = {}) {
|
|
|
321
406
|
return messages;
|
|
322
407
|
}
|
|
323
408
|
const head = clamp(from ?? systemHead(messages).from, record.through);
|
|
409
|
+
const question = keptQuestion(record, head);
|
|
324
410
|
return [
|
|
325
411
|
...messages.slice(0, head).filter((message) => !isSummary(message)),
|
|
326
412
|
summaryMessage(record.summary),
|
|
413
|
+
...(question === undefined ? [] : [messages[question]]),
|
|
327
414
|
...messages.slice(record.through),
|
|
328
415
|
];
|
|
329
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* The question a fold sends after its summary, unless the kept head already holds it.
|
|
419
|
+
*
|
|
420
|
+
* @param record - The fold.
|
|
421
|
+
* @param head - How many messages are kept ahead of the summary.
|
|
422
|
+
* @returns `record.question` where it lies from `head` up to `through`, otherwise `undefined`.
|
|
423
|
+
*/
|
|
424
|
+
const keptQuestion = ({ question, through }, head) => question !== undefined && question >= head && question < through ? question : undefined;
|
|
330
425
|
/**
|
|
331
426
|
* Where a stored index sits in the request `applyCompaction` builds, once a fold has shifted
|
|
332
427
|
* everything after it.
|
|
@@ -338,7 +433,8 @@ export function applyCompaction(messages, record, { from } = {}) {
|
|
|
338
433
|
* prompts, when the host keeps them in the array. Zero is the stored-fold case,
|
|
339
434
|
* where the summary is the request's first message.
|
|
340
435
|
* @returns The index in the request. One before the record's `through` gives `head`, where the
|
|
341
|
-
* summary sits — an index among the kept head messages themselves included
|
|
436
|
+
* summary sits — an index among the kept head messages themselves included — except the record's
|
|
437
|
+
* `question`, which is sent whole right after the summary.
|
|
342
438
|
*
|
|
343
439
|
* @remarks
|
|
344
440
|
* A transcript that stays append-only and a request that does not are two numberings of the same
|
|
@@ -350,7 +446,12 @@ export const requestIndex = (index, record, head = 0) => {
|
|
|
350
446
|
if (!record) {
|
|
351
447
|
return index;
|
|
352
448
|
}
|
|
353
|
-
|
|
449
|
+
const question = keptQuestion(record, head);
|
|
450
|
+
if (index === question) {
|
|
451
|
+
return head + 1;
|
|
452
|
+
}
|
|
453
|
+
const afterSummary = question === undefined ? 1 : 2;
|
|
454
|
+
return index < record.through ? head : index - record.through + head + afterSummary;
|
|
354
455
|
};
|
|
355
456
|
/**
|
|
356
457
|
* The transcript with the plan's stretch replaced by one system message holding its summary.
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_C
|
|
|
21
21
|
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopRequest, type AgentLoopResult, PRESELECT_APPEND, PRESELECT_EXCLUSIVE, PRESELECTED_NOTICE, runAgentLoop, type StepWindow, } from './run/agent-loop.ts';
|
|
22
22
|
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, RunEventKind, type RunMetrics, type RunMetricsOptions, RunOutcome, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from './run/events.ts';
|
|
23
23
|
export { buildBody } from './run/request-body.ts';
|
|
24
|
-
export { preview } from './run/run-calls.ts';
|
|
24
|
+
export { preview, type ToolDispatch } from './run/run-calls.ts';
|
|
25
25
|
export { resetAll } from './runtime/reset.ts';
|
|
26
26
|
export { createRuntime, defaultRuntime, type Runtime, type RuntimeOptions } from './runtime/runtime.ts';
|
|
27
27
|
export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from './runtime/snapshot.ts';
|
|
@@ -37,5 +37,5 @@ export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow,
|
|
|
37
37
|
export { CacheBreakReason, type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from './wire/stream.ts';
|
|
38
38
|
export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, SPLIT_OUTPUT, SPLIT_REASONING, type Split, stripThinking, THINK_FENCE, } from './wire/thinking.ts';
|
|
39
39
|
export { type ContextBreakdown, type ContextBreakdownOptions, compact, contextChars, contextTokens, estimateTokens, messageTokens, requestChars, requestTokens, type TokenEstimateOptions, } from './wire/tokens.ts';
|
|
40
|
-
export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, recoverToolCalls, ToolArgumentsError, type ToolCall, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult, } from './wire/tool-calls.ts';
|
|
40
|
+
export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, type ResourceLink, type RunArtifact, recoverToolCalls, type ToolAnswer, ToolArgumentsError, type ToolCall, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult, } from './wire/tool-calls.ts';
|
|
41
41
|
export { FinishReason, FUNCTION_TOOL, JSON_SCHEMA_FORMAT, PartType, Role, SchemaType } from './wire/wire.ts';
|
package/dist/run/agent-loop.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type KeywordPreselectOptions } from '../tools/preselect.ts';
|
|
|
9
9
|
import { type ToolOrder } from '../tools/tool-loading.ts';
|
|
10
10
|
import { type SideTask } from '../turn/side-task.ts';
|
|
11
11
|
import { type Turn, type TurnUsage } from '../wire/stream.ts';
|
|
12
|
-
import { type ToolCallOutcome, type ToolCallRequest, type ToolCallResult } from '../wire/tool-calls.ts';
|
|
12
|
+
import { type RunArtifact, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult } from '../wire/tool-calls.ts';
|
|
13
13
|
import { type RunEventInput, type RunMetrics } from './events.ts';
|
|
14
14
|
import { type ToolDispatch } from './run-calls.ts';
|
|
15
15
|
/**
|
|
@@ -70,6 +70,14 @@ export interface AgentLoopHooks {
|
|
|
70
70
|
preface?: string;
|
|
71
71
|
/** Hears each note, from before the request and from `afterTurn`. */
|
|
72
72
|
onNote?: OnNote;
|
|
73
|
+
/**
|
|
74
|
+
* What the run's first message is numbered as in the uuids hooks are told — the host's stored
|
|
75
|
+
* index of `messages[0]`, for a host that numbers messages across steps or runs. Read for
|
|
76
|
+
* `beforeCompact`'s `compacting` and `afterTurn`'s `turn`. See `turnMessages`.
|
|
77
|
+
*
|
|
78
|
+
* @defaultValue `0`
|
|
79
|
+
*/
|
|
80
|
+
offset?: number;
|
|
73
81
|
}
|
|
74
82
|
/** How full the window is as a step is about to be sent, as `beforeStep` is told it. */
|
|
75
83
|
export interface StepWindow {
|
|
@@ -212,7 +220,14 @@ export interface AgentLoopOptions {
|
|
|
212
220
|
* mode, where a tool is loaded only while its definition is in the history.
|
|
213
221
|
*/
|
|
214
222
|
loaded?: Iterable<string>;
|
|
215
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* Runs one tool call and returns what the model reads. What it throws, the model reads too.
|
|
225
|
+
*
|
|
226
|
+
* @remarks
|
|
227
|
+
* A `ToolAnswer` in place of the string adds what the call produced: its `text` is the whole of
|
|
228
|
+
* what goes into the transcript, and its `links` become the result's `artifacts`, an `artifact`
|
|
229
|
+
* event the first time each uri is seen, and `links` on what `onToolResult` is handed.
|
|
230
|
+
*/
|
|
216
231
|
dispatch: ToolDispatch;
|
|
217
232
|
/**
|
|
218
233
|
* Runs a step's calls together rather than one after another. Results still go into the
|
|
@@ -346,6 +361,12 @@ export interface AgentLoopResult {
|
|
|
346
361
|
* proxied preselection's among them, first.
|
|
347
362
|
*/
|
|
348
363
|
toolCalls: ToolCallOutcome[];
|
|
364
|
+
/**
|
|
365
|
+
* What the run's calls linked to, one per uri in the order first seen, each with the call and
|
|
366
|
+
* step that first linked to it and how many calls did. Empty where `dispatch` answered with
|
|
367
|
+
* strings only.
|
|
368
|
+
*/
|
|
369
|
+
artifacts: RunArtifact[];
|
|
349
370
|
/**
|
|
350
371
|
* What is loaded at the end, for `carryOver`. Empty in eager mode, and in proxied mode: a
|
|
351
372
|
* definition loaded there is already in the history, and one a compaction folded away has to be
|
package/dist/run/agent-loop.js
CHANGED
|
@@ -116,6 +116,7 @@ export async function runAgentLoop(options) {
|
|
|
116
116
|
messages: [...options.messages],
|
|
117
117
|
usage: noUsage(),
|
|
118
118
|
toolCalls: [],
|
|
119
|
+
artifacts: [],
|
|
119
120
|
loaded: [],
|
|
120
121
|
used: [],
|
|
121
122
|
preselected: [],
|
|
@@ -235,10 +236,10 @@ async function foldBeforeStep(transcript, reported, summarise, { options, charsP
|
|
|
235
236
|
return transcript;
|
|
236
237
|
}
|
|
237
238
|
const folded = await compactTranscript(transcript, plan, summarise, {
|
|
238
|
-
...(hooks ? { hooks: { run: hooks.run, context: hooks.context, onNote: hooks.onNote } } : {}),
|
|
239
|
+
...(hooks ? { hooks: { run: hooks.run, context: hooks.context, onNote: hooks.onNote, offset: hooks.offset } } : {}),
|
|
239
240
|
});
|
|
240
241
|
if (folded !== transcript) {
|
|
241
|
-
notice(`compacted ${plan.
|
|
242
|
+
notice(`compacted ${plan.toSummarise.length} messages into a summary`);
|
|
242
243
|
}
|
|
243
244
|
return folded;
|
|
244
245
|
}
|
|
@@ -260,7 +261,7 @@ const tellAfterTurn = (hooks, messages, at, reply) => notify(hooks.run, HookEven
|
|
|
260
261
|
? {
|
|
261
262
|
turn: {
|
|
262
263
|
index: turnIndex(messages, at),
|
|
263
|
-
messages: turnMessages(hooks.context.session.id, messages, at),
|
|
264
|
+
messages: turnMessages(hooks.context.session.id, messages, at, undefined, { offset: hooks.offset }),
|
|
264
265
|
},
|
|
265
266
|
}
|
|
266
267
|
: {}),
|
|
@@ -450,10 +451,12 @@ async function runSteps(options, standing) {
|
|
|
450
451
|
let messages = [...options.messages];
|
|
451
452
|
let usage = noUsage();
|
|
452
453
|
const toolCalls = [];
|
|
454
|
+
const artifacts = new Map();
|
|
453
455
|
standing.read = () => ({
|
|
454
456
|
messages,
|
|
455
457
|
usage,
|
|
456
458
|
toolCalls,
|
|
459
|
+
artifacts: [...artifacts.values()],
|
|
457
460
|
// As the result would have said it: proxied, nothing is loaded that the history does not hold.
|
|
458
461
|
loaded: proxied ? [] : [...loaded],
|
|
459
462
|
used: [...used],
|
|
@@ -498,6 +501,7 @@ async function runSteps(options, standing) {
|
|
|
498
501
|
loads,
|
|
499
502
|
used,
|
|
500
503
|
toolCalls,
|
|
504
|
+
artifacts,
|
|
501
505
|
dedupable,
|
|
502
506
|
dispatch,
|
|
503
507
|
parallel,
|
|
@@ -645,6 +649,7 @@ async function runSteps(options, standing) {
|
|
|
645
649
|
messages,
|
|
646
650
|
usage,
|
|
647
651
|
toolCalls,
|
|
652
|
+
artifacts: [...artifacts.values()],
|
|
648
653
|
loaded: proxied ? [] : [...loaded],
|
|
649
654
|
used: [...used],
|
|
650
655
|
preselected: [...preselected],
|
package/dist/run/events.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TokenLedger } from '../context/ledger.ts';
|
|
2
2
|
import type { TurnUsage } from '../wire/stream.ts';
|
|
3
3
|
import { CacheBreakReason } from '../wire/stream.ts';
|
|
4
|
+
import type { ResourceLink } from '../wire/tool-calls.ts';
|
|
4
5
|
/**
|
|
5
6
|
* What a run is doing, while it is doing it.
|
|
6
7
|
*
|
|
@@ -89,6 +90,12 @@ export declare const RunEventKind: {
|
|
|
89
90
|
readonly ToolResult: 'tool-result';
|
|
90
91
|
/** Something the runner did that is not the model's doing — a preselection, a retry. */
|
|
91
92
|
readonly Notice: 'notice';
|
|
93
|
+
/**
|
|
94
|
+
* A tool call's answer linked to something the run had not produced before. `id` is the call's,
|
|
95
|
+
* `name` the tool, `text` the link's title or else its uri, and `link` the link itself. Once
|
|
96
|
+
* per uri per run: a later call that links the same uri again only adds to its count.
|
|
97
|
+
*/
|
|
98
|
+
readonly Artifact: 'artifact';
|
|
92
99
|
/**
|
|
93
100
|
* What the run has cost so far, at the end of every turn, and what that one turn did. Sent
|
|
94
101
|
* whether or not the endpoint reported tokens, since the timings and the cache comparison are
|
|
@@ -136,8 +143,8 @@ export interface TurnReport extends TurnUsage {
|
|
|
136
143
|
* One thing that happened in a run, as a watcher receives it.
|
|
137
144
|
*
|
|
138
145
|
* @remarks
|
|
139
|
-
* Every field but `id` is always present — the empty ones are `""` or `null` rather
|
|
140
|
-
* missing — so a client reads it without guarding each key.
|
|
146
|
+
* Every field but `id` and `link` is always present — the empty ones are `""` or `null` rather
|
|
147
|
+
* than missing — so a client reads it without guarding each key.
|
|
141
148
|
*/
|
|
142
149
|
export interface RunEvent {
|
|
143
150
|
/** The run it belongs to. `emit` fills this in; a caller does not pass it. */
|
|
@@ -175,6 +182,11 @@ export interface RunEvent {
|
|
|
175
182
|
ok: boolean | null;
|
|
176
183
|
/** Running totals on `usage`, otherwise null. */
|
|
177
184
|
usage: RunUsage | null;
|
|
185
|
+
/**
|
|
186
|
+
* What the call produced, on `artifact` and on no other kind. Optional for the reason `id` is:
|
|
187
|
+
* an event built before it existed still compiles.
|
|
188
|
+
*/
|
|
189
|
+
link?: ResourceLink;
|
|
178
190
|
}
|
|
179
191
|
/**
|
|
180
192
|
* What `emit` is given: the run and the sequence are the bus's to assign.
|
|
@@ -300,6 +312,8 @@ export interface RunMetrics {
|
|
|
300
312
|
toolErrors: Record<string, number>;
|
|
301
313
|
/** `load_tools` calls. */
|
|
302
314
|
loadCalls: number;
|
|
315
|
+
/** `artifact` events: the distinct things the run's tool calls linked to. */
|
|
316
|
+
artifacts: number;
|
|
303
317
|
/**
|
|
304
318
|
* Tools `load_tools` loaded that were not loaded already. Filled by `runAgentLoop`, which sees
|
|
305
319
|
* the resolution; the events carry only its text.
|
package/dist/run/events.js
CHANGED
|
@@ -56,6 +56,12 @@ export const RunEventKind = {
|
|
|
56
56
|
ToolResult: 'tool-result',
|
|
57
57
|
/** Something the runner did that is not the model's doing — a preselection, a retry. */
|
|
58
58
|
Notice: 'notice',
|
|
59
|
+
/**
|
|
60
|
+
* A tool call's answer linked to something the run had not produced before. `id` is the call's,
|
|
61
|
+
* `name` the tool, `text` the link's title or else its uri, and `link` the link itself. Once
|
|
62
|
+
* per uri per run: a later call that links the same uri again only adds to its count.
|
|
63
|
+
*/
|
|
64
|
+
Artifact: 'artifact',
|
|
59
65
|
/**
|
|
60
66
|
* What the run has cost so far, at the end of every turn, and what that one turn did. Sent
|
|
61
67
|
* whether or not the endpoint reported tokens, since the timings and the cache comparison are
|
|
@@ -500,6 +506,7 @@ export function runMetrics(events, { contextLength } = {}) {
|
|
|
500
506
|
toolCalls: 0,
|
|
501
507
|
toolErrors: {},
|
|
502
508
|
loadCalls: 0,
|
|
509
|
+
artifacts: 0,
|
|
503
510
|
promptTokens: 0,
|
|
504
511
|
completionTokens: 0,
|
|
505
512
|
cachedTokens: 0,
|
|
@@ -540,6 +547,9 @@ export function runMetrics(events, { contextLength } = {}) {
|
|
|
540
547
|
add('toolMs', event.at - called);
|
|
541
548
|
}
|
|
542
549
|
}
|
|
550
|
+
else if (event.kind === RunEventKind.Artifact) {
|
|
551
|
+
metrics.artifacts++;
|
|
552
|
+
}
|
|
543
553
|
else if (event.kind === RunEventKind.Usage && event.usage?.turn) {
|
|
544
554
|
const turn = event.usage.turn;
|
|
545
555
|
last = turn;
|
package/dist/run/run-calls.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
2
|
import type { CatalogServer } from '../tools/catalog.ts';
|
|
3
3
|
import type { Turn } from '../wire/stream.ts';
|
|
4
|
-
import { type ToolCall, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult } from '../wire/tool-calls.ts';
|
|
4
|
+
import { type RunArtifact, type ToolAnswer, type ToolCall, type ToolCallOutcome, type ToolCallRequest, type ToolCallResult } from '../wire/tool-calls.ts';
|
|
5
5
|
import type { RunEventInput } from './events.ts';
|
|
6
6
|
/**
|
|
7
7
|
* The tools between two turns: a step's calls read, run and written into the transcript.
|
|
@@ -63,8 +63,11 @@ export declare const assistantMessage: (content: string, parsed: ReadCall[]) =>
|
|
|
63
63
|
* @returns A new array of the definitions themselves, not copies.
|
|
64
64
|
*/
|
|
65
65
|
export declare const definedAs: (definitions: ReadonlyMap<string, OpenAI.ChatCompletionTool>, names: Iterable<string>) => (OpenAI.ChatCompletionCustomTool | OpenAI.ChatCompletionFunctionTool)[];
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Runs one tool call and resolves to what the model reads, alone or with what the call produced;
|
|
68
|
+
* what it throws, the model reads too.
|
|
69
|
+
*/
|
|
70
|
+
export type ToolDispatch = (call: ToolCallRequest, signal?: AbortSignal) => Promise<string | ToolAnswer>;
|
|
68
71
|
/**
|
|
69
72
|
* What running a step's calls reads of the run and writes back into it. One per run, shared by
|
|
70
73
|
* every step: the sets and counts here are the run's own, not copies.
|
|
@@ -86,6 +89,8 @@ export interface Calling {
|
|
|
86
89
|
};
|
|
87
90
|
used: Set<string>;
|
|
88
91
|
toolCalls: ToolCallOutcome[];
|
|
92
|
+
/** What the run's calls linked to, by uri, in the order first seen. */
|
|
93
|
+
artifacts: Map<string, RunArtifact>;
|
|
89
94
|
dedupable: (call: ToolCallRequest) => boolean;
|
|
90
95
|
dispatch: ToolDispatch;
|
|
91
96
|
parallel: boolean;
|
package/dist/run/run-calls.js
CHANGED
|
@@ -84,6 +84,13 @@ export const assistantMessage = (content, parsed) => ({
|
|
|
84
84
|
* @returns A new array of the definitions themselves, not copies.
|
|
85
85
|
*/
|
|
86
86
|
export const definedAs = (definitions, names) => [...names].flatMap((name) => definitions.get(name) ?? []);
|
|
87
|
+
/**
|
|
88
|
+
* A dispatcher's answer in the one shape, whichever it gave.
|
|
89
|
+
*
|
|
90
|
+
* @param answer - What `dispatch` resolved to.
|
|
91
|
+
* @returns The text and the links, a string answer's being none.
|
|
92
|
+
*/
|
|
93
|
+
const answerOf = (answer) => typeof answer === 'string' ? { text: answer, links: [] } : { text: answer.text, links: answer.links ?? [] };
|
|
87
94
|
/**
|
|
88
95
|
* What a call is shown, counted and dispatched as: a `call_tool` as the tool it names.
|
|
89
96
|
*
|
|
@@ -129,6 +136,7 @@ function answerLoad(run, args) {
|
|
|
129
136
|
* Makes a call at most once per key, sharing the in-flight promise so two identical calls in one
|
|
130
137
|
* step make one request between them whether they run together or one after the other.
|
|
131
138
|
*
|
|
139
|
+
* @typeParam T - What a call answers with.
|
|
132
140
|
* @param answered - The step's calls so far, by key. Written to.
|
|
133
141
|
* @param key - What makes two calls the same one.
|
|
134
142
|
* @param make - Makes the call. Not called where the key has an answer, or one on its way.
|
|
@@ -156,6 +164,30 @@ async function once(answered, key, make) {
|
|
|
156
164
|
throw error;
|
|
157
165
|
}
|
|
158
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Adds one call's links to the run's artifacts, telling watchers of each uri the first time.
|
|
169
|
+
*
|
|
170
|
+
* @param run - The run. Written to: a new uri is added to `artifacts`, and a known one counted.
|
|
171
|
+
* @param links - What the call's answer linked to.
|
|
172
|
+
* @param from - The call they came from.
|
|
173
|
+
*/
|
|
174
|
+
function collect(run, links, from) {
|
|
175
|
+
// Per call, so a tool that lists one uri twice in an answer is counted as making it once.
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
for (const link of links) {
|
|
178
|
+
if (seen.has(link.uri)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
seen.add(link.uri);
|
|
182
|
+
const known = run.artifacts.get(link.uri);
|
|
183
|
+
if (known) {
|
|
184
|
+
known.count++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
run.artifacts.set(link.uri, { link, ...from, count: 1 });
|
|
188
|
+
run.onEvent({ kind: RunEventKind.Artifact, id: from.callId, name: from.tool, text: link.title || link.uri, link });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
159
191
|
/**
|
|
160
192
|
* Runs one call to its result, telling the host of both. A tool that throws is that call's answer
|
|
161
193
|
* rather than the run's end, unless the run was stopped.
|
|
@@ -163,10 +195,12 @@ async function once(answered, key, make) {
|
|
|
163
195
|
* @param run - The run the call belongs to.
|
|
164
196
|
* @param entry - The call, with its arguments read.
|
|
165
197
|
* @param answered - The step's calls already made, by tool and arguments, for `dedupeToolCalls`.
|
|
166
|
-
* @
|
|
167
|
-
*
|
|
198
|
+
* @param step - Which step this is, for the artifacts the call's links become.
|
|
199
|
+
* @returns The call's id, the name it was shown under, whether it worked, the whole of what the
|
|
200
|
+
* model reads, and the links its answer carried, where it carried any. Rejects where the run was
|
|
201
|
+
* stopped or a host callback threw.
|
|
168
202
|
*/
|
|
169
|
-
async function runCall(run, entry, answered) {
|
|
203
|
+
async function runCall(run, entry, answered, step) {
|
|
170
204
|
const { catalog, onDemand, proxied, signal } = run;
|
|
171
205
|
const { call, args, error: unreadable, normal } = entry;
|
|
172
206
|
const proxy = run.proxies && call.function.name === CALL_TOOL;
|
|
@@ -191,7 +225,10 @@ async function runCall(run, entry, answered) {
|
|
|
191
225
|
run.onEvent({ kind: RunEventKind.ToolCall, id: call.id, name, text: preview(raw) });
|
|
192
226
|
run.onToolCall?.(request);
|
|
193
227
|
let content;
|
|
228
|
+
let links = [];
|
|
194
229
|
let ok = true;
|
|
230
|
+
// Answered from an earlier identical call of the step, whose links are already counted.
|
|
231
|
+
let shared = false;
|
|
195
232
|
try {
|
|
196
233
|
if (!args) {
|
|
197
234
|
throw unreadable;
|
|
@@ -214,9 +251,13 @@ async function runCall(run, entry, answered) {
|
|
|
214
251
|
run.used.add(name);
|
|
215
252
|
// Keyed on the inner call, so one made through `call_tool` and one made natively share.
|
|
216
253
|
const key = `${name}\0${proxy ? JSON.stringify(inner) : normal}`;
|
|
217
|
-
|
|
254
|
+
const dedupable = run.dedupable(request);
|
|
255
|
+
// Read before `once` looks, which it does before it awaits anything.
|
|
256
|
+
shared = dedupable && answered.has(key);
|
|
257
|
+
const answer = dedupable
|
|
218
258
|
? await once(answered, key, () => run.dispatch(request, signal))
|
|
219
259
|
: await run.dispatch(request, signal);
|
|
260
|
+
({ text: content, links } = answerOf(answer));
|
|
220
261
|
}
|
|
221
262
|
}
|
|
222
263
|
catch (error) {
|
|
@@ -227,7 +268,11 @@ async function runCall(run, entry, answered) {
|
|
|
227
268
|
ok = false;
|
|
228
269
|
}
|
|
229
270
|
run.onEvent({ kind: RunEventKind.ToolResult, id: call.id, name, ok, text: preview(content) });
|
|
230
|
-
const
|
|
271
|
+
const isFirstAnswer = shared === false;
|
|
272
|
+
if (isFirstAnswer) {
|
|
273
|
+
collect(run, links, { callId: call.id, tool: name, step });
|
|
274
|
+
}
|
|
275
|
+
const result = { id: call.id, name, ok, content, ...(links.length ? { links } : {}) };
|
|
231
276
|
run.onToolResult?.(result);
|
|
232
277
|
return result;
|
|
233
278
|
}
|
|
@@ -270,7 +315,7 @@ export async function runCalls(run, parsed, messages, step) {
|
|
|
270
315
|
signal?.throwIfAborted();
|
|
271
316
|
begun = parsed.length;
|
|
272
317
|
await Promise.all(parsed.map(async (call, at) => {
|
|
273
|
-
outcomes[at] = await runCall(run, call, answered);
|
|
318
|
+
outcomes[at] = await runCall(run, call, answered, step);
|
|
274
319
|
}));
|
|
275
320
|
for (const { id, name, ok, content } of outcomes.flatMap((outcome) => outcome ?? [])) {
|
|
276
321
|
toolCalls.push({ id, name, ok });
|
|
@@ -281,7 +326,7 @@ export async function runCalls(run, parsed, messages, step) {
|
|
|
281
326
|
for (const [at, call] of parsed.entries()) {
|
|
282
327
|
signal?.throwIfAborted();
|
|
283
328
|
begun = at + 1;
|
|
284
|
-
const { id, name, ok, content } = await runCall(run, call, answered);
|
|
329
|
+
const { id, name, ok, content } = await runCall(run, call, answered, step);
|
|
285
330
|
toolCalls.push({ id, name, ok });
|
|
286
331
|
await keep(id, content);
|
|
287
332
|
}
|
package/dist/wire/errors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
2
|
import { ContextOverflow } from './retry.ts';
|
|
3
3
|
import type { TurnUsage } from './stream.ts';
|
|
4
|
-
import type { ToolCallOutcome } from './tool-calls.ts';
|
|
4
|
+
import type { RunArtifact, ToolCallOutcome } from './tool-calls.ts';
|
|
5
5
|
/**
|
|
6
6
|
* What went wrong, as a string.
|
|
7
7
|
*
|
|
@@ -34,6 +34,12 @@ export interface AgentLoopFailure {
|
|
|
34
34
|
usage: TurnUsage;
|
|
35
35
|
/** Every call that was made, in order. One the run was stopped during is `ok: false`. */
|
|
36
36
|
toolCalls: ToolCallOutcome[];
|
|
37
|
+
/**
|
|
38
|
+
* What the calls that came back linked to, as the result's `artifacts` would have said it: a
|
|
39
|
+
* failed run's writes were still made. `runAgentLoop` always sets it; optional so a failure
|
|
40
|
+
* built before it existed still compiles.
|
|
41
|
+
*/
|
|
42
|
+
artifacts?: RunArtifact[];
|
|
37
43
|
/** What was loaded when the run failed, for `carryOver`. Empty in eager and proxied modes. */
|
|
38
44
|
loaded: string[];
|
|
39
45
|
/** The tools the model had called, `load_tools` excluded. */
|
|
@@ -55,6 +61,7 @@ export declare class AgentLoopError extends Error implements AgentLoopFailure {
|
|
|
55
61
|
readonly messages: OpenAI.ChatCompletionMessageParam[];
|
|
56
62
|
readonly usage: TurnUsage;
|
|
57
63
|
readonly toolCalls: ToolCallOutcome[];
|
|
64
|
+
readonly artifacts: RunArtifact[];
|
|
58
65
|
readonly loaded: string[];
|
|
59
66
|
readonly used: string[];
|
|
60
67
|
readonly preselected: string[];
|
|
@@ -88,6 +95,7 @@ export declare class AgentLoopOverflow extends ContextOverflow implements AgentL
|
|
|
88
95
|
readonly messages: OpenAI.ChatCompletionMessageParam[];
|
|
89
96
|
readonly usage: TurnUsage;
|
|
90
97
|
readonly toolCalls: ToolCallOutcome[];
|
|
98
|
+
readonly artifacts: RunArtifact[];
|
|
91
99
|
readonly loaded: string[];
|
|
92
100
|
readonly used: string[];
|
|
93
101
|
readonly preselected: string[];
|
package/dist/wire/errors.js
CHANGED
|
@@ -26,6 +26,7 @@ export class AgentLoopError extends Error {
|
|
|
26
26
|
messages;
|
|
27
27
|
usage;
|
|
28
28
|
toolCalls;
|
|
29
|
+
artifacts;
|
|
29
30
|
loaded;
|
|
30
31
|
used;
|
|
31
32
|
preselected;
|
|
@@ -39,6 +40,7 @@ export class AgentLoopError extends Error {
|
|
|
39
40
|
this.messages = run.messages;
|
|
40
41
|
this.usage = run.usage;
|
|
41
42
|
this.toolCalls = run.toolCalls;
|
|
43
|
+
this.artifacts = run.artifacts ?? [];
|
|
42
44
|
this.loaded = run.loaded;
|
|
43
45
|
this.used = run.used;
|
|
44
46
|
this.preselected = run.preselected;
|
|
@@ -67,6 +69,7 @@ export class AgentLoopOverflow extends ContextOverflow {
|
|
|
67
69
|
messages;
|
|
68
70
|
usage;
|
|
69
71
|
toolCalls;
|
|
72
|
+
artifacts;
|
|
70
73
|
loaded;
|
|
71
74
|
used;
|
|
72
75
|
preselected;
|
|
@@ -80,6 +83,7 @@ export class AgentLoopOverflow extends ContextOverflow {
|
|
|
80
83
|
this.messages = run.messages;
|
|
81
84
|
this.usage = run.usage;
|
|
82
85
|
this.toolCalls = run.toolCalls;
|
|
86
|
+
this.artifacts = run.artifacts ?? [];
|
|
83
87
|
this.loaded = run.loaded;
|
|
84
88
|
this.used = run.used;
|
|
85
89
|
this.preselected = run.preselected;
|
|
@@ -146,4 +146,68 @@ export interface ToolCallResult extends ToolCallOutcome {
|
|
|
146
146
|
* `load_tools` answer, or why the arguments could not be read.
|
|
147
147
|
*/
|
|
148
148
|
content: string;
|
|
149
|
+
/**
|
|
150
|
+
* What the dispatcher's answer said the call produced, as it said it. Absent where it said
|
|
151
|
+
* nothing, and always for a call that threw. A deduplicated repeat carries the first call's.
|
|
152
|
+
*/
|
|
153
|
+
links?: ResourceLink[];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A handle to something a tool call made or found — a document it wrote, a file it saved.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* MCP's `ResourceLink` less its `type`, declared structurally rather than imported, the way
|
|
160
|
+
* agent-mcp-pool declares `ToolDefinition` against `ChatCompletionTool`: the pool's own
|
|
161
|
+
* `ResourceLink` has these field names exactly, so its answer is assignable here and neither
|
|
162
|
+
* package depends on the other.
|
|
163
|
+
*/
|
|
164
|
+
export interface ResourceLink {
|
|
165
|
+
uri: string;
|
|
166
|
+
name: string;
|
|
167
|
+
title?: string;
|
|
168
|
+
mimeType?: string;
|
|
169
|
+
description?: string;
|
|
170
|
+
/** Who the link is for, how much it matters, and when it last changed — the server's word. */
|
|
171
|
+
annotations?: {
|
|
172
|
+
audience?: ('user' | 'assistant')[];
|
|
173
|
+
priority?: number;
|
|
174
|
+
lastModified?: string;
|
|
175
|
+
};
|
|
176
|
+
_meta?: Record<string, unknown>;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* A dispatcher's answer when it has more to say than the text: what the model reads, and what the
|
|
180
|
+
* call produced for the host.
|
|
181
|
+
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* A link meant for the model alone — one whose MCP `audience` leaves out `"user"` — is the
|
|
184
|
+
* dispatcher's to drop, since that is MCP's rule and this package knows nothing of MCP.
|
|
185
|
+
*/
|
|
186
|
+
export interface ToolAnswer {
|
|
187
|
+
/** What goes into the transcript, exactly as a string answer does. */
|
|
188
|
+
text: string;
|
|
189
|
+
/** What the call produced, in the order the tool gave them. Never sent to the model. */
|
|
190
|
+
links?: ResourceLink[];
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* One thing a run's tool calls produced: the first link to a uri, and where it came from.
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* A link is what one call answered with; an artifact is what the run made, one per uri however
|
|
197
|
+
* many calls linked to it.
|
|
198
|
+
*/
|
|
199
|
+
export interface RunArtifact {
|
|
200
|
+
/** The link as the first call to give its uri gave it. A later call's says nothing new here. */
|
|
201
|
+
link: ResourceLink;
|
|
202
|
+
/** The call that first linked to it. */
|
|
203
|
+
callId: string;
|
|
204
|
+
/** That call's tool. For a `call_tool`, the tool it named. */
|
|
205
|
+
tool: string;
|
|
206
|
+
/** The loop's step that call was made in, from 0. */
|
|
207
|
+
step: number;
|
|
208
|
+
/**
|
|
209
|
+
* How many calls linked to it. A deduplicated repeat does not count, since nothing was made
|
|
210
|
+
* twice, and a call that links one uri twice counts once.
|
|
211
|
+
*/
|
|
212
|
+
count: number;
|
|
149
213
|
}
|
package/llms.txt
CHANGED
|
@@ -185,6 +185,7 @@ What a run is doing, while it is doing it.
|
|
|
185
185
|
The tools between two turns: a step's calls read, run and written into the transcript.
|
|
186
186
|
|
|
187
187
|
- `preview` — A long tool argument or result cut to what a watcher needs, with the full length said.
|
|
188
|
+
- `ToolDispatch` (type) — Runs one tool call and resolves to what the model reads, alone or with what the call produced; what it throws, the model reads too.
|
|
188
189
|
|
|
189
190
|
### reset
|
|
190
191
|
|
|
@@ -384,7 +385,10 @@ Reading what a model meant by a tool call when it did not write one cleanly.
|
|
|
384
385
|
- `ARGUMENTS_MALFORMED` — The `ToolArgumentsError` kind for a call the model wrote wrongly.
|
|
385
386
|
- `ARGUMENTS_TRUNCATED` — The `ToolArgumentsError` kind for a call cut off at the reply ceiling.
|
|
386
387
|
- `parseToolArguments` — A tool call's arguments as the object the tool is handed.
|
|
388
|
+
- `ResourceLink` (type) — A handle to something a tool call made or found — a document it wrote, a file it saved.
|
|
389
|
+
- `RunArtifact` (type) — One thing a run's tool calls produced: the first link to a uri, and where it came from.
|
|
387
390
|
- `recoverToolCalls` — Tool calls a model wrote into its reply as text, taken out of it and made into calls.
|
|
391
|
+
- `ToolAnswer` (type) — A dispatcher's answer when it has more to say than the text: what the model reads, and what the call produced for the host.
|
|
388
392
|
- `ToolArgumentsError` — Tool arguments that could not be read as an object, with why.
|
|
389
393
|
- `ToolCall` (type) — A tool call as the loop handles it, recovered or streamed.
|
|
390
394
|
- `ToolCallOutcome` (type) — What one tool call did, in the order the model asked.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.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",
|