veryfront 0.1.287 → 0.1.288

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/deno.d.ts CHANGED
@@ -45,6 +45,7 @@ declare namespace _default {
45
45
  "./extensions/interfaces": string;
46
46
  "./cli": string;
47
47
  "./chat/conversation": string;
48
+ "./chat/message-prep": string;
48
49
  };
49
50
  let imports: {
50
51
  "veryfront/head": string;
@@ -256,6 +257,7 @@ declare namespace _default {
256
257
  "class-variance-authority": string;
257
258
  "tailwind-merge": string;
258
259
  "veryfront/chat/conversation": string;
260
+ "veryfront/chat/message-prep": string;
259
261
  };
260
262
  namespace compilerOptions {
261
263
  let jsx: string;
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.287",
3
+ "version": "0.1.288",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "workspace": [
@@ -64,7 +64,8 @@ export default {
64
64
  "./extensions": "./src/extensions/index.ts",
65
65
  "./extensions/interfaces": "./src/extensions/interfaces/index.ts",
66
66
  "./cli": "./cli/main.ts",
67
- "./chat/conversation": "./src/chat/conversation.ts"
67
+ "./chat/conversation": "./src/chat/conversation.ts",
68
+ "./chat/message-prep": "./src/chat/message-prep.ts"
68
69
  },
69
70
  "imports": {
70
71
  "veryfront/head": "./src/react/runtime/core.ts",
@@ -275,7 +276,8 @@ export default {
275
276
  "vfile": "npm:vfile@6.0.3",
276
277
  "class-variance-authority": "npm:class-variance-authority@0.7.1",
277
278
  "tailwind-merge": "npm:tailwind-merge@3.5.0",
278
- "veryfront/chat/conversation": "./src/chat/conversation.ts"
279
+ "veryfront/chat/conversation": "./src/chat/conversation.ts",
280
+ "veryfront/chat/message-prep": "./src/chat/message-prep.ts"
279
281
  },
280
282
  "compilerOptions": {
281
283
  "jsx": "react-jsx",
@@ -0,0 +1,12 @@
1
+ import type { ChatModelMessage } from "./types.js";
2
+ export declare function estimateTokens(value: unknown): number;
3
+ export declare function compressTurn(messages: ChatModelMessage[], startIdx: number, endIdx: number): ChatModelMessage[];
4
+ export declare function enforceTokenBudgetWithTurnCompression(messages: ChatModelMessage[], budget: number, overhead: number): ChatModelMessage[];
5
+ export declare function maskOldToolOutputs(messages: ChatModelMessage[]): ChatModelMessage[];
6
+ export declare function repairToolPairs(messages: ChatModelMessage[]): ChatModelMessage[];
7
+ export declare function estimateOverhead(instructions: unknown, toolCount: number): number;
8
+ export declare function ensureToolCallInputs(messages: ChatModelMessage[]): ChatModelMessage[];
9
+ export declare function compactForStep(messages: ChatModelMessage[], overhead?: number): ChatModelMessage[];
10
+ export declare function dedupeToolHistory(messages: ChatModelMessage[]): ChatModelMessage[];
11
+ export declare function enforceTokenBudget(messages: ChatModelMessage[], budget?: number, overhead?: number): ChatModelMessage[];
12
+ //# sourceMappingURL=message-prep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-prep.d.ts","sourceRoot":"","sources":["../../../src/src/chat/message-prep.ts"],"names":[],"mappings":"AAAA,OAAO,yBAAyB,CAAC;AAOjC,OAAO,KAAK,EAAE,gBAAgB,EAA4C,MAAM,YAAY,CAAC;AAI7F,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAErD;AAOD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,gBAAgB,EAAE,CA+BpB;AAkCD,wBAAgB,qCAAqC,CACnD,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,gBAAgB,EAAE,CAwDpB;AAyHD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,EAAE,CAqEnF;AAWD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,EAAE,CA2IhF;AAED,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGjF;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,EAAE,CA4BrF;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,QAAQ,GAAE,MAAU,GACnB,gBAAgB,EAAE,CAepB;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,EAAE,CA0DlF;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,MAAM,GAAE,MAA6B,EACrC,QAAQ,GAAE,MAAU,GACnB,gBAAgB,EAAE,CAMpB"}
@@ -0,0 +1,492 @@
1
+ import { getStringField, isReasoningPart, isToolCallPart, isToolResultPart, } from "./conversation.js";
2
+ const CHARS_PER_TOKEN = 4;
3
+ export function estimateTokens(value) {
4
+ return Math.ceil(JSON.stringify(value ?? "").length / CHARS_PER_TOKEN);
5
+ }
6
+ function truncate(text, maxLen) {
7
+ if (text.length <= maxLen)
8
+ return text;
9
+ return `${text.slice(0, maxLen)}…`;
10
+ }
11
+ export function compressTurn(messages, startIdx, endIdx) {
12
+ let userQuery = "";
13
+ const toolNames = [];
14
+ let assistantConclusion = "";
15
+ for (let i = startIdx; i <= endIdx; i++) {
16
+ const msg = messages[i];
17
+ if (msg.role === "user") {
18
+ const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
19
+ userQuery = truncate(text, 100);
20
+ }
21
+ else if (msg.role === "assistant" && Array.isArray(msg.content)) {
22
+ for (const part of msg.content) {
23
+ if (part.type === "tool-call") {
24
+ toolNames.push(part.toolName);
25
+ }
26
+ else if (part.type === "text") {
27
+ assistantConclusion = truncate(part.text, 150);
28
+ }
29
+ }
30
+ }
31
+ else if (msg.role === "assistant" && typeof msg.content === "string") {
32
+ assistantConclusion = truncate(msg.content, 150);
33
+ }
34
+ }
35
+ const toolSummary = toolNames.length > 0 ? ` → used ${toolNames.join(", ")}` : "";
36
+ const conclusionSummary = assistantConclusion ? ` → ${assistantConclusion}` : "";
37
+ const summary = `[Compressed: ${userQuery}${toolSummary}${conclusionSummary}]`;
38
+ return [
39
+ { role: "user", content: summary },
40
+ { role: "assistant", content: "Acknowledged." },
41
+ ];
42
+ }
43
+ function collectTurns(messages) {
44
+ const turns = [];
45
+ let currentTurn = null;
46
+ for (let i = 0; i < messages.length; i++) {
47
+ if (messages[i].role === "user") {
48
+ if (currentTurn) {
49
+ currentTurn.endIdx = i - 1;
50
+ turns.push(currentTurn);
51
+ }
52
+ currentTurn = { startIdx: i, endIdx: i, tokens: estimateTokens(messages[i].content) };
53
+ }
54
+ else if (currentTurn) {
55
+ currentTurn.endIdx = i;
56
+ currentTurn.tokens += estimateTokens(messages[i].content);
57
+ }
58
+ }
59
+ if (currentTurn) {
60
+ currentTurn.endIdx = messages.length - 1;
61
+ turns.push(currentTurn);
62
+ }
63
+ return turns;
64
+ }
65
+ export function enforceTokenBudgetWithTurnCompression(messages, budget, overhead) {
66
+ const effectiveBudget = budget - overhead;
67
+ let totalTokens = messages.reduce((sum, message) => sum + estimateTokens(message.content), 0);
68
+ if (totalTokens <= effectiveBudget)
69
+ return messages;
70
+ const turns = collectTurns(messages);
71
+ const minKeep = Math.min(2, turns.length);
72
+ const result = [...messages];
73
+ let compressCount = 0;
74
+ while (totalTokens > effectiveBudget && compressCount < turns.length - minKeep) {
75
+ const turn = turns[compressCount];
76
+ if (turn.compressed) {
77
+ compressCount++;
78
+ continue;
79
+ }
80
+ const compressed = compressTurn(messages, turn.startIdx, turn.endIdx);
81
+ const compressedTokens = compressed.reduce((sum, message) => sum + estimateTokens(message.content), 0);
82
+ const saved = turn.tokens - compressedTokens;
83
+ if (saved <= 0) {
84
+ compressCount++;
85
+ continue;
86
+ }
87
+ const turnLength = turn.endIdx - turn.startIdx + 1;
88
+ result.splice(turn.startIdx, turnLength, ...compressed);
89
+ const indexShift = compressed.length - turnLength;
90
+ for (let j = compressCount + 1; j < turns.length; j++) {
91
+ turns[j].startIdx += indexShift;
92
+ turns[j].endIdx += indexShift;
93
+ }
94
+ turn.endIdx = turn.startIdx + compressed.length - 1;
95
+ turn.tokens = compressedTokens;
96
+ turn.compressed = true;
97
+ totalTokens -= saved;
98
+ compressCount++;
99
+ }
100
+ if (totalTokens <= effectiveBudget)
101
+ return result;
102
+ const finalTurns = collectTurns(result);
103
+ const finalMinKeep = Math.min(2, finalTurns.length);
104
+ let dropCount = 0;
105
+ while (totalTokens > effectiveBudget && dropCount < finalTurns.length - finalMinKeep) {
106
+ totalTokens -= finalTurns[dropCount].tokens;
107
+ dropCount++;
108
+ }
109
+ if (dropCount === 0)
110
+ return result;
111
+ const firstKeepIdx = finalTurns[dropCount].startIdx;
112
+ return result.slice(firstKeepIdx);
113
+ }
114
+ const BUDGET_RATIO = 0.85;
115
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
116
+ const DEFAULT_TOKEN_BUDGET = Math.floor(DEFAULT_CONTEXT_WINDOW * BUDGET_RATIO);
117
+ const TOKENS_PER_TOOL = 250;
118
+ const MASK_THRESHOLD = 500;
119
+ function serializedLength(value) {
120
+ return JSON.stringify(value ?? "").length;
121
+ }
122
+ function tryParseJson(value) {
123
+ if (typeof value !== "string")
124
+ return value;
125
+ try {
126
+ return JSON.parse(value);
127
+ }
128
+ catch {
129
+ return value;
130
+ }
131
+ }
132
+ function isRecord(value) {
133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
134
+ }
135
+ function buildToolCallMap(messages) {
136
+ const map = new Map();
137
+ for (const msg of messages) {
138
+ if (msg.role !== "assistant" || !Array.isArray(msg.content))
139
+ continue;
140
+ for (const part of msg.content) {
141
+ if (isToolCallPart(part)) {
142
+ map.set(part.toolCallId, { toolName: part.toolName, input: part.input });
143
+ }
144
+ }
145
+ }
146
+ return map;
147
+ }
148
+ function maskReadFile(input, charCount) {
149
+ const path = getStringField(input, "path", "unknown");
150
+ return `[File read: ${path} — content omitted (${charCount} chars)]`;
151
+ }
152
+ function maskBash(input, output, charCount) {
153
+ const cmd = truncate(getStringField(input, "command", "unknown"), 80);
154
+ let exitCode = "?";
155
+ const parsed = tryParseJson(output);
156
+ if (isRecord(parsed) && "exitCode" in parsed) {
157
+ exitCode = String(parsed.exitCode);
158
+ }
159
+ return `[Command: ${cmd} — exit ${exitCode}, output omitted (${charCount} chars)]`;
160
+ }
161
+ function maskWebSearch(output) {
162
+ const parsed = tryParseJson(output);
163
+ if (!Array.isArray(parsed))
164
+ return output;
165
+ return parsed.map((item) => {
166
+ if (!isRecord(item))
167
+ return item;
168
+ const { encryptedContent: _, ...rest } = item;
169
+ return rest;
170
+ });
171
+ }
172
+ function maskWebFetch(input, charCount) {
173
+ const url = getStringField(input, "url", "unknown");
174
+ return `[Fetched: ${url} — content omitted (${charCount} chars)]`;
175
+ }
176
+ function maskTask(output, charCount) {
177
+ const parsed = tryParseJson(output);
178
+ if (!isRecord(parsed)) {
179
+ return `[task output omitted (${charCount} chars)]`;
180
+ }
181
+ const masked = {};
182
+ if ("success" in parsed)
183
+ masked.success = parsed.success;
184
+ if ("description" in parsed)
185
+ masked.description = parsed.description;
186
+ if ("result" in parsed) {
187
+ masked.result = typeof parsed.result === "string"
188
+ ? truncate(parsed.result, 500)
189
+ : parsed.result;
190
+ }
191
+ return masked;
192
+ }
193
+ function maskGeneric(toolName, charCount) {
194
+ return `[${toolName} output omitted (${charCount} chars)]`;
195
+ }
196
+ function getOutputValue(output) {
197
+ if (!isRecord(output))
198
+ return output;
199
+ if ((output.type === "text" || output.type === "json") && "value" in output) {
200
+ return output.value;
201
+ }
202
+ return output;
203
+ }
204
+ function wrapToolResultOutput(original, newValue) {
205
+ const textValue = typeof newValue === "string" ? newValue : JSON.stringify(newValue);
206
+ if (original.type === "text") {
207
+ return { ...original, value: textValue };
208
+ }
209
+ if (original.type === "json") {
210
+ return { type: "text", value: textValue };
211
+ }
212
+ return original;
213
+ }
214
+ export function maskOldToolOutputs(messages) {
215
+ let lastUserIdx = -1;
216
+ for (let i = messages.length - 1; i >= 0; i--) {
217
+ if (messages[i].role === "user") {
218
+ lastUserIdx = i;
219
+ break;
220
+ }
221
+ }
222
+ if (lastUserIdx <= 0)
223
+ return messages;
224
+ const toolCallMap = buildToolCallMap(messages);
225
+ return messages.map((msg, idx) => {
226
+ if (idx >= lastUserIdx)
227
+ return msg;
228
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
229
+ const filtered = msg.content.filter((part) => !isReasoningPart(part));
230
+ if (filtered.length !== msg.content.length) {
231
+ return { ...msg, content: filtered };
232
+ }
233
+ return msg;
234
+ }
235
+ if (msg.role !== "tool" || !Array.isArray(msg.content))
236
+ return msg;
237
+ const newContent = msg.content.map((part) => {
238
+ if (part.type !== "tool-result") {
239
+ return part;
240
+ }
241
+ const rawValue = getOutputValue(part.output);
242
+ const charCount = serializedLength(rawValue);
243
+ if (charCount < MASK_THRESHOLD)
244
+ return part;
245
+ const callInfo = toolCallMap.get(part.toolCallId);
246
+ const toolName = part.toolName || callInfo?.toolName || "unknown";
247
+ const input = callInfo?.input;
248
+ let masked;
249
+ switch (toolName) {
250
+ case "readFile":
251
+ case "get_file":
252
+ masked = maskReadFile(input, charCount);
253
+ break;
254
+ case "bash":
255
+ masked = maskBash(input, rawValue, charCount);
256
+ break;
257
+ case "web_search":
258
+ masked = maskWebSearch(rawValue);
259
+ break;
260
+ case "web_fetch":
261
+ masked = maskWebFetch(input, charCount);
262
+ break;
263
+ case "task":
264
+ masked = maskTask(rawValue, charCount);
265
+ break;
266
+ default:
267
+ masked = maskGeneric(toolName, charCount);
268
+ break;
269
+ }
270
+ return { ...part, output: wrapToolResultOutput(part.output, masked) };
271
+ });
272
+ return { ...msg, content: newContent };
273
+ });
274
+ }
275
+ function createSyntheticToolResult(toolCallId, toolName) {
276
+ return {
277
+ type: "tool-result",
278
+ toolCallId,
279
+ toolName,
280
+ output: { type: "text", value: "[tool result unavailable]" },
281
+ };
282
+ }
283
+ export function repairToolPairs(messages) {
284
+ const result = [...messages];
285
+ let mutated = false;
286
+ for (let index = 0; index < result.length; index++) {
287
+ const message = result[index];
288
+ if (message.role !== "assistant" || !Array.isArray(message.content)) {
289
+ continue;
290
+ }
291
+ const inlineResultIds = new Set();
292
+ for (const part of message.content) {
293
+ if (isToolResultPart(part)) {
294
+ inlineResultIds.add(part.toolCallId);
295
+ }
296
+ }
297
+ const repairedContent = [];
298
+ const regularToolCalls = [];
299
+ for (const part of message.content) {
300
+ repairedContent.push(part);
301
+ if (!isToolCallPart(part)) {
302
+ continue;
303
+ }
304
+ const toolName = part.toolName ?? "unknown";
305
+ if (part.providerExecuted) {
306
+ if (!inlineResultIds.has(part.toolCallId)) {
307
+ repairedContent.push(createSyntheticToolResult(part.toolCallId, toolName));
308
+ mutated = true;
309
+ }
310
+ continue;
311
+ }
312
+ if (!inlineResultIds.has(part.toolCallId)) {
313
+ regularToolCalls.push({ id: part.toolCallId, toolName });
314
+ }
315
+ }
316
+ if (repairedContent.length !== message.content.length) {
317
+ result[index] = {
318
+ ...message,
319
+ content: repairedContent,
320
+ };
321
+ }
322
+ if (regularToolCalls.length === 0) {
323
+ continue;
324
+ }
325
+ const nextMessage = result[index + 1];
326
+ const immediateResultIds = new Set();
327
+ if (nextMessage?.role === "tool" && Array.isArray(nextMessage.content)) {
328
+ for (const part of nextMessage.content) {
329
+ if (isToolResultPart(part)) {
330
+ immediateResultIds.add(part.toolCallId);
331
+ }
332
+ }
333
+ }
334
+ const unresolvedCalls = regularToolCalls.filter((toolCall) => !immediateResultIds.has(toolCall.id));
335
+ if (unresolvedCalls.length === 0) {
336
+ continue;
337
+ }
338
+ const movedResults = new Map();
339
+ for (let laterIndex = index + 2; laterIndex < result.length && movedResults.size < unresolvedCalls.length; laterIndex++) {
340
+ const laterMessage = result[laterIndex];
341
+ if (laterMessage?.role !== "tool" || !Array.isArray(laterMessage.content)) {
342
+ continue;
343
+ }
344
+ let removedFromLater = false;
345
+ const keptLaterContent = laterMessage.content.filter((part) => {
346
+ if (!isToolResultPart(part)) {
347
+ return true;
348
+ }
349
+ if (!unresolvedCalls.some((toolCall) => toolCall.id === part.toolCallId) ||
350
+ movedResults.has(part.toolCallId)) {
351
+ return true;
352
+ }
353
+ movedResults.set(part.toolCallId, part);
354
+ removedFromLater = true;
355
+ return false;
356
+ });
357
+ if (!removedFromLater) {
358
+ continue;
359
+ }
360
+ mutated = true;
361
+ if (keptLaterContent.length === 0) {
362
+ result.splice(laterIndex, 1);
363
+ laterIndex--;
364
+ continue;
365
+ }
366
+ result[laterIndex] = {
367
+ ...laterMessage,
368
+ content: keptLaterContent,
369
+ };
370
+ }
371
+ const repairedResults = unresolvedCalls.map((toolCall) => movedResults.get(toolCall.id) ?? createSyntheticToolResult(toolCall.id, toolCall.toolName));
372
+ if (nextMessage?.role === "tool" && Array.isArray(nextMessage.content)) {
373
+ result[index + 1] = {
374
+ ...nextMessage,
375
+ content: [...repairedResults, ...nextMessage.content],
376
+ };
377
+ }
378
+ else {
379
+ const toolMessage = {
380
+ role: "tool",
381
+ content: repairedResults,
382
+ };
383
+ result.splice(index + 1, 0, toolMessage);
384
+ }
385
+ mutated = true;
386
+ }
387
+ return mutated ? result : messages;
388
+ }
389
+ export function estimateOverhead(instructions, toolCount) {
390
+ const instructionTokens = estimateTokens(instructions);
391
+ return instructionTokens + toolCount * TOKENS_PER_TOOL;
392
+ }
393
+ export function ensureToolCallInputs(messages) {
394
+ let mutated = false;
395
+ const result = messages.map((msg) => {
396
+ if (msg.role !== "assistant" || !Array.isArray(msg.content))
397
+ return msg;
398
+ let msgMutated = false;
399
+ const newContent = msg.content.map((part) => {
400
+ if (isToolCallPart(part)) {
401
+ const input = part.input;
402
+ if (input === undefined || input === null || typeof input !== "object" || Array.isArray(input)) {
403
+ msgMutated = true;
404
+ return { ...part, input: {} };
405
+ }
406
+ }
407
+ return part;
408
+ });
409
+ if (msgMutated) {
410
+ mutated = true;
411
+ return { ...msg, content: newContent };
412
+ }
413
+ return msg;
414
+ });
415
+ return mutated ? result : messages;
416
+ }
417
+ export function compactForStep(messages, overhead = 0) {
418
+ const compacted = enforceTokenBudget(maskOldToolOutputs(messages), DEFAULT_TOKEN_BUDGET, overhead);
419
+ let end = compacted.length;
420
+ while (end > 1 && compacted[end - 1].role === "assistant") {
421
+ end--;
422
+ }
423
+ const trimmed = end < compacted.length ? compacted.slice(0, end) : compacted;
424
+ return ensureToolCallInputs(repairToolPairs(dedupeToolHistory(trimmed)));
425
+ }
426
+ export function dedupeToolHistory(messages) {
427
+ const seenToolCallIds = new Set();
428
+ const seenToolResultIds = new Set();
429
+ let mutated = false;
430
+ const deduped = [];
431
+ const filterParts = (parts) => {
432
+ const filtered = parts.filter((part) => {
433
+ if (isToolCallPart(part)) {
434
+ if (seenToolCallIds.has(part.toolCallId)) {
435
+ mutated = true;
436
+ return false;
437
+ }
438
+ seenToolCallIds.add(part.toolCallId);
439
+ return true;
440
+ }
441
+ if (isToolResultPart(part)) {
442
+ if (seenToolResultIds.has(part.toolCallId)) {
443
+ mutated = true;
444
+ return false;
445
+ }
446
+ seenToolResultIds.add(part.toolCallId);
447
+ return true;
448
+ }
449
+ return true;
450
+ });
451
+ return { filtered, changed: filtered.length !== parts.length };
452
+ };
453
+ for (const message of messages) {
454
+ if (message.role === "user" && Array.isArray(message.content)) {
455
+ const { filtered, changed } = filterParts(message.content);
456
+ if (!changed) {
457
+ deduped.push(message);
458
+ continue;
459
+ }
460
+ if (filtered.length > 0)
461
+ deduped.push({ ...message, content: filtered });
462
+ }
463
+ else if (message.role === "assistant" && Array.isArray(message.content)) {
464
+ const { filtered, changed } = filterParts(message.content);
465
+ if (!changed) {
466
+ deduped.push(message);
467
+ continue;
468
+ }
469
+ if (filtered.length > 0)
470
+ deduped.push({ ...message, content: filtered });
471
+ }
472
+ else if (message.role === "tool") {
473
+ const { filtered, changed } = filterParts(message.content);
474
+ if (!changed) {
475
+ deduped.push(message);
476
+ continue;
477
+ }
478
+ if (filtered.length > 0)
479
+ deduped.push({ ...message, content: filtered });
480
+ }
481
+ else {
482
+ deduped.push(message);
483
+ }
484
+ }
485
+ return mutated ? deduped : messages;
486
+ }
487
+ export function enforceTokenBudget(messages, budget = DEFAULT_TOKEN_BUDGET, overhead = 0) {
488
+ if (messages.length === 0) {
489
+ return messages;
490
+ }
491
+ return enforceTokenBudgetWithTurnCompression(messages, budget, overhead);
492
+ }
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.287";
1
+ export declare const VERSION = "0.1.288";
2
2
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.287";
3
+ export const VERSION = "0.1.288";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.287",
3
+ "version": "0.1.288",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
@@ -181,6 +181,10 @@
181
181
  "import": "./esm/src/chat/conversation.js",
182
182
  "types": "./esm/src/chat/conversation.d.ts"
183
183
  },
184
+ "./chat/message-prep": {
185
+ "import": "./esm/src/chat/message-prep.js",
186
+ "types": "./esm/src/chat/message-prep.d.ts"
187
+ },
184
188
  "./tsconfig.json": "./tsconfig.json"
185
189
  },
186
190
  "scripts": {},
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.287",
3
+ "version": "0.1.288",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "workspace": [
@@ -64,7 +64,8 @@ export default {
64
64
  "./extensions": "./src/extensions/index.ts",
65
65
  "./extensions/interfaces": "./src/extensions/interfaces/index.ts",
66
66
  "./cli": "./cli/main.ts",
67
- "./chat/conversation": "./src/chat/conversation.ts"
67
+ "./chat/conversation": "./src/chat/conversation.ts",
68
+ "./chat/message-prep": "./src/chat/message-prep.ts"
68
69
  },
69
70
  "imports": {
70
71
  "veryfront/head": "./src/react/runtime/core.ts",
@@ -275,7 +276,8 @@ export default {
275
276
  "vfile": "npm:vfile@6.0.3",
276
277
  "class-variance-authority": "npm:class-variance-authority@0.7.1",
277
278
  "tailwind-merge": "npm:tailwind-merge@3.5.0",
278
- "veryfront/chat/conversation": "./src/chat/conversation.ts"
279
+ "veryfront/chat/conversation": "./src/chat/conversation.ts",
280
+ "veryfront/chat/message-prep": "./src/chat/message-prep.ts"
279
281
  },
280
282
  "compilerOptions": {
281
283
  "jsx": "react-jsx",
@@ -0,0 +1,617 @@
1
+ import "../../_dnt.polyfills.js";
2
+ import {
3
+ getStringField,
4
+ isReasoningPart,
5
+ isToolCallPart,
6
+ isToolResultPart,
7
+ } from "./conversation.js";
8
+ import type { ChatModelMessage, ChatToolResultOutput, ChatToolResultPart } from "./types.js";
9
+
10
+ const CHARS_PER_TOKEN = 4;
11
+
12
+ export function estimateTokens(value: unknown): number {
13
+ return Math.ceil(JSON.stringify(value ?? "").length / CHARS_PER_TOKEN);
14
+ }
15
+
16
+ function truncate(text: string, maxLen: number): string {
17
+ if (text.length <= maxLen) return text;
18
+ return `${text.slice(0, maxLen)}…`;
19
+ }
20
+
21
+ export function compressTurn(
22
+ messages: ChatModelMessage[],
23
+ startIdx: number,
24
+ endIdx: number,
25
+ ): ChatModelMessage[] {
26
+ let userQuery = "";
27
+ const toolNames: string[] = [];
28
+ let assistantConclusion = "";
29
+
30
+ for (let i = startIdx; i <= endIdx; i++) {
31
+ const msg = messages[i];
32
+ if (msg.role === "user") {
33
+ const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
34
+ userQuery = truncate(text, 100);
35
+ } else if (msg.role === "assistant" && Array.isArray(msg.content)) {
36
+ for (const part of msg.content) {
37
+ if (part.type === "tool-call") {
38
+ toolNames.push(part.toolName);
39
+ } else if (part.type === "text") {
40
+ assistantConclusion = truncate(part.text, 150);
41
+ }
42
+ }
43
+ } else if (msg.role === "assistant" && typeof msg.content === "string") {
44
+ assistantConclusion = truncate(msg.content, 150);
45
+ }
46
+ }
47
+
48
+ const toolSummary = toolNames.length > 0 ? ` → used ${toolNames.join(", ")}` : "";
49
+ const conclusionSummary = assistantConclusion ? ` → ${assistantConclusion}` : "";
50
+ const summary = `[Compressed: ${userQuery}${toolSummary}${conclusionSummary}]`;
51
+
52
+ return [
53
+ { role: "user", content: summary },
54
+ { role: "assistant", content: "Acknowledged." },
55
+ ];
56
+ }
57
+
58
+ interface TurnWindow {
59
+ startIdx: number;
60
+ endIdx: number;
61
+ tokens: number;
62
+ compressed?: boolean;
63
+ }
64
+
65
+ function collectTurns(messages: ChatModelMessage[]): TurnWindow[] {
66
+ const turns: TurnWindow[] = [];
67
+ let currentTurn: TurnWindow | null = null;
68
+
69
+ for (let i = 0; i < messages.length; i++) {
70
+ if (messages[i].role === "user") {
71
+ if (currentTurn) {
72
+ currentTurn.endIdx = i - 1;
73
+ turns.push(currentTurn);
74
+ }
75
+ currentTurn = { startIdx: i, endIdx: i, tokens: estimateTokens(messages[i].content) };
76
+ } else if (currentTurn) {
77
+ currentTurn.endIdx = i;
78
+ currentTurn.tokens += estimateTokens(messages[i].content);
79
+ }
80
+ }
81
+
82
+ if (currentTurn) {
83
+ currentTurn.endIdx = messages.length - 1;
84
+ turns.push(currentTurn);
85
+ }
86
+
87
+ return turns;
88
+ }
89
+
90
+ export function enforceTokenBudgetWithTurnCompression(
91
+ messages: ChatModelMessage[],
92
+ budget: number,
93
+ overhead: number,
94
+ ): ChatModelMessage[] {
95
+ const effectiveBudget = budget - overhead;
96
+ let totalTokens = messages.reduce((sum, message) => sum + estimateTokens(message.content), 0);
97
+ if (totalTokens <= effectiveBudget) return messages;
98
+
99
+ const turns = collectTurns(messages);
100
+ const minKeep = Math.min(2, turns.length);
101
+ const result = [...messages];
102
+
103
+ let compressCount = 0;
104
+ while (totalTokens > effectiveBudget && compressCount < turns.length - minKeep) {
105
+ const turn = turns[compressCount];
106
+ if (turn.compressed) {
107
+ compressCount++;
108
+ continue;
109
+ }
110
+
111
+ const compressed = compressTurn(messages, turn.startIdx, turn.endIdx);
112
+ const compressedTokens = compressed.reduce(
113
+ (sum, message) => sum + estimateTokens(message.content),
114
+ 0,
115
+ );
116
+ const saved = turn.tokens - compressedTokens;
117
+ if (saved <= 0) {
118
+ compressCount++;
119
+ continue;
120
+ }
121
+
122
+ const turnLength = turn.endIdx - turn.startIdx + 1;
123
+ result.splice(turn.startIdx, turnLength, ...compressed);
124
+ const indexShift = compressed.length - turnLength;
125
+ for (let j = compressCount + 1; j < turns.length; j++) {
126
+ turns[j].startIdx += indexShift;
127
+ turns[j].endIdx += indexShift;
128
+ }
129
+ turn.endIdx = turn.startIdx + compressed.length - 1;
130
+ turn.tokens = compressedTokens;
131
+ turn.compressed = true;
132
+ totalTokens -= saved;
133
+ compressCount++;
134
+ }
135
+
136
+ if (totalTokens <= effectiveBudget) return result;
137
+
138
+ const finalTurns = collectTurns(result);
139
+ const finalMinKeep = Math.min(2, finalTurns.length);
140
+ let dropCount = 0;
141
+ while (totalTokens > effectiveBudget && dropCount < finalTurns.length - finalMinKeep) {
142
+ totalTokens -= finalTurns[dropCount].tokens;
143
+ dropCount++;
144
+ }
145
+
146
+ if (dropCount === 0) return result;
147
+
148
+ const firstKeepIdx = finalTurns[dropCount].startIdx;
149
+ return result.slice(firstKeepIdx);
150
+ }
151
+
152
+ const BUDGET_RATIO = 0.85;
153
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
154
+ const DEFAULT_TOKEN_BUDGET = Math.floor(DEFAULT_CONTEXT_WINDOW * BUDGET_RATIO);
155
+ const TOKENS_PER_TOOL = 250;
156
+
157
+ const MASK_THRESHOLD = 500;
158
+
159
+ function serializedLength(value: unknown): number {
160
+ return JSON.stringify(value ?? "").length;
161
+ }
162
+
163
+ function tryParseJson(value: unknown): unknown {
164
+ if (typeof value !== "string") return value;
165
+ try {
166
+ return JSON.parse(value);
167
+ } catch {
168
+ return value;
169
+ }
170
+ }
171
+
172
+ function isRecord(value: unknown): value is Record<string, unknown> {
173
+ return typeof value === "object" && value !== null && !Array.isArray(value);
174
+ }
175
+
176
+ interface ToolCallInfo {
177
+ toolName: string;
178
+ input: unknown;
179
+ }
180
+
181
+ function buildToolCallMap(messages: ChatModelMessage[]): Map<string, ToolCallInfo> {
182
+ const map = new Map<string, ToolCallInfo>();
183
+
184
+ for (const msg of messages) {
185
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
186
+ for (const part of msg.content) {
187
+ if (isToolCallPart(part)) {
188
+ map.set(part.toolCallId, { toolName: part.toolName, input: part.input });
189
+ }
190
+ }
191
+ }
192
+
193
+ return map;
194
+ }
195
+
196
+ function maskReadFile(input: unknown, charCount: number): string {
197
+ const path = getStringField(input, "path", "unknown");
198
+ return `[File read: ${path} — content omitted (${charCount} chars)]`;
199
+ }
200
+
201
+ function maskBash(input: unknown, output: unknown, charCount: number): string {
202
+ const cmd = truncate(getStringField(input, "command", "unknown"), 80);
203
+ let exitCode = "?";
204
+ const parsed = tryParseJson(output);
205
+ if (isRecord(parsed) && "exitCode" in parsed) {
206
+ exitCode = String(parsed.exitCode);
207
+ }
208
+ return `[Command: ${cmd} — exit ${exitCode}, output omitted (${charCount} chars)]`;
209
+ }
210
+
211
+ function maskWebSearch(output: unknown): unknown {
212
+ const parsed = tryParseJson(output);
213
+ if (!Array.isArray(parsed)) return output;
214
+ return parsed.map((item: unknown) => {
215
+ if (!isRecord(item)) return item;
216
+ const { encryptedContent: _, ...rest } = item;
217
+ return rest;
218
+ });
219
+ }
220
+
221
+ function maskWebFetch(input: unknown, charCount: number): string {
222
+ const url = getStringField(input, "url", "unknown");
223
+ return `[Fetched: ${url} — content omitted (${charCount} chars)]`;
224
+ }
225
+
226
+ function maskTask(output: unknown, charCount: number): unknown {
227
+ const parsed = tryParseJson(output);
228
+ if (!isRecord(parsed)) {
229
+ return `[task output omitted (${charCount} chars)]`;
230
+ }
231
+
232
+ const masked: Record<string, unknown> = {};
233
+
234
+ if ("success" in parsed) masked.success = parsed.success;
235
+ if ("description" in parsed) masked.description = parsed.description;
236
+ if ("result" in parsed) {
237
+ masked.result = typeof parsed.result === "string"
238
+ ? truncate(parsed.result, 500)
239
+ : parsed.result;
240
+ }
241
+
242
+ return masked;
243
+ }
244
+
245
+ function maskGeneric(toolName: string, charCount: number): string {
246
+ return `[${toolName} output omitted (${charCount} chars)]`;
247
+ }
248
+
249
+ function getOutputValue(output: unknown): unknown {
250
+ if (!isRecord(output)) return output;
251
+ if ((output.type === "text" || output.type === "json") && "value" in output) {
252
+ return output.value;
253
+ }
254
+ return output;
255
+ }
256
+
257
+ function wrapToolResultOutput(
258
+ original: ChatToolResultOutput,
259
+ newValue: unknown,
260
+ ): ChatToolResultOutput {
261
+ const textValue = typeof newValue === "string" ? newValue : JSON.stringify(newValue);
262
+ if (original.type === "text") {
263
+ return { ...original, value: textValue };
264
+ }
265
+ if (original.type === "json") {
266
+ return { type: "text", value: textValue };
267
+ }
268
+ return original;
269
+ }
270
+
271
+ export function maskOldToolOutputs(messages: ChatModelMessage[]): ChatModelMessage[] {
272
+ let lastUserIdx = -1;
273
+ for (let i = messages.length - 1; i >= 0; i--) {
274
+ if (messages[i].role === "user") {
275
+ lastUserIdx = i;
276
+ break;
277
+ }
278
+ }
279
+
280
+ if (lastUserIdx <= 0) return messages;
281
+
282
+ const toolCallMap = buildToolCallMap(messages);
283
+
284
+ return messages.map((msg, idx) => {
285
+ if (idx >= lastUserIdx) return msg;
286
+
287
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
288
+ const filtered = msg.content.filter((part) => !isReasoningPart(part));
289
+ if (filtered.length !== msg.content.length) {
290
+ return { ...msg, content: filtered };
291
+ }
292
+ return msg;
293
+ }
294
+
295
+ if (msg.role !== "tool" || !Array.isArray(msg.content)) return msg;
296
+
297
+ const newContent: ChatToolResultPart[] = msg.content.map((part) => {
298
+ if (part.type !== "tool-result") {
299
+ return part;
300
+ }
301
+
302
+ const rawValue = getOutputValue(part.output);
303
+ const charCount = serializedLength(rawValue);
304
+
305
+ if (charCount < MASK_THRESHOLD) return part;
306
+
307
+ const callInfo = toolCallMap.get(part.toolCallId);
308
+ const toolName = part.toolName || callInfo?.toolName || "unknown";
309
+ const input = callInfo?.input;
310
+
311
+ let masked: unknown;
312
+
313
+ switch (toolName) {
314
+ case "readFile":
315
+ case "get_file":
316
+ masked = maskReadFile(input, charCount);
317
+ break;
318
+ case "bash":
319
+ masked = maskBash(input, rawValue, charCount);
320
+ break;
321
+ case "web_search":
322
+ masked = maskWebSearch(rawValue);
323
+ break;
324
+ case "web_fetch":
325
+ masked = maskWebFetch(input, charCount);
326
+ break;
327
+ case "task":
328
+ masked = maskTask(rawValue, charCount);
329
+ break;
330
+ default:
331
+ masked = maskGeneric(toolName, charCount);
332
+ break;
333
+ }
334
+
335
+ return { ...part, output: wrapToolResultOutput(part.output, masked) };
336
+ });
337
+
338
+ return { ...msg, content: newContent };
339
+ });
340
+ }
341
+
342
+ function createSyntheticToolResult(toolCallId: string, toolName: string): ChatToolResultPart {
343
+ return {
344
+ type: "tool-result",
345
+ toolCallId,
346
+ toolName,
347
+ output: { type: "text", value: "[tool result unavailable]" },
348
+ };
349
+ }
350
+
351
+ export function repairToolPairs(messages: ChatModelMessage[]): ChatModelMessage[] {
352
+ const result = [...messages];
353
+ let mutated = false;
354
+
355
+ for (let index = 0; index < result.length; index++) {
356
+ const message = result[index];
357
+ if (message.role !== "assistant" || !Array.isArray(message.content)) {
358
+ continue;
359
+ }
360
+
361
+ const inlineResultIds = new Set<string>();
362
+ for (const part of message.content) {
363
+ if (isToolResultPart(part)) {
364
+ inlineResultIds.add(part.toolCallId);
365
+ }
366
+ }
367
+
368
+ const repairedContent: typeof message.content = [];
369
+ const regularToolCalls: Array<{ id: string; toolName: string }> = [];
370
+
371
+ for (const part of message.content) {
372
+ repairedContent.push(part);
373
+
374
+ if (!isToolCallPart(part)) {
375
+ continue;
376
+ }
377
+
378
+ const toolName = part.toolName ?? "unknown";
379
+
380
+ if (part.providerExecuted) {
381
+ if (!inlineResultIds.has(part.toolCallId)) {
382
+ repairedContent.push(createSyntheticToolResult(part.toolCallId, toolName));
383
+ mutated = true;
384
+ }
385
+ continue;
386
+ }
387
+
388
+ if (!inlineResultIds.has(part.toolCallId)) {
389
+ regularToolCalls.push({ id: part.toolCallId, toolName });
390
+ }
391
+ }
392
+
393
+ if (repairedContent.length !== message.content.length) {
394
+ result[index] = {
395
+ ...message,
396
+ content: repairedContent,
397
+ };
398
+ }
399
+
400
+ if (regularToolCalls.length === 0) {
401
+ continue;
402
+ }
403
+
404
+ const nextMessage = result[index + 1];
405
+ const immediateResultIds = new Set<string>();
406
+
407
+ if (nextMessage?.role === "tool" && Array.isArray(nextMessage.content)) {
408
+ for (const part of nextMessage.content) {
409
+ if (isToolResultPart(part)) {
410
+ immediateResultIds.add(part.toolCallId);
411
+ }
412
+ }
413
+ }
414
+
415
+ const unresolvedCalls = regularToolCalls.filter((toolCall) =>
416
+ !immediateResultIds.has(toolCall.id)
417
+ );
418
+ if (unresolvedCalls.length === 0) {
419
+ continue;
420
+ }
421
+
422
+ const movedResults = new Map<string, ChatToolResultPart>();
423
+
424
+ for (
425
+ let laterIndex = index + 2;
426
+ laterIndex < result.length && movedResults.size < unresolvedCalls.length;
427
+ laterIndex++
428
+ ) {
429
+ const laterMessage = result[laterIndex];
430
+ if (laterMessage?.role !== "tool" || !Array.isArray(laterMessage.content)) {
431
+ continue;
432
+ }
433
+
434
+ let removedFromLater = false;
435
+ const keptLaterContent = laterMessage.content.filter((part) => {
436
+ if (!isToolResultPart(part)) {
437
+ return true;
438
+ }
439
+
440
+ if (
441
+ !unresolvedCalls.some((toolCall) => toolCall.id === part.toolCallId) ||
442
+ movedResults.has(part.toolCallId)
443
+ ) {
444
+ return true;
445
+ }
446
+
447
+ movedResults.set(part.toolCallId, part);
448
+ removedFromLater = true;
449
+ return false;
450
+ });
451
+
452
+ if (!removedFromLater) {
453
+ continue;
454
+ }
455
+
456
+ mutated = true;
457
+ if (keptLaterContent.length === 0) {
458
+ result.splice(laterIndex, 1);
459
+ laterIndex--;
460
+ continue;
461
+ }
462
+
463
+ result[laterIndex] = {
464
+ ...laterMessage,
465
+ content: keptLaterContent,
466
+ };
467
+ }
468
+
469
+ const repairedResults = unresolvedCalls.map(
470
+ (toolCall) =>
471
+ movedResults.get(toolCall.id) ?? createSyntheticToolResult(toolCall.id, toolCall.toolName),
472
+ );
473
+
474
+ if (nextMessage?.role === "tool" && Array.isArray(nextMessage.content)) {
475
+ result[index + 1] = {
476
+ ...nextMessage,
477
+ content: [...repairedResults, ...nextMessage.content],
478
+ };
479
+ } else {
480
+ const toolMessage: ChatModelMessage = {
481
+ role: "tool",
482
+ content: repairedResults,
483
+ };
484
+ result.splice(index + 1, 0, toolMessage);
485
+ }
486
+ mutated = true;
487
+ }
488
+
489
+ return mutated ? result : messages;
490
+ }
491
+
492
+ export function estimateOverhead(instructions: unknown, toolCount: number): number {
493
+ const instructionTokens = estimateTokens(instructions);
494
+ return instructionTokens + toolCount * TOKENS_PER_TOOL;
495
+ }
496
+
497
+ export function ensureToolCallInputs(messages: ChatModelMessage[]): ChatModelMessage[] {
498
+ let mutated = false;
499
+
500
+ const result = messages.map((msg) => {
501
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) return msg;
502
+
503
+ let msgMutated = false;
504
+ const newContent = msg.content.map((part) => {
505
+ if (isToolCallPart(part)) {
506
+ const input = part.input;
507
+ if (
508
+ input === undefined || input === null || typeof input !== "object" || Array.isArray(input)
509
+ ) {
510
+ msgMutated = true;
511
+ return { ...part, input: {} };
512
+ }
513
+ }
514
+ return part;
515
+ });
516
+
517
+ if (msgMutated) {
518
+ mutated = true;
519
+ return { ...msg, content: newContent };
520
+ }
521
+ return msg;
522
+ });
523
+
524
+ return mutated ? result : messages;
525
+ }
526
+
527
+ export function compactForStep(
528
+ messages: ChatModelMessage[],
529
+ overhead: number = 0,
530
+ ): ChatModelMessage[] {
531
+ const compacted = enforceTokenBudget(
532
+ maskOldToolOutputs(messages),
533
+ DEFAULT_TOKEN_BUDGET,
534
+ overhead,
535
+ );
536
+
537
+ let end = compacted.length;
538
+ while (end > 1 && compacted[end - 1].role === "assistant") {
539
+ end--;
540
+ }
541
+
542
+ const trimmed = end < compacted.length ? compacted.slice(0, end) : compacted;
543
+
544
+ return ensureToolCallInputs(repairToolPairs(dedupeToolHistory(trimmed)));
545
+ }
546
+
547
+ export function dedupeToolHistory(messages: ChatModelMessage[]): ChatModelMessage[] {
548
+ const seenToolCallIds = new Set<string>();
549
+ const seenToolResultIds = new Set<string>();
550
+ let mutated = false;
551
+
552
+ const deduped: ChatModelMessage[] = [];
553
+
554
+ const filterParts = <T>(parts: T[]): { filtered: T[]; changed: boolean } => {
555
+ const filtered = parts.filter((part) => {
556
+ if (isToolCallPart(part)) {
557
+ if (seenToolCallIds.has(part.toolCallId)) {
558
+ mutated = true;
559
+ return false;
560
+ }
561
+ seenToolCallIds.add(part.toolCallId);
562
+ return true;
563
+ }
564
+ if (isToolResultPart(part)) {
565
+ if (seenToolResultIds.has(part.toolCallId)) {
566
+ mutated = true;
567
+ return false;
568
+ }
569
+ seenToolResultIds.add(part.toolCallId);
570
+ return true;
571
+ }
572
+ return true;
573
+ });
574
+ return { filtered, changed: filtered.length !== parts.length };
575
+ };
576
+
577
+ for (const message of messages) {
578
+ if (message.role === "user" && Array.isArray(message.content)) {
579
+ const { filtered, changed } = filterParts(message.content);
580
+ if (!changed) {
581
+ deduped.push(message);
582
+ continue;
583
+ }
584
+ if (filtered.length > 0) deduped.push({ ...message, content: filtered });
585
+ } else if (message.role === "assistant" && Array.isArray(message.content)) {
586
+ const { filtered, changed } = filterParts(message.content);
587
+ if (!changed) {
588
+ deduped.push(message);
589
+ continue;
590
+ }
591
+ if (filtered.length > 0) deduped.push({ ...message, content: filtered });
592
+ } else if (message.role === "tool") {
593
+ const { filtered, changed } = filterParts(message.content);
594
+ if (!changed) {
595
+ deduped.push(message);
596
+ continue;
597
+ }
598
+ if (filtered.length > 0) deduped.push({ ...message, content: filtered });
599
+ } else {
600
+ deduped.push(message);
601
+ }
602
+ }
603
+
604
+ return mutated ? deduped : messages;
605
+ }
606
+
607
+ export function enforceTokenBudget(
608
+ messages: ChatModelMessage[],
609
+ budget: number = DEFAULT_TOKEN_BUDGET,
610
+ overhead: number = 0,
611
+ ): ChatModelMessage[] {
612
+ if (messages.length === 0) {
613
+ return messages;
614
+ }
615
+
616
+ return enforceTokenBudgetWithTurnCompression(messages, budget, overhead);
617
+ }
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.287";
3
+ export const VERSION = "0.1.288";