@buoy-gg/agent-core 7.0.41 → 7.0.42

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 (201) hide show
  1. package/lib/commonjs/blocks/receipts.js +1 -761
  2. package/lib/commonjs/blocks/runId.js +1 -31
  3. package/lib/commonjs/blocks/types.js +1 -140
  4. package/lib/commonjs/blocks/uiTool.js +6 -1405
  5. package/lib/commonjs/catalog/catalog.g.js +5 -4556
  6. package/lib/commonjs/catalog/catalog.source.json +249 -0
  7. package/lib/commonjs/catalog/catalog.types.g.js +1 -46
  8. package/lib/commonjs/catalog/normalizeParams.js +2 -332
  9. package/lib/commonjs/catalog/signature.js +1 -68
  10. package/lib/commonjs/catalog/snapshotReads.js +1 -263
  11. package/lib/commonjs/catalog/toProviderTools.js +2 -145
  12. package/lib/commonjs/catalog/validateParams.js +1 -129
  13. package/lib/commonjs/context/buildContextPack.js +1 -489
  14. package/lib/commonjs/effects/digest.js +1 -34
  15. package/lib/commonjs/effects/ledger.js +1 -345
  16. package/lib/commonjs/engine/askGate.js +1 -287
  17. package/lib/commonjs/engine/effectFor.js +1 -223
  18. package/lib/commonjs/engine/evidence.js +3 -111
  19. package/lib/commonjs/engine/historyBudget.js +1 -363
  20. package/lib/commonjs/engine/retrieve.js +1 -214
  21. package/lib/commonjs/engine/runAgentTurn.js +8 -1836
  22. package/lib/commonjs/engine/systemPrompt.js +54 -163
  23. package/lib/commonjs/engine/textToolCalls.js +1 -276
  24. package/lib/commonjs/engine/tokenCalibration.js +1 -81
  25. package/lib/commonjs/engine/verify.js +4 -297
  26. package/lib/commonjs/index.js +1 -384
  27. package/lib/commonjs/policy/labels.js +1 -319
  28. package/lib/commonjs/policy/policy.js +1 -148
  29. package/lib/commonjs/policy/redact.js +1 -172
  30. package/lib/commonjs/providers/anthropic.js +3 -445
  31. package/lib/commonjs/providers/openai.js +4 -324
  32. package/lib/commonjs/providers/problem.js +1 -98
  33. package/lib/commonjs/providers/sse.js +1 -240
  34. package/lib/commonjs/providers/streamTimer.js +1 -123
  35. package/lib/commonjs/providers/transport.js +1 -123
  36. package/lib/commonjs/providers/types.js +1 -6
  37. package/lib/commonjs/providers/xhrStream.js +1 -212
  38. package/lib/commonjs/realNow.js +1 -0
  39. package/lib/commonjs/session.js +1 -393
  40. package/lib/commonjs/types.js +0 -1
  41. package/lib/module/blocks/receipts.js +1 -755
  42. package/lib/module/blocks/runId.js +1 -26
  43. package/lib/module/blocks/types.js +1 -134
  44. package/lib/module/blocks/uiTool.js +6 -1396
  45. package/lib/module/catalog/catalog.g.js +5 -4552
  46. package/lib/module/catalog/catalog.source.json +249 -0
  47. package/lib/module/catalog/catalog.types.g.js +1 -42
  48. package/lib/module/catalog/normalizeParams.js +2 -326
  49. package/lib/module/catalog/signature.js +1 -63
  50. package/lib/module/catalog/snapshotReads.js +1 -259
  51. package/lib/module/catalog/toProviderTools.js +2 -139
  52. package/lib/module/catalog/validateParams.js +1 -124
  53. package/lib/module/context/buildContextPack.js +1 -485
  54. package/lib/module/effects/digest.js +1 -29
  55. package/lib/module/effects/ledger.js +1 -339
  56. package/lib/module/engine/askGate.js +1 -280
  57. package/lib/module/engine/effectFor.js +1 -220
  58. package/lib/module/engine/evidence.js +3 -105
  59. package/lib/module/engine/historyBudget.js +1 -355
  60. package/lib/module/engine/retrieve.js +1 -210
  61. package/lib/module/engine/runAgentTurn.js +8 -1827
  62. package/lib/module/engine/systemPrompt.js +54 -157
  63. package/lib/module/engine/textToolCalls.js +1 -270
  64. package/lib/module/engine/tokenCalibration.js +1 -75
  65. package/lib/module/engine/verify.js +4 -289
  66. package/lib/module/index.js +1 -35
  67. package/lib/module/policy/labels.js +1 -312
  68. package/lib/module/policy/policy.js +1 -142
  69. package/lib/module/policy/redact.js +1 -165
  70. package/lib/module/providers/anthropic.js +3 -441
  71. package/lib/module/providers/openai.js +4 -320
  72. package/lib/module/providers/problem.js +1 -92
  73. package/lib/module/providers/sse.js +1 -231
  74. package/lib/module/providers/streamTimer.js +1 -118
  75. package/lib/module/providers/transport.js +1 -119
  76. package/lib/module/providers/types.js +1 -4
  77. package/lib/module/providers/xhrStream.js +1 -207
  78. package/lib/module/realNow.js +1 -0
  79. package/lib/module/session.js +1 -369
  80. package/lib/module/types.js +0 -1
  81. package/lib/typescript/catalog/catalog.g.d.ts +3 -3
  82. package/lib/typescript/catalog/catalog.types.g.d.ts +3 -2
  83. package/lib/typescript/providers/problem.d.ts +0 -20
  84. package/lib/typescript/providers/streamTimer.d.ts +0 -24
  85. package/lib/typescript/realNow.d.ts +18 -0
  86. package/lib/web/index.mjs +52 -53
  87. package/package.json +1 -1
  88. package/lib/commonjs/blocks/receipts.js.map +0 -1
  89. package/lib/commonjs/blocks/runId.js.map +0 -1
  90. package/lib/commonjs/blocks/types.js.map +0 -1
  91. package/lib/commonjs/blocks/uiTool.js.map +0 -1
  92. package/lib/commonjs/catalog/catalog.g.js.map +0 -1
  93. package/lib/commonjs/catalog/catalog.types.g.js.map +0 -1
  94. package/lib/commonjs/catalog/normalizeParams.js.map +0 -1
  95. package/lib/commonjs/catalog/signature.js.map +0 -1
  96. package/lib/commonjs/catalog/snapshotReads.js.map +0 -1
  97. package/lib/commonjs/catalog/toProviderTools.js.map +0 -1
  98. package/lib/commonjs/catalog/validateParams.js.map +0 -1
  99. package/lib/commonjs/context/buildContextPack.js.map +0 -1
  100. package/lib/commonjs/effects/digest.js.map +0 -1
  101. package/lib/commonjs/effects/ledger.js.map +0 -1
  102. package/lib/commonjs/engine/askGate.js.map +0 -1
  103. package/lib/commonjs/engine/effectFor.js.map +0 -1
  104. package/lib/commonjs/engine/evidence.js.map +0 -1
  105. package/lib/commonjs/engine/historyBudget.js.map +0 -1
  106. package/lib/commonjs/engine/retrieve.js.map +0 -1
  107. package/lib/commonjs/engine/runAgentTurn.js.map +0 -1
  108. package/lib/commonjs/engine/systemPrompt.js.map +0 -1
  109. package/lib/commonjs/engine/textToolCalls.js.map +0 -1
  110. package/lib/commonjs/engine/tokenCalibration.js.map +0 -1
  111. package/lib/commonjs/engine/verify.js.map +0 -1
  112. package/lib/commonjs/index.js.map +0 -1
  113. package/lib/commonjs/policy/labels.js.map +0 -1
  114. package/lib/commonjs/policy/policy.js.map +0 -1
  115. package/lib/commonjs/policy/redact.js.map +0 -1
  116. package/lib/commonjs/providers/anthropic.js.map +0 -1
  117. package/lib/commonjs/providers/openai.js.map +0 -1
  118. package/lib/commonjs/providers/problem.js.map +0 -1
  119. package/lib/commonjs/providers/sse.js.map +0 -1
  120. package/lib/commonjs/providers/streamTimer.js.map +0 -1
  121. package/lib/commonjs/providers/transport.js.map +0 -1
  122. package/lib/commonjs/providers/types.js.map +0 -1
  123. package/lib/commonjs/providers/xhrStream.js.map +0 -1
  124. package/lib/commonjs/session.js.map +0 -1
  125. package/lib/commonjs/types.js.map +0 -1
  126. package/lib/module/blocks/receipts.js.map +0 -1
  127. package/lib/module/blocks/runId.js.map +0 -1
  128. package/lib/module/blocks/types.js.map +0 -1
  129. package/lib/module/blocks/uiTool.js.map +0 -1
  130. package/lib/module/catalog/catalog.g.js.map +0 -1
  131. package/lib/module/catalog/catalog.types.g.js.map +0 -1
  132. package/lib/module/catalog/normalizeParams.js.map +0 -1
  133. package/lib/module/catalog/signature.js.map +0 -1
  134. package/lib/module/catalog/snapshotReads.js.map +0 -1
  135. package/lib/module/catalog/toProviderTools.js.map +0 -1
  136. package/lib/module/catalog/validateParams.js.map +0 -1
  137. package/lib/module/context/buildContextPack.js.map +0 -1
  138. package/lib/module/effects/digest.js.map +0 -1
  139. package/lib/module/effects/ledger.js.map +0 -1
  140. package/lib/module/engine/askGate.js.map +0 -1
  141. package/lib/module/engine/effectFor.js.map +0 -1
  142. package/lib/module/engine/evidence.js.map +0 -1
  143. package/lib/module/engine/historyBudget.js.map +0 -1
  144. package/lib/module/engine/retrieve.js.map +0 -1
  145. package/lib/module/engine/runAgentTurn.js.map +0 -1
  146. package/lib/module/engine/systemPrompt.js.map +0 -1
  147. package/lib/module/engine/textToolCalls.js.map +0 -1
  148. package/lib/module/engine/tokenCalibration.js.map +0 -1
  149. package/lib/module/engine/verify.js.map +0 -1
  150. package/lib/module/index.js.map +0 -1
  151. package/lib/module/policy/labels.js.map +0 -1
  152. package/lib/module/policy/policy.js.map +0 -1
  153. package/lib/module/policy/redact.js.map +0 -1
  154. package/lib/module/providers/anthropic.js.map +0 -1
  155. package/lib/module/providers/openai.js.map +0 -1
  156. package/lib/module/providers/problem.js.map +0 -1
  157. package/lib/module/providers/sse.js.map +0 -1
  158. package/lib/module/providers/streamTimer.js.map +0 -1
  159. package/lib/module/providers/transport.js.map +0 -1
  160. package/lib/module/providers/types.js.map +0 -1
  161. package/lib/module/providers/xhrStream.js.map +0 -1
  162. package/lib/module/session.js.map +0 -1
  163. package/lib/module/types.js.map +0 -1
  164. package/lib/typescript/blocks/receipts.d.ts.map +0 -1
  165. package/lib/typescript/blocks/runId.d.ts.map +0 -1
  166. package/lib/typescript/blocks/types.d.ts.map +0 -1
  167. package/lib/typescript/blocks/uiTool.d.ts.map +0 -1
  168. package/lib/typescript/catalog/catalog.g.d.ts.map +0 -1
  169. package/lib/typescript/catalog/catalog.types.g.d.ts.map +0 -1
  170. package/lib/typescript/catalog/normalizeParams.d.ts.map +0 -1
  171. package/lib/typescript/catalog/signature.d.ts.map +0 -1
  172. package/lib/typescript/catalog/snapshotReads.d.ts.map +0 -1
  173. package/lib/typescript/catalog/toProviderTools.d.ts.map +0 -1
  174. package/lib/typescript/catalog/validateParams.d.ts.map +0 -1
  175. package/lib/typescript/context/buildContextPack.d.ts.map +0 -1
  176. package/lib/typescript/effects/digest.d.ts.map +0 -1
  177. package/lib/typescript/effects/ledger.d.ts.map +0 -1
  178. package/lib/typescript/engine/askGate.d.ts.map +0 -1
  179. package/lib/typescript/engine/effectFor.d.ts.map +0 -1
  180. package/lib/typescript/engine/evidence.d.ts.map +0 -1
  181. package/lib/typescript/engine/historyBudget.d.ts.map +0 -1
  182. package/lib/typescript/engine/retrieve.d.ts.map +0 -1
  183. package/lib/typescript/engine/runAgentTurn.d.ts.map +0 -1
  184. package/lib/typescript/engine/systemPrompt.d.ts.map +0 -1
  185. package/lib/typescript/engine/textToolCalls.d.ts.map +0 -1
  186. package/lib/typescript/engine/tokenCalibration.d.ts.map +0 -1
  187. package/lib/typescript/engine/verify.d.ts.map +0 -1
  188. package/lib/typescript/index.d.ts.map +0 -1
  189. package/lib/typescript/policy/labels.d.ts.map +0 -1
  190. package/lib/typescript/policy/policy.d.ts.map +0 -1
  191. package/lib/typescript/policy/redact.d.ts.map +0 -1
  192. package/lib/typescript/providers/anthropic.d.ts.map +0 -1
  193. package/lib/typescript/providers/openai.d.ts.map +0 -1
  194. package/lib/typescript/providers/problem.d.ts.map +0 -1
  195. package/lib/typescript/providers/sse.d.ts.map +0 -1
  196. package/lib/typescript/providers/streamTimer.d.ts.map +0 -1
  197. package/lib/typescript/providers/transport.d.ts.map +0 -1
  198. package/lib/typescript/providers/types.d.ts.map +0 -1
  199. package/lib/typescript/providers/xhrStream.d.ts.map +0 -1
  200. package/lib/typescript/session.d.ts.map +0 -1
  201. package/lib/typescript/types.d.ts.map +0 -1
@@ -1,355 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * How much conversation the model is allowed to be sent, and what gets cut.
5
- *
6
- * Lives here, not in `session.ts`, because it is needed in TWO places and used
7
- * to run in only one. The session trims once per turn, before the loop starts
8
- * — and a turn is not one request. A twelve-step investigation appends an
9
- * assistant message and a tool-results message per step, each result capped at
10
- * 24,000 characters, so a long read chain could add well over a hundred
11
- * thousand characters AFTER the only check had already happened and walk
12
- * straight into the provider's context limit. The engine now applies the same
13
- * budget before every request; the session still applies it when a turn opens.
14
- */
15
-
16
- import { compressedMarker } from "./evidence";
17
- import { DEFAULT_CHARS_PER_TOKEN } from "./tokenCalibration";
18
-
19
- /**
20
- * A long QA session must not die on the provider's context limit with an
21
- * opaque error, so the history is kept under a budget.
22
- *
23
- * TWO STAGES, AND THE ORDER IS THE POINT. Dropping the oldest rounds was the
24
- * only stage, and in a QA session the oldest round is usually the SETUP — the
25
- * override that was armed, the store that was written, the user someone is
26
- * impersonating. Drop it and the model can no longer explain the screen it is
27
- * looking at, or that a "wrong" response is its own mock. The reply gets worse
28
- * in exactly the sessions long enough to need trimming.
29
- *
30
- * So: COMPRESS first, DROP only if that was not enough.
31
- *
32
- * A tool result is "consumed" once the model has produced an assistant message
33
- * after seeing it — it has already been reasoned over, and what remains is
34
- * bulk. Replacing it with a marker keeps the round, its decision and its
35
- * ordering while giving back nearly all the bytes. Three exemptions carry
36
- * their weight:
37
- *
38
- * - Errors are never compressed. A failure is the diagnostic value.
39
- * - The newest tool-results message is never compressed. The model is about
40
- * to answer from it.
41
- * - The last read before a WRITE is never compressed: that is the shape the
42
- * model matched its edit to, and it is the thing it re-reads when the write
43
- * is refused and it has to correct itself.
44
- *
45
- * Only if the history is still over budget do whole rounds get dropped, as
46
- * before — a round starts at a user message, so the assistant/tool-result
47
- * pairing every provider requires stays intact.
48
- */
49
- /**
50
- * The limits are in TOKENS — the unit the provider enforces — and turned into
51
- * characters at the ratio the session has measured (engine/tokenCalibration.ts).
52
- * The `_CHARS` constants below are those limits at the default 4 chars/token,
53
- * kept for callers that have no calibration in hand (tests, one-shot use).
54
- */
55
- export const MAX_HISTORY_TOKENS = 37_500;
56
- /**
57
- * The ceiling on a WHOLE request: tools, system prompt, messages and the room
58
- * reserved for the reply. 100k tokens fits the smallest context Ask Buoy is
59
- * pointed at in practice (a 128k-token gateway) with headroom, and leaves
60
- * Anthropic's 200k far from the limit.
61
- */
62
- export const MAX_REQUEST_TOKENS = 100_000;
63
- export const MAX_HISTORY_CHARS = MAX_HISTORY_TOKENS * DEFAULT_CHARS_PER_TOKEN;
64
- export const MAX_REQUEST_CHARS = MAX_REQUEST_TOKENS * DEFAULT_CHARS_PER_TOKEN;
65
- const MIN_KEEP_MESSAGES = 4;
66
- /** Below this a marker costs more than the payload it replaces. */
67
- const MIN_COMPRESS_CHARS = 400;
68
- export function messageSize(m) {
69
- try {
70
- return JSON.stringify(m).length;
71
- } catch {
72
- return 0;
73
- }
74
- }
75
-
76
- /**
77
- * Replace consumed tool results with a marker. See the note above
78
- * MAX_HISTORY_CHARS for what "consumed" means and why the exemptions exist.
79
- */
80
- export function compressConsumed(messages, exemptNewest = true) {
81
- // The newest tool-results message: the model is about to answer from it.
82
- // Unless the caller says otherwise — `budgetForRequest` hands in the rounds
83
- // BEFORE the current one, whose last result was answered long ago.
84
- let newest = messages.length;
85
- if (exemptNewest) {
86
- newest = -1;
87
- for (let i = messages.length - 1; i >= 0; i--) {
88
- if (messages[i].role === "tool-results") {
89
- newest = i;
90
- break;
91
- }
92
- }
93
- if (newest <= 0) return messages;
94
- }
95
-
96
- // The reads a later write depended on. An assistant turn that called a
97
- // write was matching a shape it read in the round before, and that is the
98
- // read it goes back to when the write is refused.
99
- const beforeWrite = new Set();
100
- for (let i = 0; i < messages.length; i++) {
101
- const m = messages[i];
102
- if (m.role !== "assistant" || !m.toolCalls?.length) continue;
103
- if (!m.toolCalls.some(c => WRITE_ISH.test(JSON.stringify(c.input ?? {}) + c.name))) continue;
104
- for (let j = i - 1; j >= 0 && j >= i - 3; j--) {
105
- if (messages[j].role === "tool-results") {
106
- beforeWrite.add(j);
107
- break;
108
- }
109
- }
110
- }
111
- let changed = false;
112
- const out = messages.map((m, i) => {
113
- if (m.role !== "tool-results" || i >= newest || beforeWrite.has(i)) return m;
114
- let touched = false;
115
- const results = m.results.map(r => {
116
- if (r.isError || r.content.length < MIN_COMPRESS_CHARS) return r;
117
- if (r.content.startsWith("[earlier result")) return r;
118
- touched = true;
119
- // The ref survives the compression: the marker names it, and a later
120
- // `retrieve` reads the full text back. See engine/evidence.ts.
121
- return {
122
- ...r,
123
- content: compressedMarker(r.content.length, r.ref)
124
- };
125
- });
126
- if (!touched) return m;
127
- changed = true;
128
- return {
129
- ...m,
130
- results
131
- };
132
- });
133
- return changed ? out : messages;
134
- }
135
-
136
- /**
137
- * Heuristic for "this call changed something". Deliberately loose: the cost of
138
- * a false positive is one uncompressed read, the cost of a false negative is
139
- * the model losing the shape it was editing.
140
- */
141
- const WRITE_ISH = /set|write|dispatch|navigate|tap|override|restore|delete|remove|clear|save|run|impersonat|reload/i;
142
-
143
- /**
144
- * A round: a user message and everything up to the next one. `pinned` when
145
- * one of its tool calls made a change that is still applied — see
146
- * `dropRounds`.
147
- */
148
-
149
- function splitRounds(messages, pinnedCallIds) {
150
- const rounds = [];
151
- for (const m of messages) {
152
- if (m.role === "user" || rounds.length === 0) rounds.push({
153
- messages: [],
154
- size: 0,
155
- pinned: false
156
- });
157
- const round = rounds[rounds.length - 1];
158
- round.messages.push(m);
159
- round.size += messageSize(m);
160
- if (pinnedCallIds?.size && m.role === "assistant" && m.toolCalls?.some(c => pinnedCallIds.has(c.id))) round.pinned = true;
161
- }
162
- return rounds;
163
- }
164
-
165
- /**
166
- * Drop whole rounds, oldest first, until `keepSize + dropped` fits — but the
167
- * PINNED rounds last.
168
- *
169
- * Dropping oldest-first was the whole rule, and in a QA session the oldest
170
- * round is usually the SETUP: the override that was armed, the store that
171
- * was written, the user someone is impersonating. Lose it and the model can
172
- * no longer explain the screen it is looking at, or that a "wrong" response
173
- * is its own mock — and the changes list in the prompt names the change but
174
- * not the reasoning around it. The ledger knows which changes are still
175
- * applied, so a round that made one is kept (compressed, like any other) for
176
- * as long as an unpinned round remains to drop instead. Only when nothing
177
- * else is left do pinned rounds go, oldest first, and the caller says so.
178
- * Strands' `pin-message.ts` is the same idea, keyed on message metadata.
179
- *
180
- * `minMessages` is the floor the session-level trim keeps (a history that
181
- * opens mid-exchange is rejected by every provider; the floor is applied on
182
- * STARTING a drop, so a drop always runs to the next user boundary).
183
- */
184
- function dropRounds(rounds, budget, keepSize, minMessages) {
185
- const remaining = [...rounds];
186
- let size = remaining.reduce((n, r) => n + r.size, 0);
187
- let droppedRounds = 0;
188
- let droppedPinned = 0;
189
- const messageCount = () => remaining.reduce((n, r) => n + r.messages.length, 0);
190
- while (remaining.length > 0 && size + keepSize > budget && messageCount() > minMessages) {
191
- let i = remaining.findIndex(r => !r.pinned);
192
- if (i < 0) {
193
- i = 0;
194
- droppedPinned += 1;
195
- }
196
- size -= remaining[i].size;
197
- remaining.splice(i, 1);
198
- droppedRounds += 1;
199
- }
200
- return {
201
- kept: remaining.flatMap(r => r.messages),
202
- droppedRounds,
203
- droppedPinned
204
- };
205
- }
206
-
207
- /**
208
- * Trim, and say how many whole rounds were dropped — the number the UI turns
209
- * into "older messages were trimmed from the agent's memory".
210
- *
211
- * A round is a user message and everything up to the next one. Once a drop
212
- * starts it ALWAYS runs to the next user boundary: the inner loop used to stop
213
- * at `MIN_KEEP_MESSAGES` as well, so a history could come out starting with an
214
- * assistant or tool-results message — which every provider rejects. The
215
- * minimum-keep guard belongs on STARTING a drop, not on finishing one.
216
- */
217
- export function trimHistoryReport(messages, charsPerToken = DEFAULT_CHARS_PER_TOKEN, /** Tool-call ids whose change is still applied — their rounds are dropped last. See dropRounds. */
218
- pinnedCallIds) {
219
- const limit = Math.floor(MAX_HISTORY_TOKENS * charsPerToken);
220
- let total = 0;
221
- for (const m of messages) total += messageSize(m);
222
- if (total <= limit) return {
223
- messages,
224
- droppedRounds: 0,
225
- droppedPinned: 0
226
- };
227
-
228
- // Compress before dropping — a compressed round still explains the screen.
229
- const compressed = compressConsumed(messages);
230
- let after = 0;
231
- for (const m of compressed) after += messageSize(m);
232
- if (after <= limit) return {
233
- messages: compressed,
234
- droppedRounds: 0,
235
- droppedPinned: 0
236
- };
237
- const {
238
- kept,
239
- droppedRounds,
240
- droppedPinned
241
- } = dropRounds(splitRounds(compressed, pinnedCallIds), limit, 0, MIN_KEEP_MESSAGES);
242
- return {
243
- messages: kept,
244
- droppedRounds,
245
- droppedPinned
246
- };
247
- }
248
-
249
- /** Exported for the history tests; not part of the package surface. */
250
- export function trimHistory(messages) {
251
- return trimHistoryReport(messages).messages;
252
- }
253
-
254
- /**
255
- * Trim the working history of a turn that is still running.
256
- *
257
- * Differs from `trimHistoryReport` in one way that matters: the CURRENT round
258
- * is never touched. A turn's own user message and the reads it has taken since
259
- * are the thing being answered, so compressing or dropping them would make the
260
- * request cheaper and the answer wrong. Everything before the last user
261
- * message is fair game, in the same order as always — compress, then drop.
262
- *
263
- * `reserve` is what the request costs BEFORE any messages: the tool
264
- * definitions (~8k tokens for a typical install, ~23k with everything
265
- * installed), the system prompt, and the reply the model is allowed to write.
266
- * Budgeting messages alone would leave the biggest fixed cost out of the sum.
267
- */
268
- export function budgetForRequest(messages, reserve,
269
- /**
270
- * A tighter ceiling on the messages than MAX_HISTORY_CHARS — set by the
271
- * engine after the provider refused a request as too big. The proactive
272
- * budget is an estimate; the provider's refusal is the truth.
273
- */
274
- cap = MAX_HISTORY_CHARS, /** The session's measured ratio; scales the whole-request ceiling. */
275
- charsPerToken = DEFAULT_CHARS_PER_TOKEN, /** Tool-call ids whose change is still applied — their rounds are dropped last. See dropRounds. */
276
- pinnedCallIds) {
277
- /**
278
- * `MAX_HISTORY_CHARS` is a MESSAGES budget and always was — subtracting the
279
- * reserve from it is wrong by a wide margin. The tool block alone is ~94,000
280
- * characters with every tool installed, and the system prompt another
281
- * ~16,000, so `150,000 - reserve` left about 24,000 characters for the
282
- * conversation and dropped every earlier round of a two-turn chat. Caught by
283
- * the bank, which is exactly what it is for.
284
- *
285
- * So the messages budget is the smaller of the two ceilings: the one that
286
- * has always applied, and whatever is left of the whole-request ceiling once
287
- * the fixed cost is counted. In an ordinary install the first wins and
288
- * nothing changes; the second only bites when the tools are huge.
289
- */
290
- const budget = Math.min(cap, Math.floor(MAX_REQUEST_TOKENS * charsPerToken) - reserve);
291
- let total = 0;
292
- for (const m of messages) total += messageSize(m);
293
- if (total <= budget) return {
294
- messages,
295
- droppedRounds: 0,
296
- droppedPinned: 0
297
- };
298
-
299
- // Where the current round begins. Nothing from here on is eligible.
300
- let currentRound = -1;
301
- for (let i = messages.length - 1; i >= 0; i--) {
302
- if (messages[i].role === "user") {
303
- currentRound = i;
304
- break;
305
- }
306
- }
307
- if (currentRound <= 0) {
308
- // The whole history is this one round and it is over the limit by
309
- // itself. Its consumed results are compressed, the newest kept — see the
310
- // last-resort note below; this is the same move with nothing to drop.
311
- return {
312
- messages: compressConsumed(messages),
313
- droppedRounds: 0,
314
- droppedPinned: 0
315
- };
316
- }
317
- const earlier = messages.slice(0, currentRound);
318
- const current = messages.slice(currentRound);
319
- let currentSize = 0;
320
- for (const m of current) currentSize += messageSize(m);
321
-
322
- // `false`: the earlier rounds' last result was answered long ago — the
323
- // newest-result exemption is about the round being answered NOW.
324
- const compressed = compressConsumed(earlier, false);
325
- let earlierSize = 0;
326
- for (const m of compressed) earlierSize += messageSize(m);
327
- if (earlierSize + currentSize <= budget) {
328
- return {
329
- messages: [...compressed, ...current],
330
- droppedRounds: 0,
331
- droppedPinned: 0
332
- };
333
- }
334
- const {
335
- kept: out,
336
- droppedRounds,
337
- droppedPinned
338
- } = dropRounds(splitRounds(compressed, pinnedCallIds), budget, currentSize, 0);
339
- earlierSize = out.reduce((n, m) => n + messageSize(m), 0);
340
- /**
341
- * LAST RESORT, inside the current round. Everything earlier is gone and the
342
- * turn's own reads are still over the limit — a twelve-step investigation
343
- * of 24k-character results does that by itself. The consumed ones (an
344
- * assistant message came after them) are compressed the same way earlier
345
- * rounds were, newest result and the reads before a write still exempt.
346
- * Their refs make this lossless for the model: `retrieve` reads them back.
347
- */
348
- const currentOut = out.length === 0 && earlierSize + currentSize > budget ? compressConsumed(current) : current;
349
- return {
350
- messages: [...out, ...currentOut],
351
- droppedRounds,
352
- droppedPinned
353
- };
354
- }
355
- //# sourceMappingURL=historyBudget.js.map
1
+ "use strict";import{compressedMarker as P}from"./evidence";import{DEFAULT_CHARS_PER_TOKEN as a}from"./tokenCalibration";export const MAX_HISTORY_TOKENS=37500;export const MAX_REQUEST_TOKENS=1e5;export const MAX_HISTORY_CHARS=MAX_HISTORY_TOKENS*a;export const MAX_REQUEST_CHARS=MAX_REQUEST_TOKENS*a;const x=4;const A=400;export function messageSize(e){try{return JSON.stringify(e).length}catch{return 0}}export function compressConsumed(e,c=true){let i=e.length;if(c){i=-1;for(let t=e.length-1;t>=0;t--){if(e[t].role==="tool-results"){i=t;break}}if(i<=0)return e}const d=new Set;for(let t=0;t<e.length;t++){const u=e[t];if(u.role!=="assistant"||!u.toolCalls?.length)continue;if(!u.toolCalls.some(s=>z.test(JSON.stringify(s.input??{})+s.name)))continue;for(let s=t-1;s>=0&&s>=t-3;s--){if(e[s].role==="tool-results"){d.add(s);break}}}let o=false;const l=e.map((t,u)=>{if(t.role!=="tool-results"||u>=i||d.has(u))return t;let s=false;const n=t.results.map(r=>{if(r.isError||r.content.length<A)return r;if(r.content.startsWith("[earlier result"))return r;s=true;return{...r,content:P(r.content.length,r.ref)}});if(!s)return t;o=true;return{...t,results:n}});return o?l:e}const z=/set|write|dispatch|navigate|tap|override|restore|delete|remove|clear|save|run|impersonat|reload/i;function S(e,c){const i=[];for(const d of e){if(d.role==="user"||i.length===0)i.push({messages:[],size:0,pinned:false});const o=i[i.length-1];o.messages.push(d);o.size+=messageSize(d);if(c?.size&&d.role==="assistant"&&d.toolCalls?.some(l=>c.has(l.id)))o.pinned=true}return i}function m(e,c,i,d){const o=[...e];let l=o.reduce((n,r)=>n+r.size,0);let t=0;let u=0;const s=()=>o.reduce((n,r)=>n+r.messages.length,0);while(o.length>0&&l+i>c&&s()>d){let n=o.findIndex(r=>!r.pinned);if(n<0){n=0;u+=1}l-=o[n].size;o.splice(n,1);t+=1}return{kept:o.flatMap(n=>n.messages),droppedRounds:t,droppedPinned:u}}export function trimHistoryReport(e,c=a,i){const d=Math.floor(MAX_HISTORY_TOKENS*c);let o=0;for(const r of e)o+=messageSize(r);if(o<=d)return{messages:e,droppedRounds:0,droppedPinned:0};const l=compressConsumed(e);let t=0;for(const r of l)t+=messageSize(r);if(t<=d)return{messages:l,droppedRounds:0,droppedPinned:0};const{kept:u,droppedRounds:s,droppedPinned:n}=m(S(l,i),d,0,x);return{messages:u,droppedRounds:s,droppedPinned:n}}export function trimHistory(e){return trimHistoryReport(e).messages}export function budgetForRequest(e,c,i=MAX_HISTORY_CHARS,d=a,o){const l=Math.min(i,Math.floor(MAX_REQUEST_TOKENS*d)-c);let t=0;for(const p of e)t+=messageSize(p);if(t<=l)return{messages:e,droppedRounds:0,droppedPinned:0};let u=-1;for(let p=e.length-1;p>=0;p--){if(e[p].role==="user"){u=p;break}}if(u<=0){return{messages:compressConsumed(e),droppedRounds:0,droppedPinned:0}}const s=e.slice(0,u);const n=e.slice(u);let r=0;for(const p of n)r+=messageSize(p);const h=compressConsumed(s,false);let f=0;for(const p of h)f+=messageSize(p);if(f+r<=l){return{messages:[...h,...n],droppedRounds:0,droppedPinned:0}}const{kept:R,droppedRounds:_,droppedPinned:g}=m(S(h,o),l,r,0);f=R.reduce((p,M)=>p+messageSize(M),0);const E=R.length===0&&f+r>l?compressConsumed(n):n;return{messages:[...R,...E],droppedRounds:_,droppedPinned:g}}
@@ -1,210 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * `ask-buoy.retrieve` — read part of a kept result back. See evidence.ts.
5
- *
6
- * Four ways in, chosen so the model never needs the whole thing again:
7
- *
8
- * - nothing but `ref` → the SHAPE: top-level keys with type and size, so the
9
- * next call can name a path. Handing back the full
10
- * text would only hit the same cap that cut it.
11
- * - `path` → the value at a dotted / slash path ("stats.0.base_stat",
12
- * "/moves/3/move/name"). Arrays index by number.
13
- * - `slice: [a, b]` → a window: items of an array, characters of a string.
14
- * Combines with `path`.
15
- * - `pattern` → a literal, case-insensitive substring search over the
16
- * pretty-printed text; matching lines with
17
- * `contextLines` (default 2) around each. Not a regex:
18
- * a model that means "height" should not have to
19
- * escape anything, and an executable pattern over a
20
- * store of app data is a surface nobody asked for.
21
- *
22
- * Every result is capped at the same size as any other tool result, and the
23
- * engine never stashes a retrieve result — a retrieve of a retrieve is a loop.
24
- */
25
-
26
- const MAX_MATCHES = 20;
27
- const MAX_SHAPE_KEYS = 60;
28
- const DEFAULT_CONTEXT_LINES = 2;
29
- export function retrieveEvidence(store, params) {
30
- if (!store) {
31
- return {
32
- ok: false,
33
- error: "There is no evidence store in this session, so earlier results cannot be re-read. Call the tool again for the current value."
34
- };
35
- }
36
- const entry = store.get(params.ref);
37
- if (!entry) {
38
- return store.wasEvicted(params.ref) ? {
39
- ok: false,
40
- error: `${params.ref} was kept but has since been evicted to make room (the store holds the most recent ~4 MB of results). Call the tool again for the current value.`
41
- } : {
42
- ok: false,
43
- error: `No result with ref ${params.ref}. Refs look like ev_12 and appear in [truncated — …] and [earlier result — …] markers.`
44
- };
45
- }
46
- const meta = {
47
- ref: entry.ref,
48
- from: `${entry.toolId}.${entry.action}`,
49
- capturedAt: new Date(entry.capturedAt).toISOString(),
50
- totalChars: entry.text.length
51
- };
52
- let parsed;
53
- let isJson = true;
54
- try {
55
- parsed = JSON.parse(entry.text);
56
- } catch {
57
- isJson = false;
58
- }
59
- if (params.pattern !== undefined && params.pattern !== "") {
60
- return {
61
- ...meta,
62
- ...search(isJson ? JSON.stringify(parsed, null, 1) : entry.text, params.pattern, params.contextLines ?? DEFAULT_CONTEXT_LINES)
63
- };
64
- }
65
- if (!isJson) {
66
- const text = params.slice ? entry.text.slice(params.slice[0], params.slice[1]) : entry.text;
67
- return {
68
- ...meta,
69
- text
70
- };
71
- }
72
- let value = parsed;
73
- let at = "(root)";
74
- if (params.path) {
75
- const resolved = resolvePath(parsed, params.path);
76
- if (!resolved.ok) return {
77
- ...meta,
78
- ok: false,
79
- error: resolved.error
80
- };
81
- value = resolved.value;
82
- at = params.path;
83
- }
84
- if (params.slice) {
85
- const [a, b] = params.slice;
86
- if (Array.isArray(value)) return {
87
- ...meta,
88
- at,
89
- sliced: [a, b],
90
- of: value.length,
91
- value: value.slice(a, b)
92
- };
93
- if (typeof value === "string") return {
94
- ...meta,
95
- at,
96
- sliced: [a, b],
97
- of: value.length,
98
- value: value.slice(a, b)
99
- };
100
- return {
101
- ...meta,
102
- ok: false,
103
- error: `slice applies to an array or a string; the value at ${at} is ${typeOf(value)}.`
104
- };
105
- }
106
- if (!params.path) return {
107
- ...meta,
108
- shape: shapeOf(parsed),
109
- hint: "Pass path (e.g. \"stats.0\") or pattern to read a part."
110
- };
111
- return {
112
- ...meta,
113
- at,
114
- value
115
- };
116
- }
117
-
118
- // ── helpers ──────────────────────────────────────────────────────────────────
119
-
120
- function typeOf(v) {
121
- if (v === null) return "null";
122
- if (Array.isArray(v)) return "array";
123
- return typeof v;
124
- }
125
-
126
- /** Top-level keys (or array items) with a type and a size, for choosing a path. */
127
- function shapeOf(v) {
128
- if (Array.isArray(v)) {
129
- return {
130
- type: "array",
131
- length: v.length,
132
- first: v.length ? describe(v[0]) : undefined
133
- };
134
- }
135
- if (v && typeof v === "object") {
136
- const keys = Object.keys(v);
137
- const shown = keys.slice(0, MAX_SHAPE_KEYS);
138
- const out = {};
139
- for (const k of shown) out[k] = describe(v[k]);
140
- return keys.length > shown.length ? {
141
- ...out,
142
- "…": `${keys.length - shown.length} more keys`
143
- } : out;
144
- }
145
- return describe(v);
146
- }
147
- function describe(v) {
148
- if (v === null || v === undefined) return String(v);
149
- if (Array.isArray(v)) return `array(${v.length})`;
150
- if (typeof v === "object") {
151
- const keys = Object.keys(v);
152
- const size = JSON.stringify(v).length;
153
- return `object{${keys.slice(0, 6).join(", ")}${keys.length > 6 ? ", …" : ""}} ~${size.toLocaleString("en-US")} chars`;
154
- }
155
- if (typeof v === "string") return v.length > 40 ? `string(${v.length})` : JSON.stringify(v);
156
- return `${typeof v}: ${String(v)}`;
157
- }
158
- function resolvePath(root, path) {
159
- const segs = path.replace(/^\/+/, "").split(/[./]/).filter(s => s.length > 0);
160
- let cur = root;
161
- const walked = [];
162
- for (const seg of segs) {
163
- if (cur === null || typeof cur !== "object") {
164
- return {
165
- ok: false,
166
- error: `Cannot go into ${walked.join(".") || "(root)"} — it is ${typeOf(cur)}.`
167
- };
168
- }
169
- if (Array.isArray(cur)) {
170
- const i = Number(seg);
171
- if (!Number.isInteger(i) || i < 0 || i >= cur.length) {
172
- return {
173
- ok: false,
174
- error: `${walked.join(".") || "(root)"} is an array of ${cur.length}; "${seg}" is not an index in it.`
175
- };
176
- }
177
- cur = cur[i];
178
- } else {
179
- const rec = cur;
180
- if (!(seg in rec)) {
181
- return {
182
- ok: false,
183
- error: `No field "${seg}" at ${walked.join(".") || "(root)"}. It has: ${Object.keys(rec).slice(0, 30).join(", ")}.`
184
- };
185
- }
186
- cur = rec[seg];
187
- }
188
- walked.push(seg);
189
- }
190
- return {
191
- ok: true,
192
- value: cur
193
- };
194
- }
195
- function search(text, pattern, contextLines) {
196
- const lines = text.split("\n");
197
- const needle = pattern.toLowerCase();
198
- const hits = [];
199
- for (let i = 0; i < lines.length; i++) if (lines[i].toLowerCase().includes(needle)) hits.push(i);
200
- const ctx = Math.max(0, Math.min(10, contextLines));
201
- const matches = hits.slice(0, MAX_MATCHES).map(i => ({
202
- line: i + 1,
203
- text: lines.slice(Math.max(0, i - ctx), i + ctx + 1).join("\n")
204
- }));
205
- return {
206
- matches,
207
- totalMatches: hits.length
208
- };
209
- }
210
- //# sourceMappingURL=retrieve.js.map
1
+ "use strict";const h=20;const g=60;const y=2;export function retrieveEvidence(e,t){if(!e){return{ok:false,error:"There is no evidence store in this session, so earlier results cannot be re-read. Call the tool again for the current value."}}const o=e.get(t.ref);if(!o){return e.wasEvicted(t.ref)?{ok:false,error:`${t.ref} was kept but has since been evicted to make room (the store holds the most recent ~4 MB of results). Call the tool again for the current value.`}:{ok:false,error:`No result with ref ${t.ref}. Refs look like ev_12 and appear in [truncated \u2014 \u2026] and [earlier result \u2014 \u2026] markers.`}}const r={ref:o.ref,from:`${o.toolId}.${o.action}`,capturedAt:new Date(o.capturedAt).toISOString(),totalChars:o.text.length};let l;let s=true;try{l=JSON.parse(o.text)}catch{s=false}if(t.pattern!==void 0&&t.pattern!==""){return{...r,...$(s?JSON.stringify(l,null,1):o.text,t.pattern,t.contextLines??y)}}if(!s){const i=t.slice?o.text.slice(t.slice[0],t.slice[1]):o.text;return{...r,text:i}}let n=l;let c="(root)";if(t.path){const i=k(l,t.path);if(!i.ok)return{...r,ok:false,error:i.error};n=i.value;c=t.path}if(t.slice){const[i,a]=t.slice;if(Array.isArray(n))return{...r,at:c,sliced:[i,a],of:n.length,value:n.slice(i,a)};if(typeof n==="string")return{...r,at:c,sliced:[i,a],of:n.length,value:n.slice(i,a)};return{...r,ok:false,error:`slice applies to an array or a string; the value at ${c} is ${u(n)}.`}}if(!t.path)return{...r,shape:d(l),hint:'Pass path (e.g. "stats.0") or pattern to read a part.'};return{...r,at:c,value:n}}function u(e){if(e===null)return"null";if(Array.isArray(e))return"array";return typeof e}function d(e){if(Array.isArray(e)){return{type:"array",length:e.length,first:e.length?f(e[0]):void 0}}if(e&&typeof e==="object"){const t=Object.keys(e);const o=t.slice(0,g);const r={};for(const l of o)r[l]=f(e[l]);return t.length>o.length?{...r,"\u2026":`${t.length-o.length} more keys`}:r}return f(e)}function f(e){if(e===null||e===void 0)return String(e);if(Array.isArray(e))return`array(${e.length})`;if(typeof e==="object"){const t=Object.keys(e);const o=JSON.stringify(e).length;return`object{${t.slice(0,6).join(", ")}${t.length>6?", \u2026":""}} ~${o.toLocaleString("en-US")} chars`}if(typeof e==="string")return e.length>40?`string(${e.length})`:JSON.stringify(e);return`${typeof e}: ${String(e)}`}function k(e,t){const o=t.replace(/^\/+/,"").split(/[./]/).filter(s=>s.length>0);let r=e;const l=[];for(const s of o){if(r===null||typeof r!=="object"){return{ok:false,error:`Cannot go into ${l.join(".")||"(root)"} \u2014 it is ${u(r)}.`}}if(Array.isArray(r)){const n=Number(s);if(!Number.isInteger(n)||n<0||n>=r.length){return{ok:false,error:`${l.join(".")||"(root)"} is an array of ${r.length}; "${s}" is not an index in it.`}}r=r[n]}else{const n=r;if(!(s in n)){return{ok:false,error:`No field "${s}" at ${l.join(".")||"(root)"}. It has: ${Object.keys(n).slice(0,30).join(", ")}.`}}r=n[s]}l.push(s)}return{ok:true,value:r}}function $(e,t,o){const r=e.split("\n");const l=t.toLowerCase();const s=[];for(let i=0;i<r.length;i++)if(r[i].toLowerCase().includes(l))s.push(i);const n=Math.max(0,Math.min(10,o));const c=s.slice(0,h).map(i=>({line:i+1,text:r.slice(Math.max(0,i-n),i+n+1).join("\n")}));return{matches:c,totalMatches:s.length}}