@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
package/dist/index.js CHANGED
@@ -9,27 +9,32 @@
9
9
  * prompts, and whatever the run is about — because that is the caller's, and it is the part
10
10
  * that differs between one server and the next.
11
11
  */
12
- export { buildBody, preselect, preview, resolveApiKey, runAgentLoop, taskCall, } from "./agent-loop.js";
13
- export { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.js";
14
- export { capabilitiesFor, EFFORT_LADDER, effortFor, expireCapabilities, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
15
- export { configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.js";
16
- export { applyCompaction, COMPACT_AT, compactTranscript, KEEP_RATIO, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.js";
17
- export { continueTurn, isContinuable, } from "./continuation.js";
18
- export { AgentLoopError, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from "./errors.js";
19
- export { configureEvents, emit, endRun, fold, history, resetEvents, runMetrics, watch, } from "./events.js";
20
- export { HOOK_EVENTS, INJECT_EVENTS } from "./hook-events.js";
21
- export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.js";
22
- export { estimateFrom, rebaseLedger, recordRequest, tokensBetween, } from "./ledger.js";
23
- export { resetAll } from "./reset.js";
24
- export { backoffMs, 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";
25
- export { runTurn } from "./run-turn.js";
26
- export { createRuntime, defaultRuntime } from "./runtime.js";
27
- export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from "./schema-compat.js";
28
- export { ask, askJson, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
29
- export { CAPABILITY_SNAPSHOT_VERSION, exportCapabilities, importCapabilities, } from "./snapshot.js";
30
- export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, exportSpec, parseSpec, RESOLVED_DEFAULTS, resolveAgentSpec, SPEC_EVENTS, } from "./spec.js";
31
- export { streamTurn, } from "./stream.js";
32
- export { ALL_FENCES, DEFAULT_FENCES, FenceSplitter, stripThinking, THINK_FENCE, } from "./thinking.js";
33
- export { CHARS_PER_TOKEN, estimateTokens } from "./tokens.js";
34
- export { parseToolArguments, recoverToolCalls, ToolArgumentsError, } from "./tool-calls.js";
35
- export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, KEYWORD_DROPOFF, KEYWORD_MIN_SCORE, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, orderTools, PRESELECT_SCHEMA, PRESELECT_SYSTEM, PROXY_TOOLS, preselectByKeywords, preselectInput, preselection, preselectSystem, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, } from "./tool-loading.js";
12
+ export { PRESELECT_APPEND, PRESELECT_EXCLUSIVE, runAgentLoop, } from './agent-loop.js';
13
+ export { calibrate, charsPerTokenFor, resetCalibration } from './calibration.js';
14
+ export { capabilitiesFor, EFFORT_LADDER, EFFORT_NONE, EFFORT_OFF, effortFor, expireCapabilities, modelCapabilitiesFor, negotiate, resetCapabilities, } from './capabilities.js';
15
+ export { configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, NO_KEY, resetClients, resolveApiKey, servedWindow, timeoutMs, } from './client.js';
16
+ export { applyCompaction, COMPACT_AT, compactTranscript, KEEP_RATIO, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from './compaction.js';
17
+ export { ToolDiscovery, } from './config.js';
18
+ export { continueTurn, isContinuable, } from './continuation.js';
19
+ export { AgentLoopError, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from './errors.js';
20
+ export { configureEvents, emit, endRun, fold, history, RunEventKind, RunOutcome, resetEvents, runMetrics, watch, } from './events.js';
21
+ export { HOOK_EVENTS, HookEvent, INJECT_EVENTS } from './hook-events.js';
22
+ export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from './hooks.js';
23
+ export { estimateFrom, rebaseLedger, recordRequest, tokensBetween, } from './ledger.js';
24
+ export { KEYWORD_DROPOFF, KEYWORD_MIN_SCORE, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselect, preselectByKeywords, preselectInput, preselection, preselectSystem, } from './preselect.js';
25
+ export { buildBody } from './request-body.js';
26
+ export { resetAll } from './reset.js';
27
+ export { backoffMs, ContextOverflow, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, SMALLEST_LIKELY_WINDOW, sleep, } from './retry.js';
28
+ export { preview } from './run-calls.js';
29
+ export { runTurn } from './run-turn.js';
30
+ export { createRuntime, defaultRuntime } from './runtime.js';
31
+ export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from './schema-compat.js';
32
+ export { ask, askJson, clean, listLines, parseJson, resetHints, taskCall, tryAsk, } from './side-task.js';
33
+ export { CAPABILITY_SNAPSHOT_VERSION, exportCapabilities, importCapabilities, } from './snapshot.js';
34
+ export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, exportSpec, parseSpec, REF_FILE, REF_URL, RESOLVED_DEFAULTS, resolveAgentSpec, SPEC_EVENTS, } from './spec.js';
35
+ export { CacheBreakReason, streamTurn, } from './stream.js';
36
+ export { ALL_FENCES, DEFAULT_FENCES, FenceSplitter, SPLIT_OUTPUT, SPLIT_REASONING, stripThinking, THINK_FENCE, } from './thinking.js';
37
+ export { CHARS_PER_TOKEN, compact, contextChars, contextTokens, estimateTokens, messageTokens, requestChars, requestTokens, toolsChars, } from './tokens.js';
38
+ export { ARGUMENTS_MALFORMED, ARGUMENTS_TRUNCATED, parseToolArguments, recoverToolCalls, ToolArgumentsError, } from './tool-calls.js';
39
+ export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, orderTools, PROXY_TOOLS, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, } from './tool-loading.js';
40
+ export { FinishReason, FUNCTION_TOOL, JSON_SCHEMA_FORMAT, PartType, Role, SchemaType } from './wire.js';
package/dist/ledger.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type OpenAI from "openai";
2
- import { type TokenEstimateOptions } from "./retry.ts";
1
+ import type OpenAI from 'openai';
2
+ import type { TurnUsage } from './stream.ts';
3
+ import { type TokenEstimateOptions } from './tokens.ts';
3
4
  /**
4
5
  * What stretches of a transcript cost, read off the prompt counts the server already reported.
5
6
  *
@@ -17,6 +18,7 @@ type Message = OpenAI.ChatCompletionMessageParam;
17
18
  /**
18
19
  * One request's reported prompt, and how far into the transcript that request reached.
19
20
  *
21
+ * @remarks
20
22
  * Three numbers and nothing else, so it serialises as it is: a host that stores a row per message
21
23
  * keeps `prompt` and `epoch` in two columns on the row `through` names, and one that stores a
22
24
  * session keeps the array beside it.
@@ -39,12 +41,15 @@ export interface LedgerEntry {
39
41
  }
40
42
  /** A transcript's entries in the order their requests were sent, which is also by `through`. */
41
43
  export type TokenLedger = LedgerEntry[];
42
- /** What `recordRequest` needs to know of one request. */
43
- export interface LedgerRequest {
44
+ /** The part of a request that decides whether the next one only appended to it. */
45
+ export interface RequestShape {
44
46
  /** The messages as they were sent, a system prompt and any context a hook added included. */
45
47
  messages: Message[];
46
48
  /** The names of the tools declared, in the order they were sent. Absent is none. */
47
49
  tools?: readonly string[];
50
+ }
51
+ /** What `recordRequest` needs to know of one request. */
52
+ export interface LedgerRequest extends RequestShape {
48
53
  /** The prompt tokens the endpoint reported. Zero or less is no report, and records nothing. */
49
54
  prompt: number;
50
55
  /** The index, in the host's transcript, of the last message the request carried. */
@@ -53,48 +58,79 @@ export interface LedgerRequest {
53
58
  /** What `tokensBetween` and `estimateFrom` take besides the ledger and the transcript. */
54
59
  export interface LedgerEstimateOptions extends TokenEstimateOptions {
55
60
  /**
56
- * One unmeasured message's tokens. `messageTokens` by default, divided by `charsPerToken` — the
57
- * calibrated estimate, given `charsPerTokenFor` the model.
61
+ * One unmeasured message's tokens.
62
+ *
63
+ * @defaultValue `messageTokens` divided by `charsPerToken` — the calibrated estimate, given
64
+ * `charsPerTokenFor` the model.
58
65
  */
59
66
  estimate?: (message: Message) => number;
60
67
  }
68
+ /**
69
+ * Where a request stopped matching the one before it, earliest in the rendered prompt first — the
70
+ * tool block, then the system prompt, then the history — or `none-known` where it only appended.
71
+ *
72
+ * @param previous - The request before, as it was sent.
73
+ * @param next - The request being explained.
74
+ * @returns The first of the three that differs, so a request that changed its tools and its history
75
+ * is reported as `tools-changed` alone.
76
+ *
77
+ * @remarks
78
+ * Beside `recordRequest` because it is the same comparison asked for a different reason: that one
79
+ * wants to know whether two prompts subtract, and this one why a cache that should have held
80
+ * did not.
81
+ */
82
+ export declare function breakReason(previous: RequestShape, next: RequestShape): NonNullable<TurnUsage['cacheBreakReason']>;
61
83
  /**
62
84
  * The ledger with one more request on it, in the epoch the request before it says it belongs to.
63
85
  *
86
+ * @param ledger - The ledger so far. Not written to; handed back as it is when nothing is recorded.
87
+ * @param previous - The request the ledger's last entry was recorded from — not merely the last one
88
+ * sent, so that a request which reported nothing is skipped over rather than compared against.
89
+ * `undefined` where that is not known, which starts a new epoch.
90
+ * @param next - The request just answered.
91
+ * @returns `ledger` itself when nothing was recorded, otherwise a new array with the entry at its
92
+ * end. The first entry of an empty ledger is epoch 0.
93
+ *
94
+ * @remarks
64
95
  * The epoch carries on only where the new request declared the same tools in the same order and
65
96
  * sent every earlier message unchanged — the case in which the two prompts subtract. Anything else
66
97
  * starts a new one: a fold, a prune, a system prompt that moved, and a tool array that grew, whose
67
98
  * schema would otherwise be counted as the cost of the messages beside it. A request that reported
68
99
  * no prompt records nothing, and neither does one that reaches no further than the last entry in
69
100
  * the same epoch, so a retry or a continuation leaves the first reading standing.
70
- *
71
- * @param ledger The ledger so far. Not written to; handed back as it is when nothing is recorded.
72
- * @param previous The request the ledger's last entry was recorded from — not merely the last one
73
- * sent, so that a request which reported nothing is skipped over rather than compared against.
74
- * `undefined` where that is not known, which starts a new epoch.
75
- * @param next The request just answered.
76
101
  */
77
102
  export declare function recordRequest(ledger: TokenLedger, previous: LedgerRequest | undefined, next: LedgerRequest): TokenLedger;
78
103
  /**
79
104
  * The ledger for a transcript that has been rewritten, keeping the boundaries the rewrite left
80
105
  * standing.
81
106
  *
107
+ * @param ledger - The ledger for `before`. Not written to.
108
+ * @param before - The transcript the ledger's indexes are in.
109
+ * @param after - The transcript that replaces it. One that only appended to `before`, or is the same
110
+ * array, hands the ledger back as it is.
111
+ * @returns `ledger` itself where nothing was rewritten, otherwise a new array. The entries it keeps
112
+ * from the head are the same objects; the ones from the tail are copies.
113
+ *
114
+ * @remarks
82
115
  * A difference inside an epoch stays true after the epoch ends, since the prefix the two requests
83
116
  * shared cancels whatever became of it. So the entries that fall in the part of the transcript
84
117
  * still at its head keep their place, the ones in the part still at its tail move with it, and
85
118
  * only the ones in between — the messages a fold removed or a prune stubbed — are dropped. The
86
119
  * tail's entries move to a later epoch as well, so no difference is read across the rewrite.
87
- *
88
- * @param ledger The ledger for `before`. Not written to.
89
- * @param before The transcript the ledger's indexes are in.
90
- * @param after The transcript that replaces it. One that only appended to `before`, or is the same
91
- * array, hands the ledger back as it is.
92
120
  */
93
121
  export declare function rebaseLedger(ledger: TokenLedger, before: readonly Message[], after: readonly Message[]): TokenLedger;
94
122
  /**
95
123
  * What the messages from `from` up to, not including, `to` cost the window, measured where the
96
124
  * ledger can say and estimated where it cannot.
97
125
  *
126
+ * @param ledger - The ledger kept for `messages`.
127
+ * @param from - The first message counted. Below zero counts from the start.
128
+ * @param to - The first message not counted, as `slice` takes it. Past the end counts to the end.
129
+ * @param messages - The transcript the ledger's indexes are in.
130
+ * @param [options] - The estimate for what is not measured: a divisor, or a function of the caller's.
131
+ * @returns Whole tokens, rounded once over the stretch rather than per message.
132
+ *
133
+ * @remarks
98
134
  * Between two boundaries recorded in one epoch it is a subtraction and exact. A boundary falls
99
135
  * after a question and after each step's tool results, never after an assistant message, so a
100
136
  * stretch that starts or ends inside a group is given that group's measured total divided by
@@ -105,28 +141,24 @@ export declare function rebaseLedger(ledger: TokenLedger, before: readonly Messa
105
141
  * A difference of zero or less is not trusted and is estimated instead. That is how a server
106
142
  * reporting `prompt_tokens` net of its cache would show up, though one whose net counts still
107
143
  * happened to rise would not be caught by it.
108
- *
109
- * @param ledger The ledger kept for `messages`.
110
- * @param from The first message counted. Below zero counts from the start.
111
- * @param to The first message not counted, as `slice` takes it. Past the end counts to the end.
112
- * @param messages The transcript the ledger's indexes are in.
113
- * @param options The estimate for what is not measured: a divisor, or a function of the caller's.
114
- * @returns Whole tokens, rounded once over the stretch rather than per message.
115
144
  */
116
145
  export declare function tokensBetween(ledger: TokenLedger, from: number, to: number, messages: readonly Message[], options?: LedgerEstimateOptions): number;
117
146
  /**
118
147
  * A per-message cost for `planCompaction`'s `estimate`: the measured share where the ledger covers
119
148
  * the message, the calibrated estimate where it does not.
120
149
  *
150
+ * @param ledger - The ledger kept for `messages`.
151
+ * @param messages - The transcript the plan will be made for. The function it returns knows a
152
+ * message by identity, so it has to be asked about these objects and not copies of them; one it
153
+ * does not know is estimated.
154
+ * @param [options] - The estimate for what is not measured: a divisor, or a function of the caller's.
155
+ * @returns A function giving one message's tokens, unrounded where it was measured. It holds what
156
+ * the ledger said when this was called, and does not see entries recorded since.
157
+ *
158
+ * @remarks
121
159
  * The planner asks about one message at a time, and nothing reports anything that fine, so a
122
160
  * measured message is given its group's total by character share. The shares are not rounded,
123
161
  * which is what makes a whole group of them add back up to the difference the server reported.
124
- *
125
- * @param ledger The ledger kept for `messages`.
126
- * @param messages The transcript the plan will be made for. The function it returns knows a
127
- * message by identity, so it has to be asked about these objects and not copies of them; one it
128
- * does not know is estimated.
129
- * @param options The estimate for what is not measured: a divisor, or a function of the caller's.
130
162
  */
131
163
  export declare function estimateFrom(ledger: TokenLedger, messages: readonly Message[], options?: LedgerEstimateOptions): (message: Message) => number;
132
164
  export {};
package/dist/ledger.js CHANGED
@@ -1,88 +1,181 @@
1
- import { messageChars, messageTokens } from "./retry.js";
2
- /** Whether two messages say the same thing, by identity first since most of a transcript is. */
1
+ import { CacheBreakReason } from './stream.js';
2
+ import { messageChars, messageTokens } from './tokens.js';
3
+ import { Role } from './wire.js';
4
+ /**
5
+ * Whether two messages say the same thing, by identity first since most of a transcript is.
6
+ *
7
+ * @param a - One message.
8
+ * @param b - The other. The order makes no difference.
9
+ * @returns `true` for the same object, or two that serialise to the same JSON — so the same fields
10
+ * written in another key order are not the same.
11
+ */
3
12
  const sameMessage = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
4
- /** Whether a request sent everything the one before it did, in the same place, and then more. */
5
- const appended = (previous, next) => {
13
+ /**
14
+ * Whether two requests declared the same tools in the same order.
15
+ *
16
+ * @param previous - The request before. Absent `tools` reads as none.
17
+ * @param next - The request after, read the same way.
18
+ * @returns `true` when the names match one for one. Only names are compared, so a schema that
19
+ * changed under the same name is not seen.
20
+ */
21
+ const sameTools = (previous, next) => {
6
22
  const before = previous.tools ?? [];
7
23
  const now = next.tools ?? [];
8
- return (before.length === now.length &&
9
- before.every((name, at) => name === now[at]) &&
10
- previous.messages.length <= next.messages.length &&
11
- previous.messages.every((message, at) => sameMessage(message, next.messages[at])));
24
+ return before.length === now.length && before.every((name, at) => name === now[at]);
25
+ };
26
+ /**
27
+ * Whether a request sent every message the one before it did, in the same place.
28
+ *
29
+ * @param previous - The earlier request's messages.
30
+ * @param next - The later one's, which may run on past them.
31
+ * @returns `true` when `previous` is a prefix of `next` by `sameMessage` — the two being equal
32
+ * included, and an empty `previous` always.
33
+ */
34
+ const keptMessages = (previous, next) => previous.length <= next.length && previous.every((message, at) => sameMessage(message, next[at]));
35
+ /**
36
+ * Whether a request sent everything the one before it did, in the same place, and then more.
37
+ *
38
+ * @param previous - The request before.
39
+ * @param next - The request after.
40
+ * @returns `true` when the tools are the same and the earlier messages a prefix of the later —
41
+ * which a request that added nothing also is.
42
+ */
43
+ const appended = (previous, next) => sameTools(previous, next) && keptMessages(previous.messages, next.messages);
44
+ /**
45
+ * The system messages a request opens with, which a template renders ahead of the history.
46
+ *
47
+ * @param messages - The request's messages. Not written to.
48
+ * @returns A new array of the messages ahead of the first that is not `system`: empty when the
49
+ * request does not open with one, and all of them when it holds nothing else. A `developer` message
50
+ * ends the run.
51
+ */
52
+ const leadingSystem = (messages) => {
53
+ const end = messages.findIndex((message) => message.role !== Role.System);
54
+ return messages.slice(0, end === -1 ? messages.length : end);
12
55
  };
56
+ /**
57
+ * Where a request stopped matching the one before it, earliest in the rendered prompt first — the
58
+ * tool block, then the system prompt, then the history — or `none-known` where it only appended.
59
+ *
60
+ * @param previous - The request before, as it was sent.
61
+ * @param next - The request being explained.
62
+ * @returns The first of the three that differs, so a request that changed its tools and its history
63
+ * is reported as `tools-changed` alone.
64
+ *
65
+ * @remarks
66
+ * Beside `recordRequest` because it is the same comparison asked for a different reason: that one
67
+ * wants to know whether two prompts subtract, and this one why a cache that should have held
68
+ * did not.
69
+ */
70
+ export function breakReason(previous, next) {
71
+ if (!sameTools(previous, next)) {
72
+ return CacheBreakReason.ToolsChanged;
73
+ }
74
+ const before = leadingSystem(previous.messages);
75
+ const now = leadingSystem(next.messages);
76
+ const systemKept = before.length === now.length && keptMessages(before, now);
77
+ if (!systemKept) {
78
+ return CacheBreakReason.SystemChanged;
79
+ }
80
+ if (!keptMessages(previous.messages, next.messages)) {
81
+ return CacheBreakReason.HistoryRewritten;
82
+ }
83
+ return CacheBreakReason.NoneKnown;
84
+ }
13
85
  /**
14
86
  * The ledger with one more request on it, in the epoch the request before it says it belongs to.
15
87
  *
88
+ * @param ledger - The ledger so far. Not written to; handed back as it is when nothing is recorded.
89
+ * @param previous - The request the ledger's last entry was recorded from — not merely the last one
90
+ * sent, so that a request which reported nothing is skipped over rather than compared against.
91
+ * `undefined` where that is not known, which starts a new epoch.
92
+ * @param next - The request just answered.
93
+ * @returns `ledger` itself when nothing was recorded, otherwise a new array with the entry at its
94
+ * end. The first entry of an empty ledger is epoch 0.
95
+ *
96
+ * @remarks
16
97
  * The epoch carries on only where the new request declared the same tools in the same order and
17
98
  * sent every earlier message unchanged — the case in which the two prompts subtract. Anything else
18
99
  * starts a new one: a fold, a prune, a system prompt that moved, and a tool array that grew, whose
19
100
  * schema would otherwise be counted as the cost of the messages beside it. A request that reported
20
101
  * no prompt records nothing, and neither does one that reaches no further than the last entry in
21
102
  * the same epoch, so a retry or a continuation leaves the first reading standing.
22
- *
23
- * @param ledger The ledger so far. Not written to; handed back as it is when nothing is recorded.
24
- * @param previous The request the ledger's last entry was recorded from — not merely the last one
25
- * sent, so that a request which reported nothing is skipped over rather than compared against.
26
- * `undefined` where that is not known, which starts a new epoch.
27
- * @param next The request just answered.
28
103
  */
29
104
  export function recordRequest(ledger, previous, next) {
30
- if (!(next.prompt > 0))
105
+ if (!(next.prompt > 0)) {
31
106
  return ledger;
107
+ }
32
108
  const last = ledger.at(-1);
33
109
  if (last && previous && appended(previous, next)) {
34
- if (next.through <= last.through)
110
+ if (next.through <= last.through) {
35
111
  return ledger;
112
+ }
36
113
  return [...ledger, { through: next.through, prompt: next.prompt, epoch: last.epoch }];
37
114
  }
38
- return [
39
- ...ledger,
40
- { through: next.through, prompt: next.prompt, epoch: last ? last.epoch + 1 : 0 },
41
- ];
115
+ return [...ledger, { through: next.through, prompt: next.prompt, epoch: last ? last.epoch + 1 : 0 }];
42
116
  }
43
117
  /**
44
118
  * The ledger for a transcript that has been rewritten, keeping the boundaries the rewrite left
45
119
  * standing.
46
120
  *
121
+ * @param ledger - The ledger for `before`. Not written to.
122
+ * @param before - The transcript the ledger's indexes are in.
123
+ * @param after - The transcript that replaces it. One that only appended to `before`, or is the same
124
+ * array, hands the ledger back as it is.
125
+ * @returns `ledger` itself where nothing was rewritten, otherwise a new array. The entries it keeps
126
+ * from the head are the same objects; the ones from the tail are copies.
127
+ *
128
+ * @remarks
47
129
  * A difference inside an epoch stays true after the epoch ends, since the prefix the two requests
48
130
  * shared cancels whatever became of it. So the entries that fall in the part of the transcript
49
131
  * still at its head keep their place, the ones in the part still at its tail move with it, and
50
132
  * only the ones in between — the messages a fold removed or a prune stubbed — are dropped. The
51
133
  * tail's entries move to a later epoch as well, so no difference is read across the rewrite.
52
- *
53
- * @param ledger The ledger for `before`. Not written to.
54
- * @param before The transcript the ledger's indexes are in.
55
- * @param after The transcript that replaces it. One that only appended to `before`, or is the same
56
- * array, hands the ledger back as it is.
57
134
  */
58
135
  export function rebaseLedger(ledger, before, after) {
59
- if (before === after)
136
+ if (before === after) {
60
137
  return ledger;
138
+ }
61
139
  const shortest = Math.min(before.length, after.length);
62
140
  let head = 0;
63
- while (head < shortest && sameMessage(before[head], after[head]))
141
+ while (head < shortest && sameMessage(before[head], after[head])) {
64
142
  head++;
65
- if (head === before.length)
143
+ }
144
+ if (head === before.length) {
66
145
  return ledger;
146
+ }
67
147
  let tail = 0;
68
- while (tail < shortest - head &&
69
- sameMessage(before[before.length - 1 - tail], after[after.length - 1 - tail]))
148
+ while (tail < shortest - head && sameMessage(before[before.length - 1 - tail], after[after.length - 1 - tail])) {
70
149
  tail++;
150
+ }
71
151
  // The boundary just ahead of a kept tail stands too: what follows it is all still there. With
72
152
  // no tail kept there is nothing left for it to be the start of.
73
153
  const start = tail > 0 ? before.length - tail - 1 : before.length;
74
154
  const shift = after.length - before.length;
75
- return ledger.flatMap((entry) => entry.through < head
76
- ? [entry]
77
- : entry.through >= start
78
- ? [{ ...entry, through: entry.through + shift, epoch: entry.epoch + 1 }]
79
- : []);
155
+ const rebased = [];
156
+ for (const entry of ledger) {
157
+ if (entry.through < head) {
158
+ rebased.push(entry);
159
+ }
160
+ else if (entry.through >= start) {
161
+ rebased.push({ ...entry, through: entry.through + shift, epoch: entry.epoch + 1 });
162
+ }
163
+ }
164
+ return rebased;
80
165
  }
81
166
  /**
82
- * Each measured message's share of its group's cost, by index. A group is the messages between two
83
- * neighbouring entries of one epoch, and is left out — for the estimate to answer — where the
84
- * difference is not above zero, where the entries are out of order, or where the transcript does
85
- * not reach its end.
167
+ * Each measured message's share of its group's cost, by index.
168
+ *
169
+ * @param ledger - The entries, in the order their requests were sent.
170
+ * @param messages - The transcript the ledger's indexes are in. Each measured message is read for
171
+ * its characters.
172
+ * @returns Tokens by message index, unrounded, so a group's shares add back up to its difference.
173
+ * A message no group covers has no entry.
174
+ *
175
+ * @remarks
176
+ * A group is the messages between two neighbouring entries of one epoch, and is left out — for the
177
+ * estimate to answer — where the difference is not above zero, where the entries are out of order,
178
+ * or where the transcript does not reach its end.
86
179
  */
87
180
  function shares(ledger, messages) {
88
181
  const out = new Map();
@@ -93,10 +186,12 @@ function shares(ledger, messages) {
93
186
  const tokens = to.prompt - from.prompt;
94
187
  const start = from.through + 1;
95
188
  const end = to.through + 1;
96
- if (from.epoch !== to.epoch || !(tokens > 0))
189
+ if (from.epoch !== to.epoch || !(tokens > 0)) {
97
190
  continue;
98
- if (start < floor || end <= start || end > messages.length)
191
+ }
192
+ if (start < floor || end <= start || end > messages.length) {
99
193
  continue;
194
+ }
100
195
  floor = end;
101
196
  const chars = [];
102
197
  let total = 0;
@@ -110,12 +205,27 @@ function shares(ledger, messages) {
110
205
  }
111
206
  return out;
112
207
  }
113
- /** The estimate for a message nothing measured, as the options ask for it. */
208
+ /**
209
+ * The estimate for a message nothing measured, as the options ask for it.
210
+ *
211
+ * @param options - `estimate` is handed back as it is, and `charsPerToken` is then not read.
212
+ * Without one, `charsPerToken` is the divisor.
213
+ * @returns A function giving one message's tokens: the caller's own, or `messageTokens` at that
214
+ * divisor.
215
+ */
114
216
  const fallback = ({ charsPerToken, estimate }) => estimate ?? ((message) => messageTokens(message, { charsPerToken }));
115
217
  /**
116
218
  * What the messages from `from` up to, not including, `to` cost the window, measured where the
117
219
  * ledger can say and estimated where it cannot.
118
220
  *
221
+ * @param ledger - The ledger kept for `messages`.
222
+ * @param from - The first message counted. Below zero counts from the start.
223
+ * @param to - The first message not counted, as `slice` takes it. Past the end counts to the end.
224
+ * @param messages - The transcript the ledger's indexes are in.
225
+ * @param [options] - The estimate for what is not measured: a divisor, or a function of the caller's.
226
+ * @returns Whole tokens, rounded once over the stretch rather than per message.
227
+ *
228
+ * @remarks
119
229
  * Between two boundaries recorded in one epoch it is a subtraction and exact. A boundary falls
120
230
  * after a question and after each step's tool results, never after an assistant message, so a
121
231
  * stretch that starts or ends inside a group is given that group's measured total divided by
@@ -126,13 +236,6 @@ const fallback = ({ charsPerToken, estimate }) => estimate ?? ((message) => mess
126
236
  * A difference of zero or less is not trusted and is estimated instead. That is how a server
127
237
  * reporting `prompt_tokens` net of its cache would show up, though one whose net counts still
128
238
  * happened to rise would not be caught by it.
129
- *
130
- * @param ledger The ledger kept for `messages`.
131
- * @param from The first message counted. Below zero counts from the start.
132
- * @param to The first message not counted, as `slice` takes it. Past the end counts to the end.
133
- * @param messages The transcript the ledger's indexes are in.
134
- * @param options The estimate for what is not measured: a divisor, or a function of the caller's.
135
- * @returns Whole tokens, rounded once over the stretch rather than per message.
136
239
  */
137
240
  export function tokensBetween(ledger, from, to, messages, options = {}) {
138
241
  const measured = shares(ledger, messages);
@@ -147,20 +250,24 @@ export function tokensBetween(ledger, from, to, messages, options = {}) {
147
250
  * A per-message cost for `planCompaction`'s `estimate`: the measured share where the ledger covers
148
251
  * the message, the calibrated estimate where it does not.
149
252
  *
253
+ * @param ledger - The ledger kept for `messages`.
254
+ * @param messages - The transcript the plan will be made for. The function it returns knows a
255
+ * message by identity, so it has to be asked about these objects and not copies of them; one it
256
+ * does not know is estimated.
257
+ * @param [options] - The estimate for what is not measured: a divisor, or a function of the caller's.
258
+ * @returns A function giving one message's tokens, unrounded where it was measured. It holds what
259
+ * the ledger said when this was called, and does not see entries recorded since.
260
+ *
261
+ * @remarks
150
262
  * The planner asks about one message at a time, and nothing reports anything that fine, so a
151
263
  * measured message is given its group's total by character share. The shares are not rounded,
152
264
  * which is what makes a whole group of them add back up to the difference the server reported.
153
- *
154
- * @param ledger The ledger kept for `messages`.
155
- * @param messages The transcript the plan will be made for. The function it returns knows a
156
- * message by identity, so it has to be asked about these objects and not copies of them; one it
157
- * does not know is estimated.
158
- * @param options The estimate for what is not measured: a divisor, or a function of the caller's.
159
265
  */
160
266
  export function estimateFrom(ledger, messages, options = {}) {
161
267
  const estimate = fallback(options);
162
268
  const measured = new Map();
163
- for (const [at, tokens] of shares(ledger, messages))
269
+ for (const [at, tokens] of shares(ledger, messages)) {
164
270
  measured.set(messages[at], tokens);
271
+ }
165
272
  return (message) => measured.get(message) ?? estimate(message);
166
273
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The platform's own numbers and words, each under a name.
3
+ *
4
+ * A leaf with no imports. Settings are in seconds because that is what a person types, timers are
5
+ * in milliseconds because that is what the platform takes, and each module that crossed between
6
+ * the two had written the thousand out.
7
+ */
8
+ /** How many milliseconds a second is. */
9
+ export declare const MS_PER_SECOND = 1000;
10
+ /** How many milliseconds a minute is. */
11
+ export declare const MS_PER_MINUTE: number;
12
+ /** The event an `AbortSignal` fires when it aborts. */
13
+ export declare const ABORT_EVENT = "abort";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The platform's own numbers and words, each under a name.
3
+ *
4
+ * A leaf with no imports. Settings are in seconds because that is what a person types, timers are
5
+ * in milliseconds because that is what the platform takes, and each module that crossed between
6
+ * the two had written the thousand out.
7
+ */
8
+ /** How many milliseconds a second is. */
9
+ export const MS_PER_SECOND = 1000;
10
+ /** How many milliseconds a minute is. */
11
+ export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
12
+ /** The event an `AbortSignal` fires when it aborts. */
13
+ export const ABORT_EVENT = 'abort';