chainlesschain 0.45.11 → 0.45.19
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/package.json +1 -1
- package/src/assets/web-panel/assets/AppLayout-B00RARl2.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +1 -0
- package/src/assets/web-panel/assets/{Chat-5f__rMCR.js → Chat-DXtvKoM0.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-C4mrNC4c.js → Cron-BJ4ODHOy.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Logs-CC_Zuh66.js → Logs-CSeKZEG_.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-B15GiN3u.js → McpTools-BYQAK11r.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-Dbd7oLOH.js → Memory-gkUAPyuZ.js} +2 -2
- package/src/assets/web-panel/assets/{Notes-CEkc49fY.js → Notes-bjNrQgAo.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CjyPHW00.js → Providers-Dbf57Tbv.js} +1 -1
- package/src/assets/web-panel/assets/{Services-XFzHMRRd.js → Services-CS0oMdxh.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-D8oxmB3U.js → Skills-B2fgruv8.js} +1 -1
- package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
- package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
- package/src/assets/web-panel/assets/{antd-ChLPLhSn.js → antd-CJSBocer.js} +1 -1
- package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
- package/src/assets/web-panel/assets/{index-DQ5xXK7O.js → index-CF2CqPYX.js} +2 -2
- package/src/assets/web-panel/assets/{markdown-DtbPhnFe.js → markdown-Bo5cVN4u.js} +1 -1
- package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent.js +7 -8
- package/src/commands/chat.js +9 -11
- package/src/commands/serve.js +11 -106
- package/src/commands/session.js +185 -18
- package/src/commands/ui.js +10 -151
- package/src/gateways/repl/agent-repl.js +1 -0
- package/src/gateways/repl/chat-repl.js +1 -0
- package/src/gateways/ui/web-ui-server.js +1 -0
- package/src/gateways/ws/action-protocol.js +83 -0
- package/src/gateways/ws/message-dispatcher.js +73 -0
- package/src/gateways/ws/session-protocol.js +396 -0
- package/src/gateways/ws/task-protocol.js +55 -0
- package/src/gateways/ws/worktree-protocol.js +315 -0
- package/src/gateways/ws/ws-server.js +4 -0
- package/src/gateways/ws/ws-session-gateway.js +1 -0
- package/src/harness/background-task-manager.js +506 -0
- package/src/harness/background-task-worker.js +48 -0
- package/src/harness/compression-telemetry.js +214 -0
- package/src/harness/feature-flags.js +157 -0
- package/src/harness/jsonl-session-store.js +452 -0
- package/src/harness/prompt-compressor.js +416 -0
- package/src/harness/worktree-isolator.js +845 -0
- package/src/lib/agent-core.js +246 -45
- package/src/lib/background-task-manager.js +1 -305
- package/src/lib/background-task-worker.js +1 -50
- package/src/lib/compression-telemetry.js +5 -0
- package/src/lib/feature-flags.js +7 -182
- package/src/lib/interaction-adapter.js +32 -6
- package/src/lib/jsonl-session-store.js +21 -237
- package/src/lib/prompt-compressor.js +10 -351
- package/src/lib/sub-agent-context.js +91 -0
- package/src/lib/worktree-isolator.js +13 -231
- package/src/lib/ws-agent-handler.js +1 -0
- package/src/lib/ws-server.js +155 -359
- package/src/lib/ws-session-manager.js +82 -1
- package/src/repl/agent-repl.js +114 -32
- package/src/runtime/agent-runtime.js +417 -0
- package/src/runtime/contracts/agent-turn.js +11 -0
- package/src/runtime/contracts/session-record.js +31 -0
- package/src/runtime/contracts/task-record.js +18 -0
- package/src/runtime/contracts/telemetry-record.js +23 -0
- package/src/runtime/contracts/worktree-record.js +14 -0
- package/src/runtime/index.js +13 -0
- package/src/runtime/policies/agent-policy.js +45 -0
- package/src/runtime/runtime-context.js +14 -0
- package/src/runtime/runtime-events.js +37 -0
- package/src/runtime/runtime-factory.js +50 -0
- package/src/tools/index.js +22 -0
- package/src/tools/legacy-agent-tools.js +171 -0
- package/src/tools/registry.js +141 -0
- package/src/tools/tool-context.js +28 -0
- package/src/tools/tool-permissions.js +28 -0
- package/src/tools/tool-telemetry.js +39 -0
- package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-CjgO-ML6.css +0 -1
- package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
- package/src/assets/web-panel/assets/Dashboard-DsjXpZor.js +0 -3
- package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
- package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Prompt Compressor — 5 strategies for context window management.
|
|
3
|
+
*
|
|
4
|
+
* Strategies:
|
|
5
|
+
* 1. deduplication — Remove duplicate/similar messages (Jaccard similarity)
|
|
6
|
+
* 2. truncation — Keep most recent N messages
|
|
7
|
+
* 3. summarization — LLM-generated summary of old history
|
|
8
|
+
* 4. snipCompact — Remove stale tool results and processed markers
|
|
9
|
+
* 5. contextCollapse — Fold consecutive same-type messages into summaries
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { feature, featureVariant } from "../lib/feature-flags.js";
|
|
14
|
+
|
|
15
|
+
export function estimateTokens(text) {
|
|
16
|
+
if (!text) return 0;
|
|
17
|
+
const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
|
|
18
|
+
const otherChars = text.length - chineseChars;
|
|
19
|
+
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function estimateMessagesTokens(messages) {
|
|
23
|
+
return messages.reduce((sum, msg) => {
|
|
24
|
+
const content =
|
|
25
|
+
typeof msg.content === "string"
|
|
26
|
+
? msg.content
|
|
27
|
+
: JSON.stringify(msg.content || "");
|
|
28
|
+
return sum + estimateTokens(content);
|
|
29
|
+
}, 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function jaccardSimilarity(str1, str2) {
|
|
33
|
+
if (!str1 || !str2) return 0;
|
|
34
|
+
if (str1 === str2) return 1;
|
|
35
|
+
const tokens1 = new Set(str1.split(""));
|
|
36
|
+
const tokens2 = new Set(str2.split(""));
|
|
37
|
+
let intersection = 0;
|
|
38
|
+
for (const t of tokens1) {
|
|
39
|
+
if (tokens2.has(t)) intersection++;
|
|
40
|
+
}
|
|
41
|
+
return intersection / (tokens1.size + tokens2.size - intersection);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getContent(msg) {
|
|
45
|
+
return typeof msg.content === "string"
|
|
46
|
+
? msg.content
|
|
47
|
+
: JSON.stringify(msg.content || "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const CONTEXT_WINDOWS = {
|
|
51
|
+
"qwen2.5:7b": 32768,
|
|
52
|
+
"qwen2.5:14b": 32768,
|
|
53
|
+
"qwen2.5-coder:14b": 32768,
|
|
54
|
+
"qwen2:7b": 32768,
|
|
55
|
+
"llama3:8b": 8192,
|
|
56
|
+
"mistral:7b": 32768,
|
|
57
|
+
"codellama:7b": 16384,
|
|
58
|
+
"gpt-4o": 128000,
|
|
59
|
+
"gpt-4o-mini": 128000,
|
|
60
|
+
"gpt-4-turbo": 128000,
|
|
61
|
+
"gpt-3.5-turbo": 16385,
|
|
62
|
+
o1: 200000,
|
|
63
|
+
"claude-opus-4-6": 200000,
|
|
64
|
+
"claude-sonnet-4-6": 200000,
|
|
65
|
+
"claude-haiku-4-5-20251001": 200000,
|
|
66
|
+
"deepseek-chat": 64000,
|
|
67
|
+
"deepseek-coder": 64000,
|
|
68
|
+
"deepseek-reasoner": 64000,
|
|
69
|
+
"qwen-turbo": 131072,
|
|
70
|
+
"qwen-plus": 131072,
|
|
71
|
+
"qwen-max": 32768,
|
|
72
|
+
"gemini-2.0-flash": 1048576,
|
|
73
|
+
"gemini-2.0-pro": 1048576,
|
|
74
|
+
"gemini-1.5-flash": 1048576,
|
|
75
|
+
"moonshot-v1-auto": 131072,
|
|
76
|
+
"moonshot-v1-8k": 8192,
|
|
77
|
+
"moonshot-v1-32k": 32768,
|
|
78
|
+
"moonshot-v1-128k": 131072,
|
|
79
|
+
"doubao-seed-1-6-251015": 32768,
|
|
80
|
+
_provider_defaults: {
|
|
81
|
+
ollama: 32768,
|
|
82
|
+
openai: 128000,
|
|
83
|
+
anthropic: 200000,
|
|
84
|
+
deepseek: 64000,
|
|
85
|
+
dashscope: 131072,
|
|
86
|
+
gemini: 1048576,
|
|
87
|
+
kimi: 131072,
|
|
88
|
+
volcengine: 32768,
|
|
89
|
+
minimax: 32768,
|
|
90
|
+
mistral: 32768,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function getContextWindow(model, provider) {
|
|
95
|
+
if (model && CONTEXT_WINDOWS[model]) {
|
|
96
|
+
return CONTEXT_WINDOWS[model];
|
|
97
|
+
}
|
|
98
|
+
if (provider && CONTEXT_WINDOWS._provider_defaults[provider]) {
|
|
99
|
+
return CONTEXT_WINDOWS._provider_defaults[provider];
|
|
100
|
+
}
|
|
101
|
+
return 32768;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const COMPRESSION_VARIANTS = {
|
|
105
|
+
aggressive: { tokenFactor: 0.4, messageFactor: 0.7 },
|
|
106
|
+
balanced: { tokenFactor: 0.6, messageFactor: 1.0 },
|
|
107
|
+
relaxed: { tokenFactor: 0.75, messageFactor: 1.3 },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export function getCompressionVariant() {
|
|
111
|
+
if (!feature("COMPRESSION_AB")) return null;
|
|
112
|
+
const variant = featureVariant("COMPRESSION_AB") || "balanced";
|
|
113
|
+
return {
|
|
114
|
+
variant,
|
|
115
|
+
...(COMPRESSION_VARIANTS[variant] || COMPRESSION_VARIANTS.balanced),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function adaptiveThresholds(contextWindow) {
|
|
120
|
+
const abVariant = getCompressionVariant();
|
|
121
|
+
const tokenFactor = abVariant ? abVariant.tokenFactor : 0.6;
|
|
122
|
+
|
|
123
|
+
const maxTokens = Math.floor(contextWindow * tokenFactor);
|
|
124
|
+
let maxMessages = Math.min(
|
|
125
|
+
50,
|
|
126
|
+
Math.max(15, Math.floor(10 + Math.log2(contextWindow / 1024) * 5)),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (abVariant) {
|
|
130
|
+
maxMessages = Math.min(
|
|
131
|
+
50,
|
|
132
|
+
Math.max(15, Math.round(maxMessages * abVariant.messageFactor)),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const aggressive = contextWindow < 32768;
|
|
137
|
+
|
|
138
|
+
const result = { maxMessages, maxTokens, aggressive };
|
|
139
|
+
if (abVariant) result.variant = abVariant.variant;
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class PromptCompressor {
|
|
144
|
+
constructor(options = {}) {
|
|
145
|
+
if (
|
|
146
|
+
(options.model || options.provider) &&
|
|
147
|
+
!options.maxMessages &&
|
|
148
|
+
!options.maxTokens
|
|
149
|
+
) {
|
|
150
|
+
const ctxWindow = getContextWindow(options.model, options.provider);
|
|
151
|
+
const adaptive = adaptiveThresholds(ctxWindow);
|
|
152
|
+
this.maxMessages = adaptive.maxMessages;
|
|
153
|
+
this.maxTokens = adaptive.maxTokens;
|
|
154
|
+
this._adaptive = true;
|
|
155
|
+
this._contextWindow = ctxWindow;
|
|
156
|
+
} else {
|
|
157
|
+
this.maxMessages = options.maxMessages || 20;
|
|
158
|
+
this.maxTokens = options.maxTokens || 8000;
|
|
159
|
+
this._adaptive = false;
|
|
160
|
+
this._contextWindow = null;
|
|
161
|
+
}
|
|
162
|
+
this.similarityThreshold = options.similarityThreshold || 0.9;
|
|
163
|
+
this.llmQuery = options.llmQuery || null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
adaptToModel(model, provider) {
|
|
167
|
+
const ctxWindow = getContextWindow(model, provider);
|
|
168
|
+
const adaptive = adaptiveThresholds(ctxWindow);
|
|
169
|
+
this.maxMessages = adaptive.maxMessages;
|
|
170
|
+
this.maxTokens = adaptive.maxTokens;
|
|
171
|
+
this._adaptive = true;
|
|
172
|
+
this._contextWindow = ctxWindow;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async compress(messages, options = {}) {
|
|
176
|
+
if (!Array.isArray(messages) || messages.length <= 2) {
|
|
177
|
+
return {
|
|
178
|
+
messages: Array.isArray(messages) ? [...messages] : [],
|
|
179
|
+
stats: { strategy: "none", saved: 0 },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const originalTokens = estimateMessagesTokens(messages);
|
|
184
|
+
let result = [...messages];
|
|
185
|
+
const applied = [];
|
|
186
|
+
|
|
187
|
+
if (feature("CONTEXT_SNIP")) {
|
|
188
|
+
const before = result.length;
|
|
189
|
+
result = this._snipCompact(result);
|
|
190
|
+
if (result.length < before) applied.push("snip");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (result.length > 3) {
|
|
194
|
+
const before = result.length;
|
|
195
|
+
result = this._deduplicate(result);
|
|
196
|
+
if (result.length < before) applied.push("dedup");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (feature("CONTEXT_COLLAPSE") && result.length > 6) {
|
|
200
|
+
const before = result.length;
|
|
201
|
+
result = this._contextCollapse(result);
|
|
202
|
+
if (result.length < before) applied.push("collapse");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (result.length > this.maxMessages) {
|
|
206
|
+
result = this._truncate(result);
|
|
207
|
+
applied.push("truncate");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const currentTokens = estimateMessagesTokens(result);
|
|
211
|
+
if (this.llmQuery && currentTokens > this.maxTokens && result.length > 4) {
|
|
212
|
+
try {
|
|
213
|
+
result = await this._summarize(result);
|
|
214
|
+
applied.push("summarize");
|
|
215
|
+
} catch (_err) {
|
|
216
|
+
// Summarization failed — continue with what we have
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const compressedTokens = estimateMessagesTokens(result);
|
|
221
|
+
const stats = {
|
|
222
|
+
strategy: applied.join("+") || "none",
|
|
223
|
+
originalMessages: messages.length,
|
|
224
|
+
compressedMessages: result.length,
|
|
225
|
+
originalTokens,
|
|
226
|
+
compressedTokens,
|
|
227
|
+
saved: originalTokens - compressedTokens,
|
|
228
|
+
ratio: originalTokens > 0 ? compressedTokens / originalTokens : 1,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const abVariant = getCompressionVariant();
|
|
232
|
+
if (abVariant) {
|
|
233
|
+
stats.abVariant = abVariant.variant;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { messages: result, stats };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
shouldAutoCompact(messages) {
|
|
240
|
+
return (
|
|
241
|
+
messages.length > this.maxMessages ||
|
|
242
|
+
estimateMessagesTokens(messages) > this.maxTokens
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_deduplicate(messages) {
|
|
247
|
+
const system = messages.filter((m) => m.role === "system");
|
|
248
|
+
const last = [...messages].reverse().find((m) => m.role === "user");
|
|
249
|
+
const rest = messages.filter((m) => m.role !== "system" && m !== last);
|
|
250
|
+
|
|
251
|
+
const seen = new Map();
|
|
252
|
+
const deduped = [];
|
|
253
|
+
|
|
254
|
+
for (const msg of rest) {
|
|
255
|
+
const content = getContent(msg);
|
|
256
|
+
const hash = createHash("md5").update(content).digest("hex");
|
|
257
|
+
|
|
258
|
+
if (seen.has(hash)) continue;
|
|
259
|
+
|
|
260
|
+
let isDup = false;
|
|
261
|
+
for (const [, existing] of seen) {
|
|
262
|
+
if (
|
|
263
|
+
jaccardSimilarity(content, getContent(existing)) >=
|
|
264
|
+
this.similarityThreshold
|
|
265
|
+
) {
|
|
266
|
+
isDup = true;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!isDup) {
|
|
272
|
+
seen.set(hash, msg);
|
|
273
|
+
deduped.push(msg);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = [...system, ...deduped];
|
|
278
|
+
if (last && !result.includes(last)) result.push(last);
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_truncate(messages) {
|
|
283
|
+
const system = messages.filter((m) => m.role === "system");
|
|
284
|
+
const last = [...messages].reverse().find((m) => m.role === "user");
|
|
285
|
+
const rest = messages.filter((m) => m.role !== "system" && m !== last);
|
|
286
|
+
|
|
287
|
+
let slots = this.maxMessages - system.length;
|
|
288
|
+
if (last) slots -= 1;
|
|
289
|
+
|
|
290
|
+
const recent = rest.slice(-Math.max(slots, 1));
|
|
291
|
+
const result = [...system, ...recent];
|
|
292
|
+
if (last && !result.includes(last)) result.push(last);
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async _summarize(messages) {
|
|
297
|
+
const system = messages.filter((m) => m.role === "system");
|
|
298
|
+
const last = [...messages].reverse().find((m) => m.role === "user");
|
|
299
|
+
const toSummarize = messages.filter(
|
|
300
|
+
(m) => m.role !== "system" && m !== last,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (toSummarize.length < 3) return messages;
|
|
304
|
+
|
|
305
|
+
const historyText = toSummarize
|
|
306
|
+
.map((m) => `${m.role}: ${getContent(m).slice(0, 500)}`)
|
|
307
|
+
.join("\n");
|
|
308
|
+
|
|
309
|
+
const summary = await this.llmQuery(
|
|
310
|
+
`Summarize this conversation history concisely, preserving key facts and decisions:\n\n${historyText}\n\nSummary:`,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (!summary) return messages;
|
|
314
|
+
|
|
315
|
+
const result = [
|
|
316
|
+
...system,
|
|
317
|
+
{ role: "system", content: `[Conversation Summary]\n${summary}` },
|
|
318
|
+
];
|
|
319
|
+
if (last) result.push(last);
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
_snipCompact(messages) {
|
|
324
|
+
if (messages.length <= 4) return messages;
|
|
325
|
+
|
|
326
|
+
const head = messages.slice(0, 1);
|
|
327
|
+
const middle = messages.slice(1, -4);
|
|
328
|
+
const tail = messages.slice(-4);
|
|
329
|
+
|
|
330
|
+
const snipped = middle.filter((msg) => {
|
|
331
|
+
const content = getContent(msg);
|
|
332
|
+
|
|
333
|
+
if (!content || content.trim() === "") return false;
|
|
334
|
+
if (content.includes("[PROCESSED]") || content.includes("[STALE]")) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (msg.role === "tool") {
|
|
339
|
+
if (
|
|
340
|
+
content === "ok" ||
|
|
341
|
+
content === "{}" ||
|
|
342
|
+
content === "null" ||
|
|
343
|
+
content.length < 3
|
|
344
|
+
) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (msg.role === "assistant" && content.length < 10) return false;
|
|
350
|
+
return true;
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return [...head, ...snipped, ...tail];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
_contextCollapse(messages) {
|
|
357
|
+
if (messages.length <= 6) return messages;
|
|
358
|
+
|
|
359
|
+
const result = [];
|
|
360
|
+
let i = 0;
|
|
361
|
+
|
|
362
|
+
while (i < messages.length) {
|
|
363
|
+
const msg = messages[i];
|
|
364
|
+
|
|
365
|
+
if (
|
|
366
|
+
i > 0 &&
|
|
367
|
+
i < messages.length - 3 &&
|
|
368
|
+
msg.role === "assistant" &&
|
|
369
|
+
msg.tool_calls &&
|
|
370
|
+
msg.tool_calls.length > 0
|
|
371
|
+
) {
|
|
372
|
+
const toolGroup = [msg];
|
|
373
|
+
let j = i + 1;
|
|
374
|
+
while (j < messages.length - 3 && messages[j].role === "tool") {
|
|
375
|
+
toolGroup.push(messages[j]);
|
|
376
|
+
j++;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
while (
|
|
380
|
+
j < messages.length - 3 &&
|
|
381
|
+
messages[j].role === "assistant" &&
|
|
382
|
+
messages[j].tool_calls
|
|
383
|
+
) {
|
|
384
|
+
toolGroup.push(messages[j]);
|
|
385
|
+
j++;
|
|
386
|
+
while (j < messages.length - 3 && messages[j].role === "tool") {
|
|
387
|
+
toolGroup.push(messages[j]);
|
|
388
|
+
j++;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (toolGroup.length >= 3) {
|
|
393
|
+
const toolNames = toolGroup
|
|
394
|
+
.filter((m) => m.tool_calls)
|
|
395
|
+
.flatMap((m) =>
|
|
396
|
+
m.tool_calls.map((tc) => tc.function?.name || "tool"),
|
|
397
|
+
)
|
|
398
|
+
.filter(Boolean);
|
|
399
|
+
const uniqueTools = [...new Set(toolNames)];
|
|
400
|
+
|
|
401
|
+
result.push({
|
|
402
|
+
role: "system",
|
|
403
|
+
content: `[Collapsed ${toolGroup.length} tool messages: ${uniqueTools.join(", ")}]`,
|
|
404
|
+
});
|
|
405
|
+
i = j;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
result.push(msg);
|
|
411
|
+
i++;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
}
|