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.
Files changed (81) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/AppLayout-B00RARl2.js +1 -0
  3. package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +1 -0
  4. package/src/assets/web-panel/assets/{Chat-5f__rMCR.js → Chat-DXtvKoM0.js} +1 -1
  5. package/src/assets/web-panel/assets/{Cron-C4mrNC4c.js → Cron-BJ4ODHOy.js} +1 -1
  6. package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
  7. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  8. package/src/assets/web-panel/assets/{Logs-CC_Zuh66.js → Logs-CSeKZEG_.js} +1 -1
  9. package/src/assets/web-panel/assets/{McpTools-B15GiN3u.js → McpTools-BYQAK11r.js} +2 -2
  10. package/src/assets/web-panel/assets/{Memory-Dbd7oLOH.js → Memory-gkUAPyuZ.js} +2 -2
  11. package/src/assets/web-panel/assets/{Notes-CEkc49fY.js → Notes-bjNrQgAo.js} +1 -1
  12. package/src/assets/web-panel/assets/{Providers-CjyPHW00.js → Providers-Dbf57Tbv.js} +1 -1
  13. package/src/assets/web-panel/assets/{Services-XFzHMRRd.js → Services-CS0oMdxh.js} +1 -1
  14. package/src/assets/web-panel/assets/{Skills-D8oxmB3U.js → Skills-B2fgruv8.js} +1 -1
  15. package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
  16. package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
  17. package/src/assets/web-panel/assets/{antd-ChLPLhSn.js → antd-CJSBocer.js} +1 -1
  18. package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
  19. package/src/assets/web-panel/assets/{index-DQ5xXK7O.js → index-CF2CqPYX.js} +2 -2
  20. package/src/assets/web-panel/assets/{markdown-DtbPhnFe.js → markdown-Bo5cVN4u.js} +1 -1
  21. package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
  22. package/src/assets/web-panel/index.html +2 -2
  23. package/src/commands/agent.js +7 -8
  24. package/src/commands/chat.js +9 -11
  25. package/src/commands/serve.js +11 -106
  26. package/src/commands/session.js +185 -18
  27. package/src/commands/ui.js +10 -151
  28. package/src/gateways/repl/agent-repl.js +1 -0
  29. package/src/gateways/repl/chat-repl.js +1 -0
  30. package/src/gateways/ui/web-ui-server.js +1 -0
  31. package/src/gateways/ws/action-protocol.js +83 -0
  32. package/src/gateways/ws/message-dispatcher.js +73 -0
  33. package/src/gateways/ws/session-protocol.js +396 -0
  34. package/src/gateways/ws/task-protocol.js +55 -0
  35. package/src/gateways/ws/worktree-protocol.js +315 -0
  36. package/src/gateways/ws/ws-server.js +4 -0
  37. package/src/gateways/ws/ws-session-gateway.js +1 -0
  38. package/src/harness/background-task-manager.js +506 -0
  39. package/src/harness/background-task-worker.js +48 -0
  40. package/src/harness/compression-telemetry.js +214 -0
  41. package/src/harness/feature-flags.js +157 -0
  42. package/src/harness/jsonl-session-store.js +452 -0
  43. package/src/harness/prompt-compressor.js +416 -0
  44. package/src/harness/worktree-isolator.js +845 -0
  45. package/src/lib/agent-core.js +246 -45
  46. package/src/lib/background-task-manager.js +1 -305
  47. package/src/lib/background-task-worker.js +1 -50
  48. package/src/lib/compression-telemetry.js +5 -0
  49. package/src/lib/feature-flags.js +7 -182
  50. package/src/lib/interaction-adapter.js +32 -6
  51. package/src/lib/jsonl-session-store.js +21 -237
  52. package/src/lib/prompt-compressor.js +10 -351
  53. package/src/lib/sub-agent-context.js +91 -0
  54. package/src/lib/worktree-isolator.js +13 -231
  55. package/src/lib/ws-agent-handler.js +1 -0
  56. package/src/lib/ws-server.js +155 -359
  57. package/src/lib/ws-session-manager.js +82 -1
  58. package/src/repl/agent-repl.js +114 -32
  59. package/src/runtime/agent-runtime.js +417 -0
  60. package/src/runtime/contracts/agent-turn.js +11 -0
  61. package/src/runtime/contracts/session-record.js +31 -0
  62. package/src/runtime/contracts/task-record.js +18 -0
  63. package/src/runtime/contracts/telemetry-record.js +23 -0
  64. package/src/runtime/contracts/worktree-record.js +14 -0
  65. package/src/runtime/index.js +13 -0
  66. package/src/runtime/policies/agent-policy.js +45 -0
  67. package/src/runtime/runtime-context.js +14 -0
  68. package/src/runtime/runtime-events.js +37 -0
  69. package/src/runtime/runtime-factory.js +50 -0
  70. package/src/tools/index.js +22 -0
  71. package/src/tools/legacy-agent-tools.js +171 -0
  72. package/src/tools/registry.js +141 -0
  73. package/src/tools/tool-context.js +28 -0
  74. package/src/tools/tool-permissions.js +28 -0
  75. package/src/tools/tool-telemetry.js +39 -0
  76. package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +0 -1
  77. package/src/assets/web-panel/assets/AppLayout-CjgO-ML6.css +0 -1
  78. package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
  79. package/src/assets/web-panel/assets/Dashboard-DsjXpZor.js +0 -3
  80. package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
  81. package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
@@ -1,237 +1,21 @@
1
- /**
2
- * JSONL Session Store — append-only session persistence.
3
- *
4
- * Each session is a file: .chainlesschain/sessions/{session-id}.jsonl
5
- * Each line: {"type":"...", "timestamp":..., "data":{...}}
6
- *
7
- * Types: session_start, user_message, assistant_message, tool_call,
8
- * tool_result, system, compact, session_end
9
- *
10
- * Feature-flag gated: JSONL_SESSION
11
- */
12
-
13
- import {
14
- existsSync,
15
- mkdirSync,
16
- appendFileSync,
17
- readFileSync,
18
- readdirSync,
19
- renameSync,
20
- } from "node:fs";
21
- import { join, basename } from "node:path";
22
- import { createHash } from "node:crypto";
23
- import { getHomeDir } from "./paths.js";
24
-
25
- function getSessionsDir() {
26
- const dir = join(getHomeDir(), "sessions");
27
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
28
- return dir;
29
- }
30
-
31
- function sessionPath(sessionId) {
32
- return join(getSessionsDir(), `${sessionId}.jsonl`);
33
- }
34
-
35
- // ── Write operations ────────────────────────────────────────────────────
36
-
37
- /**
38
- * Append a single event to the session log.
39
- * User messages are written synchronously for crash recovery.
40
- */
41
- export function appendEvent(sessionId, type, data) {
42
- const line = JSON.stringify({ type, timestamp: Date.now(), data }) + "\n";
43
- appendFileSync(sessionPath(sessionId), line, "utf-8");
44
- }
45
-
46
- /**
47
- * Start a new session (writes session_start event).
48
- */
49
- export function startSession(sessionId, meta = {}) {
50
- const id =
51
- sessionId ||
52
- `session-${Date.now()}-${createHash("sha256").update(Math.random().toString()).digest("hex").slice(0, 6)}`;
53
-
54
- appendEvent(id, "session_start", {
55
- title: meta.title || "Untitled",
56
- provider: meta.provider || "",
57
- model: meta.model || "",
58
- });
59
-
60
- return id;
61
- }
62
-
63
- /**
64
- * Append a user message (synchronous write for crash recovery).
65
- */
66
- export function appendUserMessage(sessionId, content) {
67
- appendEvent(sessionId, "user_message", { role: "user", content });
68
- }
69
-
70
- /**
71
- * Append an assistant message.
72
- */
73
- export function appendAssistantMessage(sessionId, content) {
74
- appendEvent(sessionId, "assistant_message", { role: "assistant", content });
75
- }
76
-
77
- /**
78
- * Append a tool call event.
79
- */
80
- export function appendToolCall(sessionId, toolName, args) {
81
- appendEvent(sessionId, "tool_call", { tool: toolName, args });
82
- }
83
-
84
- /**
85
- * Append a tool result event.
86
- */
87
- export function appendToolResult(sessionId, toolName, result) {
88
- appendEvent(sessionId, "tool_result", { tool: toolName, result });
89
- }
90
-
91
- /**
92
- * Append a compact event (context was compressed).
93
- */
94
- export function appendCompactEvent(sessionId, stats) {
95
- appendEvent(sessionId, "compact", stats);
96
- }
97
-
98
- // ── Read operations ─────────────────────────────────────────────────────
99
-
100
- /**
101
- * Read all events from a session file.
102
- * @returns {Array<{type, timestamp, data}>}
103
- */
104
- export function readEvents(sessionId) {
105
- const filePath = sessionPath(sessionId);
106
- if (!existsSync(filePath)) return [];
107
-
108
- const content = readFileSync(filePath, "utf-8");
109
- const events = [];
110
-
111
- for (const line of content.split("\n")) {
112
- if (!line.trim()) continue;
113
- try {
114
- events.push(JSON.parse(line));
115
- } catch (_e) {
116
- // Skip malformed lines
117
- }
118
- }
119
-
120
- return events;
121
- }
122
-
123
- /**
124
- * Reconstruct messages array from JSONL events (for API calls).
125
- * Skips compact events and rebuilds the current conversation state.
126
- */
127
- export function rebuildMessages(sessionId) {
128
- const events = readEvents(sessionId);
129
- const messages = [];
130
- let lastCompactIndex = -1;
131
-
132
- // Find the last compact event (start from there)
133
- for (let i = events.length - 1; i >= 0; i--) {
134
- if (events[i].type === "compact" && events[i].data.messages) {
135
- lastCompactIndex = i;
136
- break;
137
- }
138
- }
139
-
140
- if (lastCompactIndex >= 0 && events[lastCompactIndex].data.messages) {
141
- messages.push(...events[lastCompactIndex].data.messages);
142
- }
143
-
144
- // Replay events after last compact
145
- const startIndex = lastCompactIndex >= 0 ? lastCompactIndex + 1 : 0;
146
-
147
- for (let i = startIndex; i < events.length; i++) {
148
- const event = events[i];
149
- if (
150
- event.type === "user_message" ||
151
- event.type === "assistant_message" ||
152
- event.type === "system"
153
- ) {
154
- messages.push(event.data);
155
- }
156
- }
157
-
158
- return messages;
159
- }
160
-
161
- /**
162
- * List all sessions (reads session_start events from all .jsonl files).
163
- */
164
- export function listJsonlSessions(options = {}) {
165
- const dir = getSessionsDir();
166
- if (!existsSync(dir)) return [];
167
-
168
- const limit = options.limit || 20;
169
- const files = readdirSync(dir)
170
- .filter((f) => f.endsWith(".jsonl"))
171
- .map((f) => {
172
- const id = basename(f, ".jsonl");
173
- const events = readEvents(id);
174
- const startEvent = events.find((e) => e.type === "session_start");
175
- const lastEvent = events[events.length - 1];
176
- const messageCount = events.filter(
177
- (e) => e.type === "user_message" || e.type === "assistant_message",
178
- ).length;
179
-
180
- return {
181
- id,
182
- title: startEvent?.data?.title || "Untitled",
183
- provider: startEvent?.data?.provider || "",
184
- model: startEvent?.data?.model || "",
185
- message_count: messageCount,
186
- created_at: startEvent
187
- ? new Date(startEvent.timestamp).toISOString()
188
- : "",
189
- updated_at: lastEvent
190
- ? new Date(lastEvent.timestamp).toISOString()
191
- : "",
192
- };
193
- })
194
- .sort((a, b) => (b.updated_at > a.updated_at ? 1 : -1))
195
- .slice(0, limit);
196
-
197
- return files;
198
- }
199
-
200
- /**
201
- * Fork a session — copies all events to a new session ID.
202
- */
203
- export function forkSession(sourceId) {
204
- const events = readEvents(sourceId);
205
- if (events.length === 0) return null;
206
-
207
- const newId = `session-${Date.now()}-${createHash("sha256").update(Math.random().toString()).digest("hex").slice(0, 6)}`;
208
- const filePath = sessionPath(newId);
209
-
210
- for (const event of events) {
211
- const line = JSON.stringify(event) + "\n";
212
- appendFileSync(filePath, line, "utf-8");
213
- }
214
-
215
- // Mark the fork
216
- appendEvent(newId, "system", {
217
- role: "system",
218
- content: `[Forked from session ${sourceId}]`,
219
- });
220
-
221
- return newId;
222
- }
223
-
224
- /**
225
- * Check if a JSONL session exists.
226
- */
227
- export function sessionExists(sessionId) {
228
- return existsSync(sessionPath(sessionId));
229
- }
230
-
231
- /**
232
- * Get the most recent session ID (for --continue).
233
- */
234
- export function getLastSessionId() {
235
- const sessions = listJsonlSessions({ limit: 1 });
236
- return sessions.length > 0 ? sessions[0].id : null;
237
- }
1
+ export {
2
+ appendEvent,
3
+ startSession,
4
+ appendUserMessage,
5
+ appendAssistantMessage,
6
+ appendToolCall,
7
+ appendToolResult,
8
+ appendCompactEvent,
9
+ readEvents,
10
+ rebuildMessages,
11
+ listJsonlSessions,
12
+ forkSession,
13
+ sessionExists,
14
+ getLastSessionId,
15
+ migrateLegacySessions,
16
+ migrateLegacySessionsBatch,
17
+ migrateLegacySessionFile,
18
+ validateJsonlSession,
19
+ validateAllJsonlSessions,
20
+ sampleMigratedSessionsValidation,
21
+ } from "../harness/jsonl-session-store.js";
@@ -1,351 +1,10 @@
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
- * Feature-flag gated: PROMPT_COMPRESSOR, CONTEXT_SNIP, CONTEXT_COLLAPSE
12
- */
13
-
14
- import { createHash } from "node:crypto";
15
- import { feature } from "./feature-flags.js";
16
-
17
- // ── Token estimation ────────────────────────────────────────────────────
18
-
19
- /**
20
- * Estimate token count for a string.
21
- * Chinese: ~1.5 chars/token, English: ~4 chars/token.
22
- */
23
- export function estimateTokens(text) {
24
- if (!text) return 0;
25
- const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
26
- const otherChars = text.length - chineseChars;
27
- return Math.ceil(chineseChars / 1.5 + otherChars / 4);
28
- }
29
-
30
- /**
31
- * Estimate total tokens for a messages array.
32
- */
33
- export function estimateMessagesTokens(messages) {
34
- return messages.reduce((sum, msg) => {
35
- const content =
36
- typeof msg.content === "string"
37
- ? msg.content
38
- : JSON.stringify(msg.content || "");
39
- return sum + estimateTokens(content);
40
- }, 0);
41
- }
42
-
43
- // ── Similarity ──────────────────────────────────────────────────────────
44
-
45
- function jaccardSimilarity(str1, str2) {
46
- if (!str1 || !str2) return 0;
47
- if (str1 === str2) return 1;
48
- const tokens1 = new Set(str1.split(""));
49
- const tokens2 = new Set(str2.split(""));
50
- let intersection = 0;
51
- for (const t of tokens1) {
52
- if (tokens2.has(t)) intersection++;
53
- }
54
- return intersection / (tokens1.size + tokens2.size - intersection);
55
- }
56
-
57
- function getContent(msg) {
58
- return typeof msg.content === "string"
59
- ? msg.content
60
- : JSON.stringify(msg.content || "");
61
- }
62
-
63
- // ── PromptCompressor class ──────────────────────────────────────────────
64
-
65
- export class PromptCompressor {
66
- /**
67
- * @param {object} options
68
- * @param {number} [options.maxMessages=20] — Max messages before truncation
69
- * @param {number} [options.maxTokens=8000] — Token threshold for auto-compact
70
- * @param {number} [options.similarityThreshold=0.9] — Dedup similarity threshold
71
- * @param {Function} [options.llmQuery] — async (prompt) => string, for summarization
72
- */
73
- constructor(options = {}) {
74
- this.maxMessages = options.maxMessages || 20;
75
- this.maxTokens = options.maxTokens || 8000;
76
- this.similarityThreshold = options.similarityThreshold || 0.9;
77
- this.llmQuery = options.llmQuery || null;
78
- }
79
-
80
- /**
81
- * Run all enabled compression strategies on messages.
82
- * Returns { messages, stats }.
83
- */
84
- async compress(messages, options = {}) {
85
- if (!Array.isArray(messages) || messages.length <= 2) {
86
- return {
87
- messages: Array.isArray(messages) ? [...messages] : [],
88
- stats: { strategy: "none", saved: 0 },
89
- };
90
- }
91
-
92
- const originalTokens = estimateMessagesTokens(messages);
93
- let result = [...messages];
94
- const applied = [];
95
-
96
- // Strategy 4: snipCompact (remove stale tool results)
97
- if (feature("CONTEXT_SNIP")) {
98
- const before = result.length;
99
- result = this._snipCompact(result);
100
- if (result.length < before) applied.push("snip");
101
- }
102
-
103
- // Strategy 1: deduplication
104
- if (result.length > 3) {
105
- const before = result.length;
106
- result = this._deduplicate(result);
107
- if (result.length < before) applied.push("dedup");
108
- }
109
-
110
- // Strategy 5: contextCollapse (fold consecutive tool results)
111
- if (feature("CONTEXT_COLLAPSE") && result.length > 6) {
112
- const before = result.length;
113
- result = this._contextCollapse(result);
114
- if (result.length < before) applied.push("collapse");
115
- }
116
-
117
- // Strategy 2: truncation
118
- if (result.length > this.maxMessages) {
119
- result = this._truncate(result);
120
- applied.push("truncate");
121
- }
122
-
123
- // Strategy 3: summarization (only if still over token limit)
124
- const currentTokens = estimateMessagesTokens(result);
125
- if (this.llmQuery && currentTokens > this.maxTokens && result.length > 4) {
126
- try {
127
- result = await this._summarize(result);
128
- applied.push("summarize");
129
- } catch (_err) {
130
- // Summarization failed — continue with what we have
131
- }
132
- }
133
-
134
- const compressedTokens = estimateMessagesTokens(result);
135
- return {
136
- messages: result,
137
- stats: {
138
- strategy: applied.join("+") || "none",
139
- originalMessages: messages.length,
140
- compressedMessages: result.length,
141
- originalTokens,
142
- compressedTokens,
143
- saved: originalTokens - compressedTokens,
144
- ratio: originalTokens > 0 ? compressedTokens / originalTokens : 1,
145
- },
146
- };
147
- }
148
-
149
- /**
150
- * Check if auto-compact should trigger.
151
- */
152
- shouldAutoCompact(messages) {
153
- return (
154
- messages.length > this.maxMessages ||
155
- estimateMessagesTokens(messages) > this.maxTokens
156
- );
157
- }
158
-
159
- // ── Strategy 1: Deduplication ───────────────────────────────────────
160
-
161
- _deduplicate(messages) {
162
- const system = messages.filter((m) => m.role === "system");
163
- const last = [...messages].reverse().find((m) => m.role === "user");
164
- const rest = messages.filter((m) => m.role !== "system" && m !== last);
165
-
166
- const seen = new Map();
167
- const deduped = [];
168
-
169
- for (const msg of rest) {
170
- const content = getContent(msg);
171
- const hash = createHash("md5").update(content).digest("hex");
172
-
173
- if (seen.has(hash)) continue;
174
-
175
- let isDup = false;
176
- for (const [, existing] of seen) {
177
- if (
178
- jaccardSimilarity(content, getContent(existing)) >=
179
- this.similarityThreshold
180
- ) {
181
- isDup = true;
182
- break;
183
- }
184
- }
185
-
186
- if (!isDup) {
187
- seen.set(hash, msg);
188
- deduped.push(msg);
189
- }
190
- }
191
-
192
- const result = [...system, ...deduped];
193
- if (last && !result.includes(last)) result.push(last);
194
- return result;
195
- }
196
-
197
- // ── Strategy 2: Truncation ──────────────────────────────────────────
198
-
199
- _truncate(messages) {
200
- const system = messages.filter((m) => m.role === "system");
201
- const last = [...messages].reverse().find((m) => m.role === "user");
202
- const rest = messages.filter((m) => m.role !== "system" && m !== last);
203
-
204
- let slots = this.maxMessages - system.length;
205
- if (last) slots -= 1;
206
-
207
- const recent = rest.slice(-Math.max(slots, 1));
208
- const result = [...system, ...recent];
209
- if (last && !result.includes(last)) result.push(last);
210
- return result;
211
- }
212
-
213
- // ── Strategy 3: Summarization ───────────────────────────────────────
214
-
215
- async _summarize(messages) {
216
- const system = messages.filter((m) => m.role === "system");
217
- const last = [...messages].reverse().find((m) => m.role === "user");
218
- const toSummarize = messages.filter(
219
- (m) => m.role !== "system" && m !== last,
220
- );
221
-
222
- if (toSummarize.length < 3) return messages;
223
-
224
- const historyText = toSummarize
225
- .map((m) => `${m.role}: ${getContent(m).slice(0, 500)}`)
226
- .join("\n");
227
-
228
- const summary = await this.llmQuery(
229
- `Summarize this conversation history concisely, preserving key facts and decisions:\n\n${historyText}\n\nSummary:`,
230
- );
231
-
232
- if (!summary) return messages;
233
-
234
- const result = [
235
- ...system,
236
- { role: "system", content: `[Conversation Summary]\n${summary}` },
237
- ];
238
- if (last) result.push(last);
239
- return result;
240
- }
241
-
242
- // ── Strategy 4: Snip Compact ────────────────────────────────────────
243
- // Removes stale markers: processed tool results older than recent window,
244
- // empty assistant messages, and system messages with [PROCESSED] tags.
245
-
246
- _snipCompact(messages) {
247
- if (messages.length <= 4) return messages;
248
-
249
- // Keep system[0] + last 4 messages untouched
250
- const head = messages.slice(0, 1);
251
- const middle = messages.slice(1, -4);
252
- const tail = messages.slice(-4);
253
-
254
- const snipped = middle.filter((msg) => {
255
- const content = getContent(msg);
256
-
257
- // Remove empty messages
258
- if (!content || content.trim() === "") return false;
259
-
260
- // Remove processed markers
261
- if (content.includes("[PROCESSED]") || content.includes("[STALE]"))
262
- return false;
263
-
264
- // Remove tool_result messages that are just "ok" or empty JSON
265
- if (msg.role === "tool") {
266
- if (
267
- content === "ok" ||
268
- content === "{}" ||
269
- content === "null" ||
270
- content.length < 3
271
- )
272
- return false;
273
- }
274
-
275
- // Remove very short assistant acknowledgments in middle
276
- if (msg.role === "assistant" && content.length < 10) return false;
277
-
278
- return true;
279
- });
280
-
281
- return [...head, ...snipped, ...tail];
282
- }
283
-
284
- // ── Strategy 5: Context Collapse ────────────────────────────────────
285
- // Folds consecutive tool_call + tool_result pairs into a single summary.
286
-
287
- _contextCollapse(messages) {
288
- if (messages.length <= 6) return messages;
289
-
290
- const result = [];
291
- let i = 0;
292
-
293
- while (i < messages.length) {
294
- const msg = messages[i];
295
-
296
- // Detect consecutive tool sequences in the middle (not last 3)
297
- if (
298
- i > 0 &&
299
- i < messages.length - 3 &&
300
- msg.role === "assistant" &&
301
- msg.tool_calls &&
302
- msg.tool_calls.length > 0
303
- ) {
304
- // Collect this tool call + all following tool results
305
- const toolGroup = [msg];
306
- let j = i + 1;
307
- while (j < messages.length - 3 && messages[j].role === "tool") {
308
- toolGroup.push(messages[j]);
309
- j++;
310
- }
311
-
312
- // Also collect next assistant with tool_calls (chained calls)
313
- while (
314
- j < messages.length - 3 &&
315
- messages[j].role === "assistant" &&
316
- messages[j].tool_calls
317
- ) {
318
- toolGroup.push(messages[j]);
319
- j++;
320
- while (j < messages.length - 3 && messages[j].role === "tool") {
321
- toolGroup.push(messages[j]);
322
- j++;
323
- }
324
- }
325
-
326
- // Only collapse if we collected 3+ messages
327
- if (toolGroup.length >= 3) {
328
- const toolNames = toolGroup
329
- .filter((m) => m.tool_calls)
330
- .flatMap((m) =>
331
- m.tool_calls.map((tc) => tc.function?.name || "tool"),
332
- )
333
- .filter(Boolean);
334
- const uniqueTools = [...new Set(toolNames)];
335
-
336
- result.push({
337
- role: "system",
338
- content: `[Collapsed ${toolGroup.length} tool messages: ${uniqueTools.join(", ")}]`,
339
- });
340
- i = j;
341
- continue;
342
- }
343
- }
344
-
345
- result.push(msg);
346
- i++;
347
- }
348
-
349
- return result;
350
- }
351
- }
1
+ export {
2
+ estimateTokens,
3
+ estimateMessagesTokens,
4
+ CONTEXT_WINDOWS,
5
+ getContextWindow,
6
+ COMPRESSION_VARIANTS,
7
+ getCompressionVariant,
8
+ adaptiveThresholds,
9
+ PromptCompressor,
10
+ } from "../harness/prompt-compressor.js";