@cubicecho/agent-core 2.24.0 → 2.25.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 +10 -7
- package/dist/agent-loop.d.ts +84 -205
- package/dist/agent-loop.js +288 -597
- package/dist/calibration.d.ts +14 -10
- package/dist/calibration.js +30 -23
- package/dist/capabilities.d.ts +111 -42
- package/dist/capabilities.js +281 -138
- package/dist/client.d.ts +104 -35
- package/dist/client.js +205 -74
- package/dist/compaction.d.ts +103 -60
- package/dist/compaction.js +160 -86
- package/dist/config.d.ts +48 -12
- package/dist/config.js +20 -1
- package/dist/continuation.d.ts +24 -16
- package/dist/continuation.js +70 -44
- package/dist/digest.d.ts +7 -0
- package/dist/digest.js +18 -0
- package/dist/errors.d.ts +24 -15
- package/dist/errors.js +23 -15
- package/dist/events.d.ts +100 -63
- package/dist/events.js +250 -120
- package/dist/guards.d.ts +35 -4
- package/dist/guards.js +38 -5
- package/dist/hook-events.d.ts +21 -5
- package/dist/hook-events.js +23 -9
- package/dist/hooks.d.ts +96 -73
- package/dist/hooks.js +202 -116
- package/dist/index.d.ts +30 -26
- package/dist/index.js +29 -24
- package/dist/ledger.d.ts +62 -30
- package/dist/ledger.js +163 -56
- package/dist/platform.d.ts +13 -0
- package/dist/platform.js +13 -0
- package/dist/preselect.d.ts +222 -0
- package/dist/preselect.js +290 -0
- package/dist/request-body.d.ts +32 -0
- package/dist/request-body.js +47 -0
- package/dist/reset.d.ts +1 -0
- package/dist/reset.js +7 -6
- package/dist/retry.d.ts +29 -138
- package/dist/retry.js +69 -282
- package/dist/run-calls.d.ts +123 -0
- package/dist/run-calls.js +351 -0
- package/dist/run-turn.d.ts +36 -15
- package/dist/run-turn.js +74 -37
- package/dist/runtime.d.ts +23 -17
- package/dist/runtime.js +34 -24
- package/dist/schema-compat.d.ts +35 -18
- package/dist/schema-compat.js +340 -175
- package/dist/scope.d.ts +28 -5
- package/dist/scope.js +37 -10
- package/dist/side-task.d.ts +123 -40
- package/dist/side-task.js +181 -63
- package/dist/snapshot.d.ts +10 -5
- package/dist/snapshot.js +73 -30
- package/dist/spec.d.ts +56 -25
- package/dist/spec.js +494 -215
- package/dist/stream.d.ts +66 -16
- package/dist/stream.js +220 -97
- package/dist/thinking.d.ts +23 -8
- package/dist/thinking.js +92 -44
- package/dist/tokens.d.ts +164 -6
- package/dist/tokens.js +319 -6
- package/dist/tool-calls.d.ts +82 -17
- package/dist/tool-calls.js +198 -83
- package/dist/tool-loading.d.ts +101 -189
- package/dist/tool-loading.js +277 -371
- package/dist/wire.d.ts +81 -0
- package/dist/wire.js +73 -0
- package/llms.txt +76 -39
- package/package.json +4 -4
package/dist/compaction.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ask } from
|
|
4
|
-
import {
|
|
1
|
+
import { HookEvent } from './hook-events.js';
|
|
2
|
+
import { consult, notify, textOf, turnMessages } from './hooks.js';
|
|
3
|
+
import { ask } from './side-task.js';
|
|
4
|
+
import { messageTokens } from './tokens.js';
|
|
5
|
+
import { holdsDefinitions } from './tool-loading.js';
|
|
6
|
+
import { FUNCTION_TOOL, Role } from './wire.js';
|
|
5
7
|
/** The fraction of the window in use before a summary is worth its own round trip. */
|
|
6
8
|
export const COMPACT_AT = 0.75;
|
|
7
9
|
/** The fraction of the window the kept tail may fill, leaving room for the run to grow again. */
|
|
@@ -10,45 +12,77 @@ export const KEEP_RATIO = 0.35;
|
|
|
10
12
|
const SUMMARY_TOKENS = 1024;
|
|
11
13
|
/** How much of any one message the summariser is shown. A pasted file is not worth it whole. */
|
|
12
14
|
const SUMMARY_SLICE = 4000;
|
|
15
|
+
/** The fewest messages worth folding: a summary of one costs a round trip and saves nothing. */
|
|
16
|
+
const FEWEST_FOLDED = 2;
|
|
17
|
+
/** The locale a cleared result's length is written in, so the stub reads the same on every host. */
|
|
18
|
+
const COUNT_LOCALE = 'en-US';
|
|
13
19
|
/**
|
|
14
20
|
* The summariser's instruction when the caller gives none.
|
|
15
21
|
*
|
|
22
|
+
* @remarks
|
|
16
23
|
* Asks for notes rather than a retelling, because what the summary replaces is the model's only
|
|
17
24
|
* record of what was decided, and a narrative spends its words on the order things happened in.
|
|
18
25
|
*/
|
|
19
|
-
export const SUMMARY_PROMPT =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
export const SUMMARY_PROMPT = 'You maintain the running memory of a long conversation. Rewrite the exchange below as ' +
|
|
27
|
+
'notes the assistant can rely on after the original messages are gone. Keep decisions, ' +
|
|
28
|
+
'facts, file paths, names, numbers, and anything still unresolved. Drop pleasantries and ' +
|
|
29
|
+
'anything already superseded. Write compact prose or bullets — no preamble, no sign-off.';
|
|
23
30
|
/**
|
|
24
31
|
* How a summary message opens, which is also how `planCompaction` knows one from a system prompt.
|
|
25
32
|
*/
|
|
26
|
-
export const SUMMARY_LEAD =
|
|
27
|
-
/**
|
|
33
|
+
export const SUMMARY_LEAD = 'Summary of the earlier part of this conversation, which is no longer shown in full:\n\n';
|
|
34
|
+
/**
|
|
35
|
+
* What the summariser reads for one message: its text and the calls it made.
|
|
36
|
+
*
|
|
37
|
+
* @param message - Any message of the transcript. Only its function calls are written out.
|
|
38
|
+
* @returns The text, then each call as `name(arguments)`, space-separated and trimmed. Empty for a
|
|
39
|
+
* message with neither. Not cut to length; `summaryInput` does that.
|
|
40
|
+
*/
|
|
28
41
|
const messageText = (message) => {
|
|
29
|
-
const calls =
|
|
42
|
+
const calls = 'tool_calls' in message && message.tool_calls
|
|
30
43
|
? message.tool_calls
|
|
31
|
-
.map((call) => call.type ===
|
|
32
|
-
.join(
|
|
33
|
-
:
|
|
44
|
+
.map((call) => (call.type === FUNCTION_TOOL ? `${call.function.name}(${call.function.arguments})` : ''))
|
|
45
|
+
.join(' ')
|
|
46
|
+
: '';
|
|
34
47
|
return `${textOf(message.content)} ${calls}`.trim();
|
|
35
48
|
};
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Whether a message is a summary an earlier fold left, rather than a system prompt.
|
|
51
|
+
*
|
|
52
|
+
* @param message - Any message of the transcript.
|
|
53
|
+
* @returns `true` for a `system` message whose text opens with `SUMMARY_LEAD`, which is what
|
|
54
|
+
* `summaryMessage` writes.
|
|
55
|
+
*/
|
|
56
|
+
const isSummary = (message) => message.role === Role.System && textOf(message.content).startsWith(SUMMARY_LEAD);
|
|
57
|
+
/**
|
|
58
|
+
* The summary as it sits in a transcript, which is the one shape `isSummary` recognises again.
|
|
59
|
+
*
|
|
60
|
+
* @param summary - The notes, without the lead. Trimmed here.
|
|
61
|
+
* @returns A new `system` message: `SUMMARY_LEAD`, then the notes.
|
|
62
|
+
*/
|
|
38
63
|
const summaryMessage = (summary) => ({
|
|
39
|
-
role:
|
|
64
|
+
role: Role.System,
|
|
40
65
|
content: `${SUMMARY_LEAD}${summary.trim()}`,
|
|
41
66
|
});
|
|
42
67
|
/**
|
|
43
68
|
* The leading system messages a fold never touches, and the summary an earlier one left among
|
|
44
|
-
* them.
|
|
69
|
+
* them.
|
|
70
|
+
*
|
|
71
|
+
* @param messages - The transcript. Not written to.
|
|
72
|
+
* @returns `from` is the index of the first message that is not `system` — the length when all of
|
|
73
|
+
* them are. `previous` is the text of the last summary among those ahead of it, lead removed, and
|
|
74
|
+
* absent when there is none.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* What a caller who keeps its system prompt out of the array knows already, and passes.
|
|
45
78
|
*/
|
|
46
79
|
const systemHead = (messages) => {
|
|
47
80
|
let from = 0;
|
|
48
81
|
let previous;
|
|
49
|
-
while (from < messages.length && messages[from].role ===
|
|
50
|
-
if (isSummary(messages[from]))
|
|
82
|
+
while (from < messages.length && messages[from].role === Role.System) {
|
|
83
|
+
if (isSummary(messages[from])) {
|
|
51
84
|
previous = textOf(messages[from].content).slice(SUMMARY_LEAD.length);
|
|
85
|
+
}
|
|
52
86
|
from++;
|
|
53
87
|
}
|
|
54
88
|
return { from, previous };
|
|
@@ -56,6 +90,12 @@ const systemHead = (messages) => {
|
|
|
56
90
|
/**
|
|
57
91
|
* The transcript with every tool result but the latest few replaced by a one-line stub.
|
|
58
92
|
*
|
|
93
|
+
* @param messages - The transcript. Not written to.
|
|
94
|
+
* @param [options] - How many results to keep and how long one must be to clear.
|
|
95
|
+
* @returns `messages` itself when nothing was cleared, otherwise a new array in which each cleared
|
|
96
|
+
* result is a new message and every other one the same object.
|
|
97
|
+
*
|
|
98
|
+
* @remarks
|
|
59
99
|
* The cheap half of compaction. A `read_file` of a 40k-character file is 10k tokens on every turn
|
|
60
100
|
* after it, and by then the model has usually taken what it wanted from it; the stub keeps the
|
|
61
101
|
* call answered — a call with no result is a malformed transcript — and says how much was there,
|
|
@@ -64,39 +104,54 @@ const systemHead = (messages) => {
|
|
|
64
104
|
* only copy of them the model has (`holdsDefinitions`); either still counts as one of the latest
|
|
65
105
|
* few. Returns the same array when there was nothing to clear. Rewrites the prefix;
|
|
66
106
|
* see the module comment on when to run it.
|
|
67
|
-
*
|
|
68
|
-
* @param messages The transcript. Not written to.
|
|
69
|
-
* @param options How many results to keep and how long one must be to clear.
|
|
70
107
|
*/
|
|
71
108
|
export function pruneToolResults(messages, { keepLast = 5, maxChars = 256 } = {}) {
|
|
72
109
|
let kept = 0;
|
|
73
110
|
let out;
|
|
74
111
|
for (let at = messages.length - 1; at >= 0; at--) {
|
|
75
112
|
const message = messages[at];
|
|
76
|
-
if (message.role !==
|
|
113
|
+
if (message.role !== Role.Tool) {
|
|
77
114
|
continue;
|
|
78
|
-
|
|
115
|
+
}
|
|
116
|
+
if (kept++ < keepLast) {
|
|
79
117
|
continue;
|
|
118
|
+
}
|
|
80
119
|
const text = textOf(message.content);
|
|
81
|
-
if (text.length <= maxChars || text.startsWith(
|
|
120
|
+
if (text.length <= maxChars || text.startsWith('[result cleared')) {
|
|
82
121
|
continue;
|
|
122
|
+
}
|
|
83
123
|
// A proxied load's result is the schema itself; stubbed, the tool is one the model can still
|
|
84
124
|
// name and no longer call correctly.
|
|
85
|
-
if (holdsDefinitions(text))
|
|
125
|
+
if (holdsDefinitions(text)) {
|
|
86
126
|
continue;
|
|
127
|
+
}
|
|
87
128
|
out ??= [...messages];
|
|
88
129
|
out[at] = {
|
|
89
130
|
...message,
|
|
90
|
-
content: `[result cleared, ${text.length.toLocaleString(
|
|
131
|
+
content: `[result cleared, ${text.length.toLocaleString(COUNT_LOCALE)} chars]`,
|
|
91
132
|
};
|
|
92
133
|
}
|
|
93
134
|
return out ?? messages;
|
|
94
135
|
}
|
|
95
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* An index held between the start of the transcript and `most`, for a caller's `from` that is
|
|
138
|
+
* neither.
|
|
139
|
+
*
|
|
140
|
+
* @param index - The index as it was given, which may be negative or past the end.
|
|
141
|
+
* @param most - The highest index allowed, itself included.
|
|
142
|
+
* @returns `index` where it lies from zero to `most`, otherwise the nearer of the two.
|
|
143
|
+
*/
|
|
96
144
|
const clamp = (index, most) => Math.min(Math.max(index, 0), most);
|
|
97
145
|
/**
|
|
98
146
|
* Where to fold a transcript that has grown into its window, or `undefined` when it should not be.
|
|
99
147
|
*
|
|
148
|
+
* @param messages - The transcript, system prompts included if the caller keeps them in it.
|
|
149
|
+
* @param options - The window, what is in use, the ratios, and where the last fold ended. See
|
|
150
|
+
* `CompactionOptions`.
|
|
151
|
+
* @returns The plan, its `toSummarise` a new array of the same message objects, or `undefined` when
|
|
152
|
+
* there is nothing to fold. `messages` is not written to.
|
|
153
|
+
*
|
|
154
|
+
* @remarks
|
|
100
155
|
* The kept tail is walked back from the end until it fills `keepRatio` of the window, then moved
|
|
101
156
|
* forward onto a user message: a transcript resuming mid-exchange — a tool result with no call
|
|
102
157
|
* before it, a reply with no question — is malformed and servers refuse it. The system prompts at
|
|
@@ -117,17 +172,15 @@ const clamp = (index, most) => Math.min(Math.max(index, 0), most);
|
|
|
117
172
|
* comes, and `after` says how far short it fell. An earlier summary the new one replaces is not
|
|
118
173
|
* credited, so `after` errs high by at most that much. It is only as good as `estimate` — see
|
|
119
174
|
* `estimateFrom` for one that is measured.
|
|
120
|
-
*
|
|
121
|
-
* @param messages The transcript, system prompts included if the caller keeps them in it.
|
|
122
|
-
* @param options The window, what is in use, the ratios, and where the last fold ended. See
|
|
123
|
-
* `CompactionOptions`.
|
|
124
175
|
*/
|
|
125
176
|
export function planCompaction(messages, { limit, used, compactAt = COMPACT_AT, keepRatio = KEEP_RATIO, target, summaryTokens = SUMMARY_TOKENS, charsPerToken, estimate = (message) => messageTokens(message, { charsPerToken }), from: givenFrom, previous: givenPrevious, }) {
|
|
126
|
-
if (!(limit > 0))
|
|
177
|
+
if (!(limit > 0)) {
|
|
127
178
|
return undefined;
|
|
179
|
+
}
|
|
128
180
|
const cost = used ?? messages.reduce((total, message) => total + estimate(message), 0);
|
|
129
|
-
if (cost < limit * compactAt)
|
|
181
|
+
if (cost < limit * compactAt) {
|
|
130
182
|
return undefined;
|
|
183
|
+
}
|
|
131
184
|
const head = systemHead(messages);
|
|
132
185
|
const from = clamp(givenFrom ?? head.from, messages.length);
|
|
133
186
|
const previous = givenPrevious ?? head.previous;
|
|
@@ -137,11 +190,12 @@ export function planCompaction(messages, { limit, used, compactAt = COMPACT_AT,
|
|
|
137
190
|
// Forward from the head, so the first cut that reaches the target is the one that folds least.
|
|
138
191
|
let folded = 0;
|
|
139
192
|
for (let at = from; at < messages.length; at++) {
|
|
140
|
-
if (messages[at].role ===
|
|
193
|
+
if (messages[at].role === Role.User) {
|
|
141
194
|
cut = at;
|
|
142
195
|
after = cost - folded + summaryTokens;
|
|
143
|
-
if (after <= limit * target)
|
|
196
|
+
if (after <= limit * target) {
|
|
144
197
|
break;
|
|
198
|
+
}
|
|
145
199
|
}
|
|
146
200
|
folded += estimate(messages[at]);
|
|
147
201
|
}
|
|
@@ -151,15 +205,18 @@ export function planCompaction(messages, { limit, used, compactAt = COMPACT_AT,
|
|
|
151
205
|
let kept = 0;
|
|
152
206
|
for (let at = messages.length - 1; at > from; at--) {
|
|
153
207
|
kept += estimate(messages[at]);
|
|
154
|
-
if (kept > budget)
|
|
208
|
+
if (kept > budget) {
|
|
155
209
|
break;
|
|
210
|
+
}
|
|
156
211
|
cut = at;
|
|
157
212
|
}
|
|
158
|
-
while (cut < messages.length && messages[cut].role !==
|
|
213
|
+
while (cut < messages.length && messages[cut].role !== Role.User) {
|
|
159
214
|
cut++;
|
|
215
|
+
}
|
|
160
216
|
}
|
|
161
|
-
if (cut >= messages.length || cut - from <
|
|
217
|
+
if (cut >= messages.length || cut - from < FEWEST_FOLDED) {
|
|
162
218
|
return undefined;
|
|
219
|
+
}
|
|
163
220
|
return {
|
|
164
221
|
from,
|
|
165
222
|
cut,
|
|
@@ -172,16 +229,18 @@ export function planCompaction(messages, { limit, used, compactAt = COMPACT_AT,
|
|
|
172
229
|
* What the summariser is handed for a plan: the earlier summary if there was one, then each
|
|
173
230
|
* message as its role and at most 4000 characters of its text.
|
|
174
231
|
*
|
|
175
|
-
* @param plan What `planCompaction` returned.
|
|
232
|
+
* @param plan - What `planCompaction` returned.
|
|
233
|
+
* @returns The text for the summariser, messages a blank line apart. A message with neither text
|
|
234
|
+
* nor calls is left out, and a plan with a `previous` opens with it under `Notes so far:`.
|
|
176
235
|
*/
|
|
177
236
|
export function summaryInput(plan) {
|
|
178
237
|
const transcript = plan.toSummarise
|
|
179
238
|
.map((message) => {
|
|
180
239
|
const text = messageText(message);
|
|
181
|
-
return text ? `${message.role}: ${text.slice(0, SUMMARY_SLICE)}` :
|
|
240
|
+
return text ? `${message.role}: ${text.slice(0, SUMMARY_SLICE)}` : '';
|
|
182
241
|
})
|
|
183
242
|
.filter(Boolean)
|
|
184
|
-
.join(
|
|
243
|
+
.join('\n\n');
|
|
185
244
|
return plan.previous
|
|
186
245
|
? `Notes so far:\n${plan.previous}\n\nContinue them with this exchange:\n\n${transcript}`
|
|
187
246
|
: transcript;
|
|
@@ -189,26 +248,29 @@ export function summaryInput(plan) {
|
|
|
189
248
|
/**
|
|
190
249
|
* A summariser that asks `model` with `SUMMARY_PROMPT`, for `compactTranscript`.
|
|
191
250
|
*
|
|
192
|
-
* @param config The endpoint the summary is written through.
|
|
193
|
-
* @param model The model to write it, which may be a smaller one than the run's.
|
|
194
|
-
* @param options Cancellation and notices
|
|
195
|
-
*
|
|
251
|
+
* @param config - The endpoint the summary is written through.
|
|
252
|
+
* @param model - The model to write it, which may be a smaller one than the run's.
|
|
253
|
+
* @param [options] - Cancellation and notices, and the instruction and ceiling the summary is
|
|
254
|
+
* written under.
|
|
255
|
+
* @returns A function from `summaryInput`'s text to the summary. It rejects as `ask` does, and
|
|
256
|
+
* resolves with an empty string where the model wrote nothing.
|
|
196
257
|
*/
|
|
197
258
|
export const summariser = (config, model, { system = SUMMARY_PROMPT, maxTokens = SUMMARY_TOKENS, ...options } = {}) => (text) => ask(config, model, system, text, { maxTokens, ...options });
|
|
198
259
|
/**
|
|
199
260
|
* The hooks and the summariser for a plan, as a record to store rather than a transcript to send.
|
|
200
261
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* @param messages The transcript the plan was made for. Read only, and only for the hooks.
|
|
206
|
-
* @param plan What `planCompaction` returned for it.
|
|
207
|
-
* @param summarise Writes the summary from `summaryInput`'s text. See `summariser`. Not called
|
|
262
|
+
* @param messages - The transcript the plan was made for. Read only, and only for the hooks.
|
|
263
|
+
* @param plan - What `planCompaction` returned for it.
|
|
264
|
+
* @param summarise - Writes the summary from `summaryInput`'s text. See `summariser`. Not called
|
|
208
265
|
* when a hook vetoes.
|
|
209
|
-
* @param options Hooks to tell and whether the window is already past. See `CompactionRunOptions`.
|
|
266
|
+
* @param [options] - Hooks to tell and whether the window is already past. See `CompactionRunOptions`.
|
|
210
267
|
* @returns `undefined` when nothing was folded — a hook vetoed, or the summary came back empty —
|
|
211
268
|
* so the caller stores nothing and the transcript is still whole.
|
|
269
|
+
*
|
|
270
|
+
* @remarks
|
|
271
|
+
* What `compactTranscript` does before it rewrites anything, which is all a host needs when the
|
|
272
|
+
* fold lives on the session row and the messages stay where they are. Nothing here is persisted or
|
|
273
|
+
* logged — that is the host's, and so is deciding what to do with a fold that did not happen.
|
|
212
274
|
*/
|
|
213
275
|
export async function runCompaction(messages, plan, summarise, { hooks, forced = false } = {}) {
|
|
214
276
|
const context = hooks && {
|
|
@@ -218,40 +280,46 @@ export async function runCompaction(messages, plan, summarise, { hooks, forced =
|
|
|
218
280
|
};
|
|
219
281
|
let summary;
|
|
220
282
|
if (hooks && context && hooks.honourVeto && !forced) {
|
|
221
|
-
const { vetoed } = await consult(hooks.run,
|
|
222
|
-
if (vetoed)
|
|
283
|
+
const { vetoed } = await consult(hooks.run, HookEvent.BeforeCompact, context, hooks.onNote);
|
|
284
|
+
if (vetoed) {
|
|
223
285
|
return undefined;
|
|
286
|
+
}
|
|
224
287
|
summary = await summarise(summaryInput(plan));
|
|
225
288
|
}
|
|
226
289
|
else {
|
|
227
290
|
[summary] = await Promise.all([
|
|
228
291
|
summarise(summaryInput(plan)),
|
|
229
|
-
hooks && context && notify(hooks.run,
|
|
292
|
+
hooks && context && notify(hooks.run, HookEvent.BeforeCompact, context, hooks.onNote),
|
|
230
293
|
]);
|
|
231
294
|
}
|
|
232
|
-
if (!summary.trim())
|
|
295
|
+
if (!summary.trim()) {
|
|
233
296
|
return undefined;
|
|
297
|
+
}
|
|
234
298
|
return { summary: summary.trim(), through: plan.cut, at: new Date().toISOString() };
|
|
235
299
|
}
|
|
236
300
|
/**
|
|
237
301
|
* The transcript as the server should see it: the folded head replaced by its summary.
|
|
238
302
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* continues these notes rather than summarising them a second time. Any earlier summary message in
|
|
242
|
-
* the kept head is dropped, since the record's already contains it.
|
|
243
|
-
*
|
|
244
|
-
* @param messages The stored transcript, whole. Not written to.
|
|
245
|
-
* @param record The fold, or `undefined` for a session that has not been compacted, which hands
|
|
303
|
+
* @param messages - The stored transcript, whole. Not written to.
|
|
304
|
+
* @param [record] - The fold, or `undefined` for a session that has not been compacted, which hands
|
|
246
305
|
* back `messages` itself.
|
|
247
|
-
* @param options `from` is the first message the fold was allowed to take — the plan's, for a host
|
|
306
|
+
* @param [options] - `from` is the first message the fold was allowed to take — the plan's, for a host
|
|
248
307
|
* that keeps its system prompts in the array; everything before it is kept ahead of the summary.
|
|
249
308
|
* Absent, the leading `system` messages are found by scanning, and zero of them is the ordinary
|
|
250
309
|
* case for a host whose system prompt is a separate argument.
|
|
310
|
+
* @returns `messages` itself when there is no record or its summary is blank, otherwise a new
|
|
311
|
+
* array: the kept head, one summary message, then everything from the record's `through` on.
|
|
312
|
+
*
|
|
313
|
+
* @remarks
|
|
314
|
+
* The inverse of storing a `CompactionRecord`, and the shape `planCompaction` expects to meet
|
|
315
|
+
* again — the same `SUMMARY_LEAD`, in a `system` message at the same place — so the next fold
|
|
316
|
+
* continues these notes rather than summarising them a second time. Any earlier summary message in
|
|
317
|
+
* the kept head is dropped, since the record's already contains it.
|
|
251
318
|
*/
|
|
252
319
|
export function applyCompaction(messages, record, { from } = {}) {
|
|
253
|
-
if (!record?.summary.trim())
|
|
320
|
+
if (!record?.summary.trim()) {
|
|
254
321
|
return messages;
|
|
322
|
+
}
|
|
255
323
|
const head = clamp(from ?? systemHead(messages).from, record.through);
|
|
256
324
|
return [
|
|
257
325
|
...messages.slice(0, head).filter((message) => !isSummary(message)),
|
|
@@ -263,26 +331,40 @@ export function applyCompaction(messages, record, { from } = {}) {
|
|
|
263
331
|
* Where a stored index sits in the request `applyCompaction` builds, once a fold has shifted
|
|
264
332
|
* everything after it.
|
|
265
333
|
*
|
|
334
|
+
* @param index - The position in the stored transcript.
|
|
335
|
+
* @param [record] - The fold in force, or `undefined` for a session that has none, which hands the
|
|
336
|
+
* index straight back.
|
|
337
|
+
* @param [head] - How many messages the request keeps ahead of the summary — the leading system
|
|
338
|
+
* prompts, when the host keeps them in the array. Zero is the stored-fold case,
|
|
339
|
+
* where the summary is the request's first message.
|
|
340
|
+
* @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.
|
|
342
|
+
*
|
|
343
|
+
* @remarks
|
|
266
344
|
* A transcript that stays append-only and a request that does not are two numberings of the same
|
|
267
345
|
* conversation, and anything that names a position — `withContext`'s index, a range handed to a
|
|
268
346
|
* hook — has to say which it is in. An index inside the folded stretch answers with the summary
|
|
269
347
|
* message that now stands for it.
|
|
270
|
-
*
|
|
271
|
-
* @param index The position in the stored transcript.
|
|
272
|
-
* @param record The fold in force, or `undefined` for a session that has none, which hands the
|
|
273
|
-
* index straight back.
|
|
274
|
-
* @param head How many messages the request keeps ahead of the summary — the leading system
|
|
275
|
-
* prompts, when the host keeps them in the array. Zero, the default, is the stored-fold case,
|
|
276
|
-
* where the summary is the request's first message.
|
|
277
348
|
*/
|
|
278
349
|
export const requestIndex = (index, record, head = 0) => {
|
|
279
|
-
if (!record)
|
|
350
|
+
if (!record) {
|
|
280
351
|
return index;
|
|
352
|
+
}
|
|
281
353
|
return index < record.through ? head : index - record.through + head + 1;
|
|
282
354
|
};
|
|
283
355
|
/**
|
|
284
356
|
* The transcript with the plan's stretch replaced by one system message holding its summary.
|
|
285
357
|
*
|
|
358
|
+
* @param messages - The transcript the plan was made for. Not written to.
|
|
359
|
+
* @param plan - What `planCompaction` returned for it.
|
|
360
|
+
* @param summarise - Writes the summary from `summaryInput`'s text. See `summariser`. Not called
|
|
361
|
+
* when a hook vetoes.
|
|
362
|
+
* @param [options] - Hooks to tell and whether the window is already past. See
|
|
363
|
+
* `CompactionRunOptions`.
|
|
364
|
+
* @returns `messages` itself when nothing was folded — a veto or an empty summary — otherwise a
|
|
365
|
+
* new array.
|
|
366
|
+
*
|
|
367
|
+
* @remarks
|
|
286
368
|
* `beforeCompact` is told what is being folded while the summary is written, beside it rather
|
|
287
369
|
* than ahead of it — a memory server filing it is not a rescue worth making the run wait for, and
|
|
288
370
|
* `notify` never rejects. A host that wants its hooks able to stop a compaction sets
|
|
@@ -296,19 +378,11 @@ export const requestIndex = (index, record, head = 0) => {
|
|
|
296
378
|
* `runCompaction` and `applyCompaction` are its two halves, and it is nothing but the two in
|
|
297
379
|
* order, so a host that stores the fold instead of the array gets the same summary at the same
|
|
298
380
|
* cut rather than a second implementation that drifts from this one.
|
|
299
|
-
*
|
|
300
|
-
* @param messages The transcript the plan was made for. Not written to.
|
|
301
|
-
* @param plan What `planCompaction` returned for it.
|
|
302
|
-
* @param summarise Writes the summary from `summaryInput`'s text. See `summariser`. Not called
|
|
303
|
-
* when a hook vetoes.
|
|
304
|
-
* @param options Hooks to tell and whether the window is already past. See
|
|
305
|
-
* `CompactionRunOptions`.
|
|
306
|
-
* @returns `messages` itself when nothing was folded — a veto or an empty summary — otherwise a
|
|
307
|
-
* new array.
|
|
308
381
|
*/
|
|
309
382
|
export async function compactTranscript(messages, plan, summarise, options = {}) {
|
|
310
383
|
const record = await runCompaction(messages, plan, summarise, options);
|
|
311
|
-
if (!record)
|
|
384
|
+
if (!record) {
|
|
312
385
|
return messages;
|
|
386
|
+
}
|
|
313
387
|
return applyCompaction(messages, record, { from: plan.from });
|
|
314
388
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface Endpoint {
|
|
|
21
21
|
* How long an endpoint may go quiet. Zero, less, or absent means no limit — what a local model
|
|
22
22
|
* answering slowly needs.
|
|
23
23
|
*
|
|
24
|
+
* @remarks
|
|
24
25
|
* Not the whole request, which may take as long as the model keeps talking. On a streamed turn
|
|
25
26
|
* it is the silence allowed between chunks, the idle watchdog; the wait for the first chunk is
|
|
26
27
|
* `firstTokenSeconds`. On a call that does not stream, a side task or a model listing, it is
|
|
@@ -33,9 +34,11 @@ export interface Endpoint {
|
|
|
33
34
|
*/
|
|
34
35
|
requestTimeoutSeconds?: number;
|
|
35
36
|
/**
|
|
36
|
-
* How long a streamed turn may wait for its first chunk.
|
|
37
|
-
* `requestTimeoutSeconds`; zero or less is no limit.
|
|
37
|
+
* How long a streamed turn may wait for its first chunk. Zero or less is no limit.
|
|
38
38
|
*
|
|
39
|
+
* @defaultValue `FIRST_TOKEN_FACTOR` times `requestTimeoutSeconds`
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
39
42
|
* Its own number because the first wait is prefill, and on a local server prefill of a long
|
|
40
43
|
* prompt is tens of seconds on a GPU and minutes on a CPU, where the gap between tokens is a
|
|
41
44
|
* fraction of a second. A server loading the model on demand, Ollama after `keep_alive` or LM
|
|
@@ -45,6 +48,28 @@ export interface Endpoint {
|
|
|
45
48
|
*/
|
|
46
49
|
firstTokenSeconds?: number;
|
|
47
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* What makes an endpoint the one it is: where it lives and whose key reaches it.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* The narrow half of `Endpoint`, for everything that only has to tell two endpoints apart — the
|
|
56
|
+
* client pool, the capability latches, a side task borrowing the agent's key. The timeouts are
|
|
57
|
+
* not in it, since a different patience is not a different server.
|
|
58
|
+
*/
|
|
59
|
+
export interface EndpointIdentity {
|
|
60
|
+
/** The base URL as given. Compared as written, so a trailing slash is another endpoint. */
|
|
61
|
+
baseUrl: string;
|
|
62
|
+
/** Absent and empty mean the same: no key, which `endpointKey` reads as `NO_KEY`. */
|
|
63
|
+
apiKey?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The request fields `extraBody` may not set, because the loop's request is built around them.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* Here beside `extraBody` rather than with `buildBody`, which is what enforces it, because `spec`
|
|
70
|
+
* says so on import as well and has to stay loadable in a browser.
|
|
71
|
+
*/
|
|
72
|
+
export declare const RESERVED_BODY_FIELDS: readonly string[];
|
|
48
73
|
/** What to ask the model for. */
|
|
49
74
|
export interface ModelParams {
|
|
50
75
|
model: string;
|
|
@@ -60,6 +85,7 @@ export interface ModelParams {
|
|
|
60
85
|
/**
|
|
61
86
|
* Request fields this interface cannot spell, merged into the body last by `buildBody`.
|
|
62
87
|
*
|
|
88
|
+
* @remarks
|
|
63
89
|
* What a model card asks for and nothing here names: `top_k`, `min_p`, `repeat_penalty` —
|
|
64
90
|
* what actually stops a small model looping — and a server's own fields, such as llama.cpp's
|
|
65
91
|
* `id_slot`, which pins a session to one slot of a `--parallel` server so its KV cache is
|
|
@@ -71,15 +97,24 @@ export interface ModelParams {
|
|
|
71
97
|
*/
|
|
72
98
|
extraBody?: Record<string, unknown>;
|
|
73
99
|
}
|
|
74
|
-
/** How
|
|
75
|
-
export
|
|
100
|
+
/** How tool definitions reach the model. See `tool-loading.ts`. */
|
|
101
|
+
export declare const ToolDiscovery: {
|
|
102
|
+
/** Every tool definition, on every request. */
|
|
103
|
+
readonly Eager: 'eager';
|
|
104
|
+
/** A name-only catalogue, from which the model pulls in the schemas it needs. */
|
|
105
|
+
readonly OnDemand: 'ondemand';
|
|
76
106
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* behind a tool array that never changes — `load_tools` and `call_tool` — for a server whose
|
|
80
|
-
* prompt cache a growing tool array throws away. See `tool-loading.ts`.
|
|
107
|
+
* The same catalogue behind a tool array that never changes — `load_tools` and `call_tool` —
|
|
108
|
+
* for a server whose prompt cache a growing tool array throws away.
|
|
81
109
|
*/
|
|
82
|
-
|
|
110
|
+
readonly Proxy: 'proxy';
|
|
111
|
+
};
|
|
112
|
+
/** Any one of the modes in `ToolDiscovery`. */
|
|
113
|
+
export type ToolDiscovery = (typeof ToolDiscovery)[keyof typeof ToolDiscovery];
|
|
114
|
+
/** How tools reach the model, and how long it may keep calling them. */
|
|
115
|
+
export interface ToolPolicy {
|
|
116
|
+
/** How tool definitions reach the model. */
|
|
117
|
+
toolDiscovery: ToolDiscovery;
|
|
83
118
|
/** The model that does the preselection pass. Empty means don't preselect. */
|
|
84
119
|
toolSelectModel: string;
|
|
85
120
|
/** Hard stop on runaway tool loops. */
|
|
@@ -98,9 +133,10 @@ export interface RetryPolicy {
|
|
|
98
133
|
/**
|
|
99
134
|
* A whole agent configuration — every part, plus the two fields that belong to no group.
|
|
100
135
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* Provided for callers that want one name for the lot. No function in this package asks for it:
|
|
138
|
+
* they take the parts, so a caller missing `contextLength` can still use all of them bar the
|
|
139
|
+
* window guard. `ResolvedAgent` extends it, which is how a resolved spec satisfies every one.
|
|
104
140
|
*/
|
|
105
141
|
export interface AgentConfig extends Endpoint, ModelParams, ToolPolicy, RetryPolicy {
|
|
106
142
|
/** The agent's own standing instruction, if it has one. */
|
package/dist/config.js
CHANGED
|
@@ -11,4 +11,23 @@
|
|
|
11
11
|
* it structurally. Nothing imports a config *type* from a consumer, and no consumer has to grow
|
|
12
12
|
* a field it has no use for in order to call `getClient`.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The request fields `extraBody` may not set, because the loop's request is built around them.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* Here beside `extraBody` rather than with `buildBody`, which is what enforces it, because `spec`
|
|
19
|
+
* says so on import as well and has to stay loadable in a browser.
|
|
20
|
+
*/
|
|
21
|
+
export const RESERVED_BODY_FIELDS = ['model', 'messages', 'stream', 'tools'];
|
|
22
|
+
/** How tool definitions reach the model. See `tool-loading.ts`. */
|
|
23
|
+
export const ToolDiscovery = {
|
|
24
|
+
/** Every tool definition, on every request. */
|
|
25
|
+
Eager: 'eager',
|
|
26
|
+
/** A name-only catalogue, from which the model pulls in the schemas it needs. */
|
|
27
|
+
OnDemand: 'ondemand',
|
|
28
|
+
/**
|
|
29
|
+
* The same catalogue behind a tool array that never changes — `load_tools` and `call_tool` —
|
|
30
|
+
* for a server whose prompt cache a growing tool array throws away.
|
|
31
|
+
*/
|
|
32
|
+
Proxy: 'proxy',
|
|
33
|
+
};
|
package/dist/continuation.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type OpenAI from
|
|
2
|
-
import { type Capabilities } from
|
|
3
|
-
import { type RequestBuilder, type RunTurnOptions } from
|
|
4
|
-
import type
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
|
+
import { type Capabilities } from './capabilities.ts';
|
|
3
|
+
import { type RequestBuilder, type RunTurnOptions } from './run-turn.ts';
|
|
4
|
+
import { type Turn } from './stream.ts';
|
|
5
5
|
/**
|
|
6
6
|
* Picking up an answer the token ceiling cut off, instead of keeping half of it.
|
|
7
7
|
*
|
|
@@ -13,8 +13,10 @@ import type { Turn } from "./stream.ts";
|
|
|
13
13
|
/** What `continueTurn` takes besides what `runTurn` does. */
|
|
14
14
|
export interface ContinueTurnOptions extends RunTurnOptions {
|
|
15
15
|
/**
|
|
16
|
-
* How many more requests one answer may be given
|
|
17
|
-
*
|
|
16
|
+
* How many more requests one answer may be given; zero continues nothing. The cap is what
|
|
17
|
+
* stops a model that never reaches a stop token from looping on the ceiling.
|
|
18
|
+
*
|
|
19
|
+
* @defaultValue `1`
|
|
18
20
|
*/
|
|
19
21
|
maxContinuations?: number;
|
|
20
22
|
}
|
|
@@ -22,18 +24,32 @@ export interface ContinueTurnOptions extends RunTurnOptions {
|
|
|
22
24
|
* Whether a turn is one a continuation can finish: cut off at the ceiling, with an answer begun
|
|
23
25
|
* and no tool call in it.
|
|
24
26
|
*
|
|
27
|
+
* @param turn - The turn as it came back.
|
|
28
|
+
* @returns False for a turn that stopped for any reason but `length`, whose content is blank, or
|
|
29
|
+
* that holds a tool call.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
25
32
|
* An answer not begun is a turn cut off in its scratchpad, and prefilling a half-closed fence is
|
|
26
33
|
* the template's business rather than something this can do the same way everywhere — llama.cpp
|
|
27
34
|
* refuses a prefill outright on a template with thinking enabled. A tool call is excluded because
|
|
28
35
|
* its arguments are what was cut, and `parseToolArguments` already reports that truncation.
|
|
29
|
-
*
|
|
30
|
-
* @param turn The turn as it came back.
|
|
31
36
|
*/
|
|
32
37
|
export declare const isContinuable: (turn: Turn) => boolean;
|
|
33
38
|
/**
|
|
34
39
|
* Carries on an answer the token ceiling cut off, by sending the transcript again with the answer
|
|
35
40
|
* so far as a trailing assistant message, and joins the pieces into one turn.
|
|
36
41
|
*
|
|
42
|
+
* @param client - The pooled client for this endpoint.
|
|
43
|
+
* @param supports - What the endpoint has already refused.
|
|
44
|
+
* @param request - Builds the body the cut-off turn was sent, exactly as `runTurn` was given it. The
|
|
45
|
+
* prefill is appended to what it builds.
|
|
46
|
+
* @param turn - The turn that came back cut off.
|
|
47
|
+
* @param [options] - `runTurn`'s options, with `model` needed for the latch — without one nothing is
|
|
48
|
+
* latched and each continuation finds out again — and the cap on continuations.
|
|
49
|
+
* @returns `turn` itself where nothing was joined to it; otherwise a new turn, with the last
|
|
50
|
+
* continuation's `finishReason` and tool calls.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
37
53
|
* Only a turn `isContinuable` accepts is continued; any other comes back as it was. Content and
|
|
38
54
|
* reasoning are joined in order, the tool calls a continuation makes are kept, and usage is summed
|
|
39
55
|
* across the requests with `continuations` counting them. The continuation is read as starting in
|
|
@@ -47,13 +63,5 @@ export declare const isContinuable: (turn: Turn) => boolean;
|
|
|
47
63
|
* that check is only as good as a restart being word for word. Either way the answer so far is
|
|
48
64
|
* kept, with a notice. So is it when the continuation fails any other way, since the tokens
|
|
49
65
|
* already in hand are worth more than the error; only a stop is thrown.
|
|
50
|
-
*
|
|
51
|
-
* @param client The pooled client for this endpoint.
|
|
52
|
-
* @param supports What the endpoint has already refused.
|
|
53
|
-
* @param request Builds the body the cut-off turn was sent, exactly as `runTurn` was given it. The
|
|
54
|
-
* prefill is appended to what it builds.
|
|
55
|
-
* @param turn The turn that came back cut off.
|
|
56
|
-
* @param options `runTurn`'s options, with `model` needed for the latch — without one nothing is
|
|
57
|
-
* latched and each continuation finds out again — and the cap on continuations.
|
|
58
66
|
*/
|
|
59
67
|
export declare function continueTurn(client: OpenAI, supports: Capabilities, request: RequestBuilder, turn: Turn, { maxContinuations, ...options }?: ContinueTurnOptions): Promise<Turn>;
|