@cubicecho/agent-core 2.12.0 → 2.14.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 +114 -3
- package/dist/agent-loop.d.ts +15 -4
- package/dist/agent-loop.js +21 -11
- package/dist/capabilities.d.ts +46 -2
- package/dist/capabilities.js +48 -4
- package/dist/compaction.d.ts +112 -16
- package/dist/compaction.js +118 -32
- package/dist/hooks.d.ts +20 -2
- package/dist/hooks.js +23 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/retry.d.ts +60 -0
- package/dist/retry.js +83 -0
- package/dist/snapshot.d.ts +11 -2
- package/dist/snapshot.js +18 -4
- package/dist/tool-loading.d.ts +29 -4
- package/dist/tool-loading.js +33 -4
- package/llms.txt +13 -1
- package/package.json +1 -1
package/dist/compaction.js
CHANGED
|
@@ -37,6 +37,25 @@ const messageText = (message) => {
|
|
|
37
37
|
return `${textOf(message.content)} ${calls}`.trim();
|
|
38
38
|
};
|
|
39
39
|
const isSummary = (message) => message.role === "system" && textOf(message.content).startsWith(SUMMARY_LEAD);
|
|
40
|
+
/** The summary as it sits in a transcript, which is the one shape `isSummary` recognises again. */
|
|
41
|
+
const summaryMessage = (summary) => ({
|
|
42
|
+
role: "system",
|
|
43
|
+
content: `${SUMMARY_LEAD}${summary.trim()}`,
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* The leading system messages a fold never touches, and the summary an earlier one left among
|
|
47
|
+
* them. What a caller who keeps its system prompt out of the array knows already, and passes.
|
|
48
|
+
*/
|
|
49
|
+
const systemHead = (messages) => {
|
|
50
|
+
let from = 0;
|
|
51
|
+
let previous;
|
|
52
|
+
while (from < messages.length && messages[from].role === "system") {
|
|
53
|
+
if (isSummary(messages[from]))
|
|
54
|
+
previous = textOf(messages[from].content).slice(SUMMARY_LEAD.length);
|
|
55
|
+
from++;
|
|
56
|
+
}
|
|
57
|
+
return { from, previous };
|
|
58
|
+
};
|
|
40
59
|
/**
|
|
41
60
|
* The transcript with every tool result but the latest few replaced by a one-line stub.
|
|
42
61
|
*
|
|
@@ -80,22 +99,24 @@ export function pruneToolResults(messages, { keepLast = 5, maxChars = 256 } = {}
|
|
|
80
99
|
* than summarised as if it were conversation. No plan comes back when the window is not full
|
|
81
100
|
* enough, or when the only legal cut folds too little to pay for the summary.
|
|
82
101
|
*
|
|
102
|
+
* Both of those are recovered by reading the transcript, which is what a host whose array holds
|
|
103
|
+
* everything it sends has to do. A host that keeps its fold as a record beside an append-only
|
|
104
|
+
* transcript — its system prompt a separate argument, no summary message in the array at all —
|
|
105
|
+
* knows them exactly, and passes `from` and `previous` instead of hoping the scan agrees.
|
|
106
|
+
*
|
|
83
107
|
* @param messages The transcript, system prompts included if the caller keeps them in it.
|
|
84
|
-
* @param options The window, what is in use, and the
|
|
108
|
+
* @param options The window, what is in use, the ratios, and where the last fold ended. See
|
|
109
|
+
* `CompactionOptions`.
|
|
85
110
|
*/
|
|
86
|
-
export function planCompaction(messages, { limit, used, compactAt = COMPACT_AT, keepRatio = KEEP_RATIO, charsPerToken, estimate = (message) => messageTokens(message, { charsPerToken }), }) {
|
|
111
|
+
export function planCompaction(messages, { limit, used, compactAt = COMPACT_AT, keepRatio = KEEP_RATIO, charsPerToken, estimate = (message) => messageTokens(message, { charsPerToken }), from: givenFrom, previous: givenPrevious, }) {
|
|
87
112
|
if (!(limit > 0))
|
|
88
113
|
return undefined;
|
|
89
114
|
const cost = used ?? messages.reduce((total, message) => total + estimate(message), 0);
|
|
90
115
|
if (cost < limit * compactAt)
|
|
91
116
|
return undefined;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (isSummary(messages[from]))
|
|
96
|
-
previous = textOf(messages[from].content).slice(SUMMARY_LEAD.length);
|
|
97
|
-
from++;
|
|
98
|
-
}
|
|
117
|
+
const head = systemHead(messages);
|
|
118
|
+
const from = Math.min(Math.max(givenFrom ?? head.from, 0), messages.length);
|
|
119
|
+
const previous = givenPrevious ?? head.previous;
|
|
99
120
|
const budget = limit * keepRatio;
|
|
100
121
|
let kept = 0;
|
|
101
122
|
let cut = messages.length;
|
|
@@ -139,31 +160,21 @@ export function summaryInput(plan) {
|
|
|
139
160
|
*/
|
|
140
161
|
export const summariser = (config, model, { system = SUMMARY_PROMPT, maxTokens = 1024, ...options } = {}) => (text) => ask(config, model, system, text, { maxTokens, ...options });
|
|
141
162
|
/**
|
|
142
|
-
* The
|
|
163
|
+
* The hooks and the summariser for a plan, as a record to store rather than a transcript to send.
|
|
143
164
|
*
|
|
144
|
-
* `
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* `honourVeto`, and then they run first and the summary waits on them: any `ok` outcome carrying
|
|
148
|
-
* `veto` leaves the transcript as it was, and each vetoing hook is noted by name. A `forced`
|
|
149
|
-
* compaction ignores a veto and runs the hooks beside the summary as before, because a run already
|
|
150
|
-
* past its window has no better option — a veto there only trades the summary for a
|
|
151
|
-
* `ContextOverflow`. An empty summary folds nothing either. Rewrites the prefix; see the module
|
|
152
|
-
* comment on when to run it.
|
|
165
|
+
* What `compactTranscript` does before it rewrites anything, which is all a host needs when the
|
|
166
|
+
* fold lives on the session row and the messages stay where they are. Nothing here is persisted or
|
|
167
|
+
* logged — that is the host's, and so is deciding what to do with a fold that did not happen.
|
|
153
168
|
*
|
|
154
|
-
* @param messages The transcript the plan was made for.
|
|
169
|
+
* @param messages The transcript the plan was made for. Read only, and only for the hooks.
|
|
155
170
|
* @param plan What `planCompaction` returned for it.
|
|
156
171
|
* @param summarise Writes the summary from `summaryInput`'s text. See `summariser`. Not called
|
|
157
172
|
* when a hook vetoes.
|
|
158
|
-
* @param options Hooks to tell
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* caught a `ContextOverflow`, or is compacting to make a refused request fit — and overrides
|
|
162
|
-
* `honourVeto`.
|
|
163
|
-
* @returns `messages` itself when nothing was folded — a veto or an empty summary — otherwise a
|
|
164
|
-
* new array.
|
|
173
|
+
* @param options Hooks to tell and whether the window is already past. See `CompactionRunOptions`.
|
|
174
|
+
* @returns `undefined` when nothing was folded — a hook vetoed, or the summary came back empty —
|
|
175
|
+
* so the caller stores nothing and the transcript is still whole.
|
|
165
176
|
*/
|
|
166
|
-
export async function
|
|
177
|
+
export async function runCompaction(messages, plan, summarise, { hooks, forced = false } = {}) {
|
|
167
178
|
const context = hooks && {
|
|
168
179
|
...hooks.context,
|
|
169
180
|
compacting: turnMessages(hooks.context.session.id, messages, plan.from, plan.cut),
|
|
@@ -173,7 +184,7 @@ export async function compactTranscript(messages, plan, summarise, { hooks, forc
|
|
|
173
184
|
if (hooks && context && hooks.honourVeto && !forced) {
|
|
174
185
|
const { vetoed } = await consult(hooks.run, "beforeCompact", context, hooks.onNote);
|
|
175
186
|
if (vetoed)
|
|
176
|
-
return
|
|
187
|
+
return undefined;
|
|
177
188
|
summary = await summarise(summaryInput(plan));
|
|
178
189
|
}
|
|
179
190
|
else {
|
|
@@ -183,10 +194,85 @@ export async function compactTranscript(messages, plan, summarise, { hooks, forc
|
|
|
183
194
|
]);
|
|
184
195
|
}
|
|
185
196
|
if (!summary.trim())
|
|
197
|
+
return undefined;
|
|
198
|
+
return { summary: summary.trim(), through: plan.cut, at: new Date().toISOString() };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The transcript as the server should see it: the folded head replaced by its summary.
|
|
202
|
+
*
|
|
203
|
+
* The inverse of storing a `CompactionRecord`, and the shape `planCompaction` expects to meet
|
|
204
|
+
* again — the same `SUMMARY_LEAD`, in a `system` message at the same place — so the next fold
|
|
205
|
+
* continues these notes rather than summarising them a second time. Any earlier summary message in
|
|
206
|
+
* the kept head is dropped, since the record's already contains it.
|
|
207
|
+
*
|
|
208
|
+
* @param messages The stored transcript, whole. Not written to.
|
|
209
|
+
* @param record The fold, or `undefined` for a session that has not been compacted, which hands
|
|
210
|
+
* back `messages` itself.
|
|
211
|
+
* @param options `from` is the first message the fold was allowed to take — the plan's, for a host
|
|
212
|
+
* that keeps its system prompts in the array; everything before it is kept ahead of the summary.
|
|
213
|
+
* Absent, the leading `system` messages are found by scanning, and zero of them is the ordinary
|
|
214
|
+
* case for a host whose system prompt is a separate argument.
|
|
215
|
+
*/
|
|
216
|
+
export function applyCompaction(messages, record, { from } = {}) {
|
|
217
|
+
if (!record?.summary.trim())
|
|
186
218
|
return messages;
|
|
219
|
+
const head = Math.min(Math.max(from ?? systemHead(messages).from, 0), record.through);
|
|
187
220
|
return [
|
|
188
|
-
...messages.slice(0,
|
|
189
|
-
|
|
190
|
-
...messages.slice(
|
|
221
|
+
...messages.slice(0, head).filter((message) => !isSummary(message)),
|
|
222
|
+
summaryMessage(record.summary),
|
|
223
|
+
...messages.slice(record.through),
|
|
191
224
|
];
|
|
192
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Where a stored index sits in the request `applyCompaction` builds, once a fold has shifted
|
|
228
|
+
* everything after it.
|
|
229
|
+
*
|
|
230
|
+
* A transcript that stays append-only and a request that does not are two numberings of the same
|
|
231
|
+
* conversation, and anything that names a position — `withContext`'s index, a range handed to a
|
|
232
|
+
* hook — has to say which it is in. An index inside the folded stretch answers with the summary
|
|
233
|
+
* message that now stands for it.
|
|
234
|
+
*
|
|
235
|
+
* @param index The position in the stored transcript.
|
|
236
|
+
* @param record The fold in force, or `undefined` for a session that has none, which hands the
|
|
237
|
+
* index straight back.
|
|
238
|
+
* @param head How many messages the request keeps ahead of the summary — the leading system
|
|
239
|
+
* prompts, when the host keeps them in the array. Zero, the default, is the stored-fold case,
|
|
240
|
+
* where the summary is the request's first message.
|
|
241
|
+
*/
|
|
242
|
+
export const requestIndex = (index, record, head = 0) => {
|
|
243
|
+
if (!record)
|
|
244
|
+
return index;
|
|
245
|
+
return index < record.through ? head : index - record.through + head + 1;
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* The transcript with the plan's stretch replaced by one system message holding its summary.
|
|
249
|
+
*
|
|
250
|
+
* `beforeCompact` is told what is being folded while the summary is written, beside it rather
|
|
251
|
+
* than ahead of it — a memory server filing it is not a rescue worth making the run wait for, and
|
|
252
|
+
* `notify` never rejects. A host that wants its hooks able to stop a compaction sets
|
|
253
|
+
* `honourVeto`, and then they run first and the summary waits on them: any `ok` outcome carrying
|
|
254
|
+
* `veto` leaves the transcript as it was, and each vetoing hook is noted by name. A `forced`
|
|
255
|
+
* compaction ignores a veto and runs the hooks beside the summary as before, because a run already
|
|
256
|
+
* past its window has no better option — a veto there only trades the summary for a
|
|
257
|
+
* `ContextOverflow`. An empty summary folds nothing either. Rewrites the prefix; see the module
|
|
258
|
+
* comment on when to run it.
|
|
259
|
+
*
|
|
260
|
+
* `runCompaction` and `applyCompaction` are its two halves, and it is nothing but the two in
|
|
261
|
+
* order, so a host that stores the fold instead of the array gets the same summary at the same
|
|
262
|
+
* cut rather than a second implementation that drifts from this one.
|
|
263
|
+
*
|
|
264
|
+
* @param messages The transcript the plan was made for. Not written to.
|
|
265
|
+
* @param plan What `planCompaction` returned for it.
|
|
266
|
+
* @param summarise Writes the summary from `summaryInput`'s text. See `summariser`. Not called
|
|
267
|
+
* when a hook vetoes.
|
|
268
|
+
* @param options Hooks to tell and whether the window is already past. See
|
|
269
|
+
* `CompactionRunOptions`.
|
|
270
|
+
* @returns `messages` itself when nothing was folded — a veto or an empty summary — otherwise a
|
|
271
|
+
* new array.
|
|
272
|
+
*/
|
|
273
|
+
export async function compactTranscript(messages, plan, summarise, options = {}) {
|
|
274
|
+
const record = await runCompaction(messages, plan, summarise, options);
|
|
275
|
+
if (!record)
|
|
276
|
+
return messages;
|
|
277
|
+
return applyCompaction(messages, record, { from: plan.from });
|
|
278
|
+
}
|
package/dist/hooks.d.ts
CHANGED
|
@@ -256,26 +256,44 @@ export declare function untrusted(text: string, { source }?: {
|
|
|
256
256
|
* two identical "ok"s one memory. The same message sent twice, after its turn and again when it
|
|
257
257
|
* is compacted, is one.
|
|
258
258
|
*
|
|
259
|
+
* That position is the array's, which is the host's only while the two agree. Once a compaction
|
|
260
|
+
* has folded the head of a request into one summary message, the same message sits at a lower
|
|
261
|
+
* index there than it does in a transcript the host kept whole — so the same turn, sent after it
|
|
262
|
+
* and again when it is compacted, would arrive under two uuids and be remembered twice. `offset`
|
|
263
|
+
* is what puts the numbering back on the host's own indexes; passing the stored transcript rather
|
|
264
|
+
* than the request avoids the question entirely.
|
|
265
|
+
*
|
|
259
266
|
* @param sessionId Prefixes every uuid, so two sessions never share one.
|
|
260
267
|
* @param messages The transcript, in whatever shape the host stores it, so long as each message
|
|
261
268
|
* has an OpenAI-style `role` and `content`.
|
|
262
269
|
* @param from The first index, inclusive. Below zero reads from the start.
|
|
263
270
|
* @param to The end, exclusive. Absent, or past the end, reads to the end.
|
|
271
|
+
* @param options `offset` is what the array's first message is numbered as in the uuids — the
|
|
272
|
+
* stored index of `messages[0]`, when `messages` is a request a fold has shifted. Zero by default,
|
|
273
|
+
* which numbers by position as before. `from` and `to` stay array indexes either way.
|
|
264
274
|
*/
|
|
265
275
|
export declare function turnMessages(sessionId: string, messages: readonly {
|
|
266
276
|
role: string;
|
|
267
277
|
content?: unknown;
|
|
268
|
-
}[], from: number, to?: number
|
|
278
|
+
}[], from: number, to?: number, { offset }?: {
|
|
279
|
+
offset?: number;
|
|
280
|
+
}): HookMessage[];
|
|
269
281
|
/**
|
|
270
282
|
* Which turn of a session begins at a point, from 0: the user messages ahead of it.
|
|
271
283
|
*
|
|
284
|
+
* Counted over what it is given, which is the session only while nothing has been folded away — a
|
|
285
|
+
* compacted request has lost the questions the summary now stands for, and turn eleven counting
|
|
286
|
+
* itself as turn two is the kind of thing a hook writes into a memory. Count over the stored
|
|
287
|
+
* transcript, or add what the fold took as `offset`.
|
|
288
|
+
*
|
|
272
289
|
* @param messages The transcript.
|
|
273
290
|
* @param before Where the turn begins. Absent is the end, which is the index of a turn whose
|
|
274
291
|
* question has not been appended yet.
|
|
292
|
+
* @param offset Turns already folded away and so not in `messages`. Zero by default.
|
|
275
293
|
*/
|
|
276
294
|
export declare const turnIndex: (messages: readonly {
|
|
277
295
|
role: string;
|
|
278
|
-
}[], before?: number) => number;
|
|
296
|
+
}[], before?: number, offset?: number) => number;
|
|
279
297
|
/**
|
|
280
298
|
* Runs the hooks ahead of a request and builds what they add to it.
|
|
281
299
|
*
|
package/dist/hooks.js
CHANGED
|
@@ -204,13 +204,23 @@ const textOf = (content) => {
|
|
|
204
204
|
* two identical "ok"s one memory. The same message sent twice, after its turn and again when it
|
|
205
205
|
* is compacted, is one.
|
|
206
206
|
*
|
|
207
|
+
* That position is the array's, which is the host's only while the two agree. Once a compaction
|
|
208
|
+
* has folded the head of a request into one summary message, the same message sits at a lower
|
|
209
|
+
* index there than it does in a transcript the host kept whole — so the same turn, sent after it
|
|
210
|
+
* and again when it is compacted, would arrive under two uuids and be remembered twice. `offset`
|
|
211
|
+
* is what puts the numbering back on the host's own indexes; passing the stored transcript rather
|
|
212
|
+
* than the request avoids the question entirely.
|
|
213
|
+
*
|
|
207
214
|
* @param sessionId Prefixes every uuid, so two sessions never share one.
|
|
208
215
|
* @param messages The transcript, in whatever shape the host stores it, so long as each message
|
|
209
216
|
* has an OpenAI-style `role` and `content`.
|
|
210
217
|
* @param from The first index, inclusive. Below zero reads from the start.
|
|
211
218
|
* @param to The end, exclusive. Absent, or past the end, reads to the end.
|
|
219
|
+
* @param options `offset` is what the array's first message is numbered as in the uuids — the
|
|
220
|
+
* stored index of `messages[0]`, when `messages` is a request a fold has shifted. Zero by default,
|
|
221
|
+
* which numbers by position as before. `from` and `to` stay array indexes either way.
|
|
212
222
|
*/
|
|
213
|
-
export function turnMessages(sessionId, messages, from, to) {
|
|
223
|
+
export function turnMessages(sessionId, messages, from, to, { offset = 0 } = {}) {
|
|
214
224
|
const end = Math.min(to ?? messages.length, messages.length);
|
|
215
225
|
const out = [];
|
|
216
226
|
for (let at = Math.max(0, from); at < end; at++) {
|
|
@@ -221,18 +231,28 @@ export function turnMessages(sessionId, messages, from, to) {
|
|
|
221
231
|
if (!text)
|
|
222
232
|
continue;
|
|
223
233
|
const digest = createHash("sha256").update(`${message.role}\0${text}`).digest("hex");
|
|
224
|
-
out.push({
|
|
234
|
+
out.push({
|
|
235
|
+
speaker: message.role,
|
|
236
|
+
text,
|
|
237
|
+
uuid: `${sessionId}:${at + offset}:${digest.slice(0, 12)}`,
|
|
238
|
+
});
|
|
225
239
|
}
|
|
226
240
|
return out;
|
|
227
241
|
}
|
|
228
242
|
/**
|
|
229
243
|
* Which turn of a session begins at a point, from 0: the user messages ahead of it.
|
|
230
244
|
*
|
|
245
|
+
* Counted over what it is given, which is the session only while nothing has been folded away — a
|
|
246
|
+
* compacted request has lost the questions the summary now stands for, and turn eleven counting
|
|
247
|
+
* itself as turn two is the kind of thing a hook writes into a memory. Count over the stored
|
|
248
|
+
* transcript, or add what the fold took as `offset`.
|
|
249
|
+
*
|
|
231
250
|
* @param messages The transcript.
|
|
232
251
|
* @param before Where the turn begins. Absent is the end, which is the index of a turn whose
|
|
233
252
|
* question has not been appended yet.
|
|
253
|
+
* @param offset Turns already folded away and so not in `messages`. Zero by default.
|
|
234
254
|
*/
|
|
235
|
-
export const turnIndex = (messages, before = messages.length) => messages.slice(0, before).filter((message) => message.role === "user").length;
|
|
255
|
+
export const turnIndex = (messages, before = messages.length, offset = 0) => offset + messages.slice(0, before).filter((message) => message.role === "user").length;
|
|
236
256
|
/** A runner that rejected, as the one outcome its event can still be noted by. */
|
|
237
257
|
const rejected = (event, error) => ({
|
|
238
258
|
serverId: "",
|
package/dist/index.d.ts
CHANGED
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopResult, buildBody, preselect, preview, resolveApiKey, runAgentLoop, type ToolCallOutcome, type ToolCallRequest, } from "./agent-loop.ts";
|
|
13
13
|
export { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.ts";
|
|
14
|
-
export { type Capabilities, capabilitiesFor, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
14
|
+
export { type Capabilities, capabilitiesFor, expireCapabilities, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
15
15
|
export type { CatalogServer } from "./catalog.ts";
|
|
16
16
|
export { type ClientPoolOptions, configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, type ModelInfo, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.ts";
|
|
17
|
-
export { COMPACT_AT, type CompactionOptions, type CompactionPlan, compactTranscript, KEEP_RATIO, type PruneOptions, planCompaction, pruneToolResults, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.ts";
|
|
17
|
+
export { applyCompaction, COMPACT_AT, type CompactionOptions, type CompactionPlan, type CompactionRecord, type CompactionRunOptions, compactTranscript, KEEP_RATIO, type PruneOptions, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.ts";
|
|
18
18
|
export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } from "./config.ts";
|
|
19
19
|
export { type ContinueTurnOptions, continueTurn, isContinuable, } from "./continuation.ts";
|
|
20
20
|
export { errorMessage } from "./errors.ts";
|
|
21
21
|
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunMetrics, type RunMetricsOptions, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from "./events.ts";
|
|
22
22
|
export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, type HookContext, type HookEvent, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.ts";
|
|
23
23
|
export { resetAll } from "./reset.ts";
|
|
24
|
-
export { backoffMs, CHARS_PER_TOKEN, ContextOverflow, compact, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, type TokenEstimateOptions, toolsChars, } from "./retry.ts";
|
|
24
|
+
export { backoffMs, CHARS_PER_TOKEN, type ContextBreakdown, type ContextBreakdownOptions, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, type TokenEstimateOptions, toolsChars, } from "./retry.ts";
|
|
25
25
|
export { type RunTurnOptions, runTurn } from "./run-turn.ts";
|
|
26
26
|
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.ts";
|
|
27
27
|
export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTaskInput, type SideTaskOptions, tryAsk, } from "./side-task.ts";
|
|
@@ -30,4 +30,4 @@ export { type Produced, type StreamTurnOptions, streamTurn, type Turn, type Turn
|
|
|
30
30
|
export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, type Split, stripThinking, THINK_FENCE, } from "./thinking.ts";
|
|
31
31
|
export { estimateTokens } from "./tokens.ts";
|
|
32
32
|
export { parseToolArguments, recoverToolCalls, ToolArgumentsError, type ToolCall, } from "./tool-calls.ts";
|
|
33
|
-
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.ts";
|
|
33
|
+
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, orderTools, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, type ToolOrder, } from "./tool-loading.ts";
|
package/dist/index.js
CHANGED
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { buildBody, preselect, preview, resolveApiKey, runAgentLoop, } from "./agent-loop.js";
|
|
13
13
|
export { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.js";
|
|
14
|
-
export { capabilitiesFor, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
14
|
+
export { capabilitiesFor, expireCapabilities, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
15
15
|
export { configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.js";
|
|
16
|
-
export { COMPACT_AT, compactTranscript, KEEP_RATIO, planCompaction, pruneToolResults, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.js";
|
|
16
|
+
export { applyCompaction, COMPACT_AT, compactTranscript, KEEP_RATIO, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.js";
|
|
17
17
|
export { continueTurn, isContinuable, } from "./continuation.js";
|
|
18
18
|
export { errorMessage } from "./errors.js";
|
|
19
19
|
export { configureEvents, emit, endRun, fold, history, resetEvents, runMetrics, watch, } from "./events.js";
|
|
20
20
|
export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.js";
|
|
21
21
|
export { resetAll } from "./reset.js";
|
|
22
|
-
export { backoffMs, CHARS_PER_TOKEN, ContextOverflow, compact, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, toolsChars, } from "./retry.js";
|
|
22
|
+
export { backoffMs, CHARS_PER_TOKEN, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, toolsChars, } from "./retry.js";
|
|
23
23
|
export { runTurn } from "./run-turn.js";
|
|
24
24
|
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.js";
|
|
25
25
|
export { ask, askJson, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
|
|
@@ -28,4 +28,4 @@ export { streamTurn, } from "./stream.js";
|
|
|
28
28
|
export { ALL_FENCES, DEFAULT_FENCES, FenceSplitter, stripThinking, THINK_FENCE, } from "./thinking.js";
|
|
29
29
|
export { estimateTokens } from "./tokens.js";
|
|
30
30
|
export { parseToolArguments, recoverToolCalls, ToolArgumentsError, } from "./tool-calls.js";
|
|
31
|
-
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.js";
|
|
31
|
+
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, orderTools, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.js";
|
package/dist/retry.d.ts
CHANGED
|
@@ -79,6 +79,66 @@ export declare function requestChars(body: OpenAI.ChatCompletionCreateParamsStre
|
|
|
79
79
|
* @param options The divisor, `CHARS_PER_TOKEN` when none is given.
|
|
80
80
|
*/
|
|
81
81
|
export declare const requestTokens: (body: OpenAI.ChatCompletionCreateParamsStreaming, { charsPerToken }?: TokenEstimateOptions) => number;
|
|
82
|
+
/**
|
|
83
|
+
* What a request is made of, by the part of it a consumer can actually do something about.
|
|
84
|
+
*
|
|
85
|
+
* The question an operator asks is not how big the request is — the total already answers that —
|
|
86
|
+
* but what is filling the window, and the only useful answer names a lever: a system prompt to
|
|
87
|
+
* shorten, a tool list to load on demand instead of declaring whole, a transcript to compact,
|
|
88
|
+
* results to prune. So the cut follows the levers rather than the roles: `toolResults` is exactly
|
|
89
|
+
* what `pruneToolResults` can shrink, and `history` is everything `planCompaction` folds, the
|
|
90
|
+
* arguments of the calls in it included.
|
|
91
|
+
*/
|
|
92
|
+
export interface ContextBreakdown {
|
|
93
|
+
/** Every system and developer message, wherever it sits in the transcript. */
|
|
94
|
+
system: number;
|
|
95
|
+
/** The declared tool schemas, which a chat template renders ahead of the system prompt. */
|
|
96
|
+
tools: number;
|
|
97
|
+
/** What was said: the user and assistant messages, and the calls the assistant asked for. */
|
|
98
|
+
history: number;
|
|
99
|
+
/** What the tools handed back — the `tool` messages, and nothing else. */
|
|
100
|
+
toolResults: number;
|
|
101
|
+
/** The four above, summed. */
|
|
102
|
+
total: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* What each part of a request is worth in characters, by the same walk `requestTokens` divides.
|
|
106
|
+
*
|
|
107
|
+
* Exact and additive: the parts sum to `total`, which is `requestChars` plus `toolsChars`. The
|
|
108
|
+
* conversion to tokens is `contextTokens`' business, because that is where an estimate and a
|
|
109
|
+
* reported count have to be told apart.
|
|
110
|
+
*
|
|
111
|
+
* @param body The request as it will be sent, tools included.
|
|
112
|
+
*/
|
|
113
|
+
export declare function contextChars(body: OpenAI.ChatCompletionCreateParamsStreaming): ContextBreakdown;
|
|
114
|
+
/** What `contextTokens` takes besides the request. */
|
|
115
|
+
export interface ContextBreakdownOptions extends TokenEstimateOptions {
|
|
116
|
+
/**
|
|
117
|
+
* The prompt count the endpoint reported for this request, if it has answered. Given one, the
|
|
118
|
+
* parts are shares of it and the breakdown sums to what was actually charged rather than to an
|
|
119
|
+
* estimate; left out, they are shares of `requestTokens`.
|
|
120
|
+
*/
|
|
121
|
+
promptTokens?: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* What each part of a request costs the window, in tokens, adding up to the whole.
|
|
125
|
+
*
|
|
126
|
+
* Shares rather than four independent estimates, because a readout whose parts do not add up to
|
|
127
|
+
* the total beside them is a readout nobody trusts. Nothing in the round trip reports anything
|
|
128
|
+
* finer than a prompt count — a completion says how many tokens it read and not a word about
|
|
129
|
+
* where they came from — so the proportions are an estimate whatever the total is.
|
|
130
|
+
*
|
|
131
|
+
* Without a reported count the total is `requestTokens`, and the tools are counted the way it and
|
|
132
|
+
* `TurnMetrics.toolSchemaTokens` count them rather than shared out, so the two agree by
|
|
133
|
+
* construction and an operator does not read one number for the tool block in the metrics and a
|
|
134
|
+
* different one here. With a reported count every part is a share of it, the tools included:
|
|
135
|
+
* that number is the server's, and the point of using it is that the parts sum to what was
|
|
136
|
+
* charged.
|
|
137
|
+
*
|
|
138
|
+
* @param body The request as it will be sent, tools included.
|
|
139
|
+
* @param options The divisor, and the reported prompt count when there is one.
|
|
140
|
+
*/
|
|
141
|
+
export declare function contextTokens(body: OpenAI.ChatCompletionCreateParamsStreaming, { charsPerToken, promptTokens }?: ContextBreakdownOptions): ContextBreakdown;
|
|
82
142
|
/**
|
|
83
143
|
* One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
|
|
84
144
|
*
|
package/dist/retry.js
CHANGED
|
@@ -183,6 +183,89 @@ export const requestTokens = (body, { charsPerToken } = {}) => {
|
|
|
183
183
|
const per = divisor(charsPerToken);
|
|
184
184
|
return Math.ceil(requestChars(body) / per) + Math.ceil(toolsChars(body.tools ?? []) / per);
|
|
185
185
|
};
|
|
186
|
+
/** The parts, in the order a readout reads them. */
|
|
187
|
+
const PARTS = ["system", "tools", "history", "toolResults"];
|
|
188
|
+
/**
|
|
189
|
+
* What each part of a request is worth in characters, by the same walk `requestTokens` divides.
|
|
190
|
+
*
|
|
191
|
+
* Exact and additive: the parts sum to `total`, which is `requestChars` plus `toolsChars`. The
|
|
192
|
+
* conversion to tokens is `contextTokens`' business, because that is where an estimate and a
|
|
193
|
+
* reported count have to be told apart.
|
|
194
|
+
*
|
|
195
|
+
* @param body The request as it will be sent, tools included.
|
|
196
|
+
*/
|
|
197
|
+
export function contextChars(body) {
|
|
198
|
+
const out = {
|
|
199
|
+
system: 0,
|
|
200
|
+
tools: toolsChars(body.tools ?? []),
|
|
201
|
+
history: 0,
|
|
202
|
+
toolResults: 0,
|
|
203
|
+
total: 0,
|
|
204
|
+
};
|
|
205
|
+
for (const message of body.messages) {
|
|
206
|
+
const chars = messageChars(message);
|
|
207
|
+
// Every system message and not just the leading one: a host that appends guidance, or a
|
|
208
|
+
// hook that injects a preface, has put more of the window there and wants to be told so.
|
|
209
|
+
if (message.role === "system" || message.role === "developer")
|
|
210
|
+
out.system += chars;
|
|
211
|
+
else if (message.role === "tool")
|
|
212
|
+
out.toolResults += chars;
|
|
213
|
+
else
|
|
214
|
+
out.history += chars;
|
|
215
|
+
}
|
|
216
|
+
out.total = out.system + out.tools + out.history + out.toolResults;
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Shares `total` out over these parts by their character counts, the largest absorbing the
|
|
221
|
+
* rounding so they add up to it exactly rather than to within a few tokens of it.
|
|
222
|
+
*/
|
|
223
|
+
function share(chars, over, total) {
|
|
224
|
+
const out = { system: 0, tools: 0, history: 0, toolResults: 0, total };
|
|
225
|
+
const measured = over.reduce((sum, part) => sum + chars[part], 0);
|
|
226
|
+
if (measured <= 0 || total <= 0)
|
|
227
|
+
return out;
|
|
228
|
+
const absorber = over.reduce((a, b) => (chars[b] > chars[a] ? b : a));
|
|
229
|
+
let assigned = 0;
|
|
230
|
+
for (const part of over) {
|
|
231
|
+
if (part === absorber)
|
|
232
|
+
continue;
|
|
233
|
+
out[part] = Math.round((chars[part] / measured) * total);
|
|
234
|
+
assigned += out[part];
|
|
235
|
+
}
|
|
236
|
+
out[absorber] = Math.max(0, total - assigned);
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* What each part of a request costs the window, in tokens, adding up to the whole.
|
|
241
|
+
*
|
|
242
|
+
* Shares rather than four independent estimates, because a readout whose parts do not add up to
|
|
243
|
+
* the total beside them is a readout nobody trusts. Nothing in the round trip reports anything
|
|
244
|
+
* finer than a prompt count — a completion says how many tokens it read and not a word about
|
|
245
|
+
* where they came from — so the proportions are an estimate whatever the total is.
|
|
246
|
+
*
|
|
247
|
+
* Without a reported count the total is `requestTokens`, and the tools are counted the way it and
|
|
248
|
+
* `TurnMetrics.toolSchemaTokens` count them rather than shared out, so the two agree by
|
|
249
|
+
* construction and an operator does not read one number for the tool block in the metrics and a
|
|
250
|
+
* different one here. With a reported count every part is a share of it, the tools included:
|
|
251
|
+
* that number is the server's, and the point of using it is that the parts sum to what was
|
|
252
|
+
* charged.
|
|
253
|
+
*
|
|
254
|
+
* @param body The request as it will be sent, tools included.
|
|
255
|
+
* @param options The divisor, and the reported prompt count when there is one.
|
|
256
|
+
*/
|
|
257
|
+
export function contextTokens(body, { charsPerToken, promptTokens } = {}) {
|
|
258
|
+
const chars = contextChars(body);
|
|
259
|
+
if (promptTokens !== undefined && promptTokens > 0)
|
|
260
|
+
return share(chars, PARTS, promptTokens);
|
|
261
|
+
const per = divisor(charsPerToken);
|
|
262
|
+
const tools = Math.ceil(chars.tools / per);
|
|
263
|
+
const rest = Math.ceil((chars.total - chars.tools) / per);
|
|
264
|
+
const out = share(chars, ["system", "history", "toolResults"], rest);
|
|
265
|
+
out.tools = tools;
|
|
266
|
+
out.total = rest + tools;
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
186
269
|
/**
|
|
187
270
|
* One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
|
|
188
271
|
*
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface EndpointSnapshot {
|
|
|
28
28
|
strictSchemas: boolean;
|
|
29
29
|
usageInStream: boolean;
|
|
30
30
|
models: Record<string, ModelSnapshot>;
|
|
31
|
+
/**
|
|
32
|
+
* When this endpoint was first met, as epoch milliseconds, so `expireCapabilities` measures a
|
|
33
|
+
* latch from when it was learned rather than from the boot that imported it. Absent in a snapshot
|
|
34
|
+
* taken before it was written, which reads as met now — the behaviour of every release until this
|
|
35
|
+
* one, and the reason no version bump is owed.
|
|
36
|
+
*/
|
|
37
|
+
since?: number;
|
|
31
38
|
}
|
|
32
39
|
/** Every latched refusal in the process, JSON-safe. See `exportCapabilities`. */
|
|
33
40
|
export interface CapabilitySnapshot {
|
|
@@ -43,7 +50,8 @@ export interface CapabilitySnapshot {
|
|
|
43
50
|
* Covers what `negotiate` latches on endpoints and models and the models `ask` found refusing the
|
|
44
51
|
* no-thinking hints. Only what was actually refused is in it, so a snapshot of a process that met
|
|
45
52
|
* no refusals has no endpoints. Endpoints are named by digest rather than URL and key, since the
|
|
46
|
-
* blob is meant to be written somewhere and a key must not be written with it.
|
|
53
|
+
* blob is meant to be written somewhere and a key must not be written with it. Each carries the
|
|
54
|
+
* `since` it was learned at, so importing it does not make an old latch young again.
|
|
47
55
|
*/
|
|
48
56
|
export declare function exportCapabilities(): CapabilitySnapshot;
|
|
49
57
|
/**
|
|
@@ -53,7 +61,8 @@ export declare function exportCapabilities(): CapabilitySnapshot;
|
|
|
53
61
|
* off whatever the snapshot says, and one the snapshot has off is turned off. A snapshot of another
|
|
54
62
|
* version, or anything that is not one, is ignored — a stale shape costs the refused requests it
|
|
55
63
|
* would have saved, which is what a restart cost before. How old is too old is the consumer's call,
|
|
56
|
-
* made on `savedAt` before importing,
|
|
64
|
+
* made on `savedAt` before importing, or afterwards per endpoint with `expireCapabilities`, since a
|
|
65
|
+
* server behind a URL can be upgraded between boots.
|
|
57
66
|
*
|
|
58
67
|
* @param snapshot What `exportCapabilities` returned, as stored. Read defensively: a field of the
|
|
59
68
|
* wrong type is skipped rather than trusted.
|
package/dist/snapshot.js
CHANGED
|
@@ -32,13 +32,21 @@ const refusedAnything = (model) => !model.reasoningEffort ||
|
|
|
32
32
|
* Covers what `negotiate` latches on endpoints and models and the models `ask` found refusing the
|
|
33
33
|
* no-thinking hints. Only what was actually refused is in it, so a snapshot of a process that met
|
|
34
34
|
* no refusals has no endpoints. Endpoints are named by digest rather than URL and key, since the
|
|
35
|
-
* blob is meant to be written somewhere and a key must not be written with it.
|
|
35
|
+
* blob is meant to be written somewhere and a key must not be written with it. Each carries the
|
|
36
|
+
* `since` it was learned at, so importing it does not make an old latch young again.
|
|
36
37
|
*/
|
|
37
38
|
export function exportCapabilities() {
|
|
38
39
|
const endpoints = {};
|
|
39
40
|
const entry = (id) => {
|
|
40
|
-
endpoints[id]
|
|
41
|
-
|
|
41
|
+
const held = endpoints[id];
|
|
42
|
+
if (held)
|
|
43
|
+
return held;
|
|
44
|
+
const fresh = { strictSchemas: true, usageInStream: true, models: {} };
|
|
45
|
+
const since = knownCapabilities().get(id)?.since;
|
|
46
|
+
if (since !== undefined)
|
|
47
|
+
fresh.since = since;
|
|
48
|
+
endpoints[id] = fresh;
|
|
49
|
+
return fresh;
|
|
42
50
|
};
|
|
43
51
|
for (const [id, supports] of knownCapabilities()) {
|
|
44
52
|
const models = {};
|
|
@@ -79,7 +87,8 @@ const isRecord = (value) => typeof value === "object" && value !== null && !Arra
|
|
|
79
87
|
* off whatever the snapshot says, and one the snapshot has off is turned off. A snapshot of another
|
|
80
88
|
* version, or anything that is not one, is ignored — a stale shape costs the refused requests it
|
|
81
89
|
* would have saved, which is what a restart cost before. How old is too old is the consumer's call,
|
|
82
|
-
* made on `savedAt` before importing,
|
|
90
|
+
* made on `savedAt` before importing, or afterwards per endpoint with `expireCapabilities`, since a
|
|
91
|
+
* server behind a URL can be upgraded between boots.
|
|
83
92
|
*
|
|
84
93
|
* @param snapshot What `exportCapabilities` returned, as stored. Read defensively: a field of the
|
|
85
94
|
* wrong type is skipped rather than trusted.
|
|
@@ -98,6 +107,11 @@ export function importCapabilities(snapshot) {
|
|
|
98
107
|
supports.strictSchemas = false;
|
|
99
108
|
if (endpoint.usageInStream === false)
|
|
100
109
|
supports.usageInStream = false;
|
|
110
|
+
// Older of the two, so a snapshot ages an entry and never rejuvenates one: importing must not
|
|
111
|
+
// be a way to keep a latch from ever reaching `expireCapabilities`.
|
|
112
|
+
if (typeof endpoint.since === "number" && endpoint.since < supports.since) {
|
|
113
|
+
supports.since = endpoint.since;
|
|
114
|
+
}
|
|
101
115
|
if (!isRecord(endpoint.models))
|
|
102
116
|
continue;
|
|
103
117
|
for (const [name, model] of Object.entries(endpoint.models)) {
|