@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.
Files changed (71) hide show
  1. package/README.md +10 -7
  2. package/dist/agent-loop.d.ts +84 -205
  3. package/dist/agent-loop.js +288 -597
  4. package/dist/calibration.d.ts +14 -10
  5. package/dist/calibration.js +30 -23
  6. package/dist/capabilities.d.ts +111 -42
  7. package/dist/capabilities.js +281 -138
  8. package/dist/client.d.ts +104 -35
  9. package/dist/client.js +205 -74
  10. package/dist/compaction.d.ts +103 -60
  11. package/dist/compaction.js +160 -86
  12. package/dist/config.d.ts +48 -12
  13. package/dist/config.js +20 -1
  14. package/dist/continuation.d.ts +24 -16
  15. package/dist/continuation.js +70 -44
  16. package/dist/digest.d.ts +7 -0
  17. package/dist/digest.js +18 -0
  18. package/dist/errors.d.ts +24 -15
  19. package/dist/errors.js +23 -15
  20. package/dist/events.d.ts +100 -63
  21. package/dist/events.js +250 -120
  22. package/dist/guards.d.ts +35 -4
  23. package/dist/guards.js +38 -5
  24. package/dist/hook-events.d.ts +21 -5
  25. package/dist/hook-events.js +23 -9
  26. package/dist/hooks.d.ts +96 -73
  27. package/dist/hooks.js +202 -116
  28. package/dist/index.d.ts +30 -26
  29. package/dist/index.js +29 -24
  30. package/dist/ledger.d.ts +62 -30
  31. package/dist/ledger.js +163 -56
  32. package/dist/platform.d.ts +13 -0
  33. package/dist/platform.js +13 -0
  34. package/dist/preselect.d.ts +222 -0
  35. package/dist/preselect.js +290 -0
  36. package/dist/request-body.d.ts +32 -0
  37. package/dist/request-body.js +47 -0
  38. package/dist/reset.d.ts +1 -0
  39. package/dist/reset.js +7 -6
  40. package/dist/retry.d.ts +29 -138
  41. package/dist/retry.js +69 -282
  42. package/dist/run-calls.d.ts +123 -0
  43. package/dist/run-calls.js +351 -0
  44. package/dist/run-turn.d.ts +36 -15
  45. package/dist/run-turn.js +74 -37
  46. package/dist/runtime.d.ts +23 -17
  47. package/dist/runtime.js +34 -24
  48. package/dist/schema-compat.d.ts +35 -18
  49. package/dist/schema-compat.js +340 -175
  50. package/dist/scope.d.ts +28 -5
  51. package/dist/scope.js +37 -10
  52. package/dist/side-task.d.ts +123 -40
  53. package/dist/side-task.js +181 -63
  54. package/dist/snapshot.d.ts +10 -5
  55. package/dist/snapshot.js +73 -30
  56. package/dist/spec.d.ts +56 -25
  57. package/dist/spec.js +494 -215
  58. package/dist/stream.d.ts +66 -16
  59. package/dist/stream.js +220 -97
  60. package/dist/thinking.d.ts +23 -8
  61. package/dist/thinking.js +92 -44
  62. package/dist/tokens.d.ts +164 -6
  63. package/dist/tokens.js +319 -6
  64. package/dist/tool-calls.d.ts +82 -17
  65. package/dist/tool-calls.js +198 -83
  66. package/dist/tool-loading.d.ts +101 -189
  67. package/dist/tool-loading.js +277 -371
  68. package/dist/wire.d.ts +81 -0
  69. package/dist/wire.js +73 -0
  70. package/llms.txt +76 -39
  71. package/package.json +4 -4
@@ -1,7 +1,9 @@
1
- import { consult, notify, textOf, turnMessages, } from "./hooks.js";
2
- import { messageTokens } from "./retry.js";
3
- import { ask } from "./side-task.js";
4
- import { holdsDefinitions } from "./tool-loading.js";
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 = "You maintain the running memory of a long conversation. Rewrite the exchange below as " +
20
- "notes the assistant can rely on after the original messages are gone. Keep decisions, " +
21
- "facts, file paths, names, numbers, and anything still unresolved. Drop pleasantries and " +
22
- "anything already superseded. Write compact prose or bullets — no preamble, no sign-off.";
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 = "Summary of the earlier part of this conversation, which is no longer shown in full:\n\n";
27
- /** What the summariser reads for one message: its text and the calls it made. */
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 = "tool_calls" in message && message.tool_calls
42
+ const calls = 'tool_calls' in message && message.tool_calls
30
43
  ? message.tool_calls
31
- .map((call) => call.type === "function" ? `${call.function.name}(${call.function.arguments})` : "")
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
- const isSummary = (message) => message.role === "system" && textOf(message.content).startsWith(SUMMARY_LEAD);
37
- /** The summary as it sits in a transcript, which is the one shape `isSummary` recognises again. */
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: "system",
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. What a caller who keeps its system prompt out of the array knows already, and passes.
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 === "system") {
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 !== "tool")
113
+ if (message.role !== Role.Tool) {
77
114
  continue;
78
- if (kept++ < keepLast)
115
+ }
116
+ if (kept++ < keepLast) {
79
117
  continue;
118
+ }
80
119
  const text = textOf(message.content);
81
- if (text.length <= maxChars || text.startsWith("[result cleared"))
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("en-US")} chars]`,
131
+ content: `[result cleared, ${text.length.toLocaleString(COUNT_LOCALE)} chars]`,
91
132
  };
92
133
  }
93
134
  return out ?? messages;
94
135
  }
95
- /** An index held between the start of the transcript and `most`, for a caller's `from` that is neither. */
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 === "user") {
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 !== "user")
213
+ while (cut < messages.length && messages[cut].role !== Role.User) {
159
214
  cut++;
215
+ }
160
216
  }
161
- if (cut >= messages.length || cut - from < 2)
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("\n\n");
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; the ceiling is 1024 and the instruction
195
- * `SUMMARY_PROMPT` unless given.
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
- * What `compactTranscript` does before it rewrites anything, which is all a host needs when the
202
- * fold lives on the session row and the messages stay where they are. Nothing here is persisted or
203
- * logged — that is the host's, and so is deciding what to do with a fold that did not happen.
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, "beforeCompact", context, hooks.onNote);
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, "beforeCompact", context, hooks.onNote),
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
- * The inverse of storing a `CompactionRecord`, and the shape `planCompaction` expects to meet
240
- * again — the same `SUMMARY_LEAD`, in a `system` message at the same place — so the next fold
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. Absent is five times
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 tools reach the model, and how long it may keep calling them. */
75
- export interface ToolPolicy {
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
- * "eager" sends every tool definition on every request. "ondemand" sends a name-only
78
- * catalogue and lets the model pull in the schemas it needs. "proxy" is the same catalogue
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
- toolDiscovery: "eager" | "ondemand" | "proxy";
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
- * Provided for callers that want one name for the lot. Nothing in this package asks for it:
102
- * the functions take the parts, so a caller missing `contextLength` can still use all of them
103
- * bar the window guard.
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
- export {};
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
+ };
@@ -1,7 +1,7 @@
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";
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, 1 unless given; zero continues nothing.
17
- * The cap is what stops a model that never reaches a stop token from looping on the ceiling.
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>;