llm-agent-cli 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/agent.d.ts +112 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +730 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/audit.d.ts +24 -0
  6. package/dist/audit.d.ts.map +1 -0
  7. package/dist/audit.js +94 -0
  8. package/dist/audit.js.map +1 -0
  9. package/dist/auth.d.ts +36 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +236 -0
  12. package/dist/auth.js.map +1 -0
  13. package/dist/config.d.ts +35 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +92 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/context.d.ts +48 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +94 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/diff.d.ts +27 -0
  22. package/dist/diff.d.ts.map +1 -0
  23. package/dist/diff.js +174 -0
  24. package/dist/diff.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +905 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/input.d.ts +13 -0
  30. package/dist/input.d.ts.map +1 -0
  31. package/dist/input.js +88 -0
  32. package/dist/input.js.map +1 -0
  33. package/dist/memory.d.ts +32 -0
  34. package/dist/memory.d.ts.map +1 -0
  35. package/dist/memory.js +103 -0
  36. package/dist/memory.js.map +1 -0
  37. package/dist/preprocessor.d.ts +12 -0
  38. package/dist/preprocessor.d.ts.map +1 -0
  39. package/dist/preprocessor.js +138 -0
  40. package/dist/preprocessor.js.map +1 -0
  41. package/dist/project.d.ts +10 -0
  42. package/dist/project.d.ts.map +1 -0
  43. package/dist/project.js +145 -0
  44. package/dist/project.js.map +1 -0
  45. package/dist/renderer.d.ts +2 -0
  46. package/dist/renderer.d.ts.map +1 -0
  47. package/dist/renderer.js +31 -0
  48. package/dist/renderer.js.map +1 -0
  49. package/dist/session.d.ts +36 -0
  50. package/dist/session.d.ts.map +1 -0
  51. package/dist/session.js +78 -0
  52. package/dist/session.js.map +1 -0
  53. package/dist/tools/filesystem.d.ts +138 -0
  54. package/dist/tools/filesystem.d.ts.map +1 -0
  55. package/dist/tools/filesystem.js +539 -0
  56. package/dist/tools/filesystem.js.map +1 -0
  57. package/dist/tools/git.d.ts +60 -0
  58. package/dist/tools/git.d.ts.map +1 -0
  59. package/dist/tools/git.js +188 -0
  60. package/dist/tools/git.js.map +1 -0
  61. package/dist/tools/index.d.ts +386 -0
  62. package/dist/tools/index.d.ts.map +1 -0
  63. package/dist/tools/index.js +142 -0
  64. package/dist/tools/index.js.map +1 -0
  65. package/dist/tools/linter.d.ts +44 -0
  66. package/dist/tools/linter.d.ts.map +1 -0
  67. package/dist/tools/linter.js +426 -0
  68. package/dist/tools/linter.js.map +1 -0
  69. package/dist/tools/network.d.ts +17 -0
  70. package/dist/tools/network.d.ts.map +1 -0
  71. package/dist/tools/network.js +121 -0
  72. package/dist/tools/network.js.map +1 -0
  73. package/dist/tools/search.d.ts +33 -0
  74. package/dist/tools/search.d.ts.map +1 -0
  75. package/dist/tools/search.js +263 -0
  76. package/dist/tools/search.js.map +1 -0
  77. package/dist/tools/security.d.ts +18 -0
  78. package/dist/tools/security.d.ts.map +1 -0
  79. package/dist/tools/security.js +242 -0
  80. package/dist/tools/security.js.map +1 -0
  81. package/dist/tools/shell.d.ts +14 -0
  82. package/dist/tools/shell.d.ts.map +1 -0
  83. package/dist/tools/shell.js +68 -0
  84. package/dist/tools/shell.js.map +1 -0
  85. package/dist/tools/types.d.ts +5 -0
  86. package/dist/tools/types.d.ts.map +1 -0
  87. package/dist/tools/types.js +2 -0
  88. package/dist/tools/types.js.map +1 -0
  89. package/package.json +59 -0
package/dist/agent.js ADDED
@@ -0,0 +1,730 @@
1
+ import { TOOL_DEFINITIONS, TOOL_MAP, configureToolRuntime, getToolRuntime, } from "./tools/index.js";
2
+ import { estimateTokens } from "./context.js";
3
+ class RouterHttpError extends Error {
4
+ status;
5
+ constructor(status, message) {
6
+ super(message);
7
+ this.name = "RouterHttpError";
8
+ this.status = status;
9
+ }
10
+ }
11
+ export class TurnCancelledError extends Error {
12
+ constructor(message = "Turn cancelled") {
13
+ super(message);
14
+ this.name = "TurnCancelledError";
15
+ }
16
+ }
17
+ const RETRYABLE_STATUS = new Set([429, 500, 503]);
18
+ const MAX_ATTEMPTS = 3;
19
+ function getRouterConfig() {
20
+ const baseUrl = process.env.LLM_ROUTER_BASE_URL;
21
+ const apiKey = process.env.LLM_ROUTER_API_KEY;
22
+ const model = process.env.LLM_ROUTER_MODEL ?? "auto";
23
+ if (!baseUrl)
24
+ throw new Error("LLM_ROUTER_BASE_URL is not set in .env");
25
+ if (!apiKey)
26
+ throw new Error("LLM_ROUTER_API_KEY is not set in .env");
27
+ return { baseUrl, apiKey, model };
28
+ }
29
+ async function sleep(ms, signal) {
30
+ if (signal?.aborted) {
31
+ throw new TurnCancelledError();
32
+ }
33
+ await new Promise((resolve, reject) => {
34
+ const timeout = setTimeout(() => {
35
+ cleanup();
36
+ resolve();
37
+ }, ms);
38
+ const onAbort = () => {
39
+ cleanup();
40
+ reject(new TurnCancelledError());
41
+ };
42
+ const cleanup = () => {
43
+ clearTimeout(timeout);
44
+ signal?.removeEventListener("abort", onAbort);
45
+ };
46
+ signal?.addEventListener("abort", onAbort, { once: true });
47
+ });
48
+ }
49
+ function isRetryableError(err) {
50
+ if (err instanceof TurnCancelledError)
51
+ return false;
52
+ if (err instanceof RouterHttpError) {
53
+ return RETRYABLE_STATUS.has(err.status);
54
+ }
55
+ if (err instanceof TypeError) {
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ function buildSystemPrompt(workspaceRoot, runtime, systemContext) {
61
+ const contextBlock = systemContext
62
+ ? `\nProject context:\n${systemContext}\n`
63
+ : "";
64
+ return `You are a capable terminal-based AI agent.
65
+
66
+ Available tools:
67
+ - read_file
68
+ - write_to_file
69
+ - apply_diff
70
+ - list_directory
71
+ - create_directory
72
+ - delete_file
73
+ - move_file
74
+ - get_file_info
75
+ - search_files
76
+ - find_files
77
+ - fetch_url
78
+ - run_bash_command
79
+
80
+ Execution constraints:
81
+ - Workspace root: ${workspaceRoot}
82
+ - Permission mode: ${runtime.permissionMode}
83
+ - Max read size: ${runtime.maxReadBytes} bytes
84
+ - Max tool result passed back to model: ${runtime.maxToolResultChars} chars
85
+ - Use targeted edits (apply_diff) when possible instead of full rewrites.
86
+ - Inspect files before editing.
87
+ - Summarize what changed when complete.${contextBlock}`;
88
+ }
89
+ function buildFullSystemPrompt(base, memory) {
90
+ if (!memory.trim())
91
+ return base;
92
+ return `${base}\n\n# Persistent Memory\n${memory}`;
93
+ }
94
+ function normalizeToolCalls(map) {
95
+ return Array.from(map.entries())
96
+ .sort(([a], [b]) => a - b)
97
+ .map(([, value]) => value)
98
+ .filter((call) => call.function.name.trim().length > 0);
99
+ }
100
+ async function parseSseResponse(response, signal, onToken) {
101
+ const reader = response.body?.getReader();
102
+ if (!reader) {
103
+ throw new Error("Router response body is empty");
104
+ }
105
+ const decoder = new TextDecoder();
106
+ let buffer = "";
107
+ let role = "assistant";
108
+ let content = "";
109
+ let finishReason = "stop";
110
+ let usage;
111
+ let model;
112
+ const toolCalls = new Map();
113
+ while (true) {
114
+ if (signal?.aborted) {
115
+ throw new TurnCancelledError();
116
+ }
117
+ const { done, value } = await reader.read();
118
+ if (done)
119
+ break;
120
+ if (!value)
121
+ continue;
122
+ buffer += decoder.decode(value, { stream: true });
123
+ let newlineIndex = buffer.indexOf("\n");
124
+ while (newlineIndex !== -1) {
125
+ const rawLine = buffer.slice(0, newlineIndex);
126
+ buffer = buffer.slice(newlineIndex + 1);
127
+ const line = rawLine.trim();
128
+ if (!line || line.startsWith(":")) {
129
+ newlineIndex = buffer.indexOf("\n");
130
+ continue;
131
+ }
132
+ if (!line.startsWith("data:")) {
133
+ newlineIndex = buffer.indexOf("\n");
134
+ continue;
135
+ }
136
+ const payload = line.slice(5).trim();
137
+ if (!payload || payload === "[DONE]") {
138
+ newlineIndex = buffer.indexOf("\n");
139
+ continue;
140
+ }
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(payload);
144
+ }
145
+ catch {
146
+ newlineIndex = buffer.indexOf("\n");
147
+ continue;
148
+ }
149
+ if (parsed.model) {
150
+ model = parsed.model;
151
+ }
152
+ if (parsed.usage) {
153
+ usage = parsed.usage;
154
+ }
155
+ const choice = parsed.choices?.[0];
156
+ if (choice?.finish_reason) {
157
+ finishReason = choice.finish_reason;
158
+ }
159
+ if (choice?.delta?.role) {
160
+ role = choice.delta.role;
161
+ }
162
+ const deltaContent = choice?.delta?.content;
163
+ if (deltaContent) {
164
+ content += deltaContent;
165
+ if (onToken) {
166
+ await onToken(deltaContent);
167
+ }
168
+ }
169
+ const deltaToolCalls = choice?.delta?.tool_calls ?? [];
170
+ for (const deltaToolCall of deltaToolCalls) {
171
+ const index = deltaToolCall.index ?? 0;
172
+ const existing = toolCalls.get(index) ?? {
173
+ id: deltaToolCall.id ?? `tool_call_${index}`,
174
+ type: "function",
175
+ function: {
176
+ name: "",
177
+ arguments: "",
178
+ },
179
+ };
180
+ if (deltaToolCall.id) {
181
+ existing.id = deltaToolCall.id;
182
+ }
183
+ const functionName = deltaToolCall.function?.name;
184
+ if (functionName) {
185
+ existing.function.name += functionName;
186
+ }
187
+ const functionArgs = deltaToolCall.function?.arguments;
188
+ if (functionArgs) {
189
+ existing.function.arguments += functionArgs;
190
+ }
191
+ toolCalls.set(index, existing);
192
+ }
193
+ newlineIndex = buffer.indexOf("\n");
194
+ }
195
+ }
196
+ return {
197
+ message: {
198
+ role,
199
+ content: content || null,
200
+ tool_calls: normalizeToolCalls(toolCalls),
201
+ },
202
+ finishReason,
203
+ usage,
204
+ model,
205
+ };
206
+ }
207
+ async function callRouterOnce(messages, options) {
208
+ const { baseUrl, apiKey, model } = getRouterConfig();
209
+ const body = {
210
+ model,
211
+ messages,
212
+ };
213
+ if (options.toolsEnabled) {
214
+ body.tools = TOOL_DEFINITIONS;
215
+ body.tool_choice = "auto";
216
+ }
217
+ if (options.stream) {
218
+ body.stream = true;
219
+ body.stream_options = { include_usage: true };
220
+ }
221
+ const response = await fetch(`${baseUrl}/v1/chat/completions`, {
222
+ method: "POST",
223
+ headers: {
224
+ "Content-Type": "application/json",
225
+ Authorization: `Bearer ${apiKey}`,
226
+ "X-Client-Type": "cli-agent",
227
+ Accept: options.stream ? "text/event-stream, application/json" : "application/json",
228
+ },
229
+ signal: options.signal,
230
+ body: JSON.stringify(body),
231
+ });
232
+ if (!response.ok) {
233
+ const errorBody = await response.text();
234
+ throw new RouterHttpError(response.status, `LLM Router error ${response.status}: ${errorBody}`);
235
+ }
236
+ if (options.stream) {
237
+ const contentType = response.headers.get("content-type") ?? "";
238
+ if (!contentType.includes("application/json")) {
239
+ return parseSseResponse(response, options.signal, options.onToken);
240
+ }
241
+ }
242
+ const data = (await response.json());
243
+ const choice = data.choices[0];
244
+ if (!choice) {
245
+ throw new Error("No choice returned from router");
246
+ }
247
+ return {
248
+ message: choice.message,
249
+ finishReason: choice.finish_reason,
250
+ usage: data.usage,
251
+ model: data.model,
252
+ };
253
+ }
254
+ async function callRouterWithRetry(messages, signal, callbacks) {
255
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
256
+ try {
257
+ return await callRouterOnce(messages, {
258
+ signal,
259
+ toolsEnabled: true,
260
+ stream: true,
261
+ onToken: callbacks.onToken,
262
+ });
263
+ }
264
+ catch (err) {
265
+ if (signal?.aborted) {
266
+ throw new TurnCancelledError();
267
+ }
268
+ if (!isRetryableError(err) || attempt >= MAX_ATTEMPTS) {
269
+ throw err;
270
+ }
271
+ const delayMs = 1000 * 2 ** (attempt - 1);
272
+ const reason = err instanceof Error ? err.message : String(err);
273
+ if (callbacks.onRetry) {
274
+ await callbacks.onRetry(attempt, delayMs, reason);
275
+ }
276
+ await sleep(delayMs, signal);
277
+ }
278
+ }
279
+ throw new Error("Router call failed after maximum retries");
280
+ }
281
+ function previewArgs(rawArgs) {
282
+ const collapsed = rawArgs.replace(/\s+/g, " ").trim();
283
+ if (collapsed.length <= 120)
284
+ return collapsed;
285
+ return `${collapsed.slice(0, 117)}...`;
286
+ }
287
+ function truncateToolResult(rawResult) {
288
+ const runtime = getToolRuntime();
289
+ if (rawResult.length <= runtime.maxToolResultChars) {
290
+ return { output: rawResult, truncated: false };
291
+ }
292
+ const trimmed = rawResult.slice(0, runtime.maxToolResultChars);
293
+ const dropped = rawResult.length - runtime.maxToolResultChars;
294
+ return {
295
+ output: `${trimmed}\n\n[Tool output truncated: removed ${dropped} chars before returning to model]`,
296
+ truncated: true,
297
+ };
298
+ }
299
+ function isApprovedFromToolResult(toolName, toolResult) {
300
+ const lower = toolResult.toLowerCase();
301
+ if (!lower.startsWith("error")) {
302
+ return true;
303
+ }
304
+ if (lower.includes("user denied") ||
305
+ lower.includes("disabled in safe mode") ||
306
+ lower.includes("requires confirmation") ||
307
+ lower.includes("blocked dangerous command pattern")) {
308
+ return false;
309
+ }
310
+ if (toolName === "run_bash_command" && lower.includes("shell commands are disabled")) {
311
+ return false;
312
+ }
313
+ return true;
314
+ }
315
+ function normalizeMessageContent(content) {
316
+ if (content === undefined)
317
+ return null;
318
+ return content;
319
+ }
320
+ export async function checkRouterHealth() {
321
+ const baseUrl = process.env.LLM_ROUTER_BASE_URL;
322
+ if (!baseUrl) {
323
+ return { ok: false, message: "LLM_ROUTER_BASE_URL is not set" };
324
+ }
325
+ const controller = new AbortController();
326
+ const timeout = setTimeout(() => controller.abort(), 2_000);
327
+ try {
328
+ const response = await fetch(`${baseUrl}/health`, {
329
+ method: "GET",
330
+ signal: controller.signal,
331
+ });
332
+ if (!response.ok) {
333
+ return {
334
+ ok: false,
335
+ message: `Router health check failed with HTTP ${response.status}`,
336
+ };
337
+ }
338
+ return { ok: true, message: "Router reachable" };
339
+ }
340
+ catch {
341
+ return {
342
+ ok: false,
343
+ message: `Router unreachable at ${baseUrl}`,
344
+ };
345
+ }
346
+ finally {
347
+ clearTimeout(timeout);
348
+ }
349
+ }
350
+ export class Agent {
351
+ history = [];
352
+ lastUserInput = null;
353
+ verboseTools = false;
354
+ auditLogger;
355
+ baseSystemPrompt;
356
+ memory;
357
+ contextWindowTokens;
358
+ constructor(options = {}) {
359
+ configureToolRuntime({
360
+ workspaceRoot: options.workspaceRoot,
361
+ permissionMode: options.permissionMode,
362
+ maxReadBytes: options.maxReadBytes,
363
+ maxToolResultChars: options.maxToolResultChars,
364
+ maxCommandOutputChars: options.maxCommandOutputChars,
365
+ shellTimeoutMs: options.shellTimeoutMs,
366
+ });
367
+ const runtime = getToolRuntime();
368
+ this.baseSystemPrompt = buildSystemPrompt(runtime.workspaceRoot, runtime, options.systemContext);
369
+ this.memory = options.memory ?? "";
370
+ this.auditLogger = options.auditLogger;
371
+ this.contextWindowTokens = options.contextWindowTokens ?? 200_000;
372
+ this.history.push({
373
+ role: "system",
374
+ content: buildFullSystemPrompt(this.baseSystemPrompt, this.memory),
375
+ });
376
+ }
377
+ /**
378
+ * Hot-update the persistent memory injected into the system prompt.
379
+ * Safe to call mid-session — patches history[0] in place so the model sees
380
+ * the new memory on the very next turn.
381
+ */
382
+ updateMemory(memory) {
383
+ this.memory = memory;
384
+ if (this.history[0]) {
385
+ this.history[0].content = buildFullSystemPrompt(this.baseSystemPrompt, this.memory);
386
+ }
387
+ }
388
+ getLastUserInput() {
389
+ return this.lastUserInput;
390
+ }
391
+ getPermissionMode() {
392
+ return getToolRuntime().permissionMode;
393
+ }
394
+ getWorkspaceRoot() {
395
+ return getToolRuntime().workspaceRoot;
396
+ }
397
+ isVerboseTools() {
398
+ return this.verboseTools;
399
+ }
400
+ setVerboseTools(enabled) {
401
+ this.verboseTools = enabled;
402
+ }
403
+ reset() {
404
+ this.history = [
405
+ {
406
+ role: "system",
407
+ content: buildFullSystemPrompt(this.baseSystemPrompt, this.memory),
408
+ },
409
+ ];
410
+ this.lastUserInput = null;
411
+ }
412
+ undoLastTurn() {
413
+ for (let i = this.history.length - 1; i >= 1; i -= 1) {
414
+ if (this.history[i]?.role === "user") {
415
+ const removedUser = this.history[i]?.content ?? null;
416
+ this.history = this.history.slice(0, i);
417
+ this.lastUserInput = removedUser;
418
+ return removedUser;
419
+ }
420
+ }
421
+ return null;
422
+ }
423
+ exportState(contextState) {
424
+ return {
425
+ history: this.history.map((msg) => ({
426
+ role: msg.role,
427
+ content: normalizeMessageContent(msg.content),
428
+ tool_calls: msg.tool_calls,
429
+ tool_call_id: msg.tool_call_id,
430
+ name: msg.name,
431
+ })),
432
+ lastUserInput: this.lastUserInput,
433
+ verboseTools: this.verboseTools,
434
+ cwd: process.cwd(),
435
+ contextState,
436
+ };
437
+ }
438
+ importState(state) {
439
+ if (!state.history || state.history.length === 0) {
440
+ this.reset();
441
+ return;
442
+ }
443
+ this.history = state.history.map((msg) => ({
444
+ role: msg.role,
445
+ content: normalizeMessageContent(msg.content),
446
+ tool_calls: msg.tool_calls,
447
+ tool_call_id: msg.tool_call_id,
448
+ name: msg.name,
449
+ }));
450
+ this.lastUserInput = state.lastUserInput;
451
+ this.verboseTools = state.verboseTools;
452
+ }
453
+ async run(userInput, options = {}) {
454
+ if (options.signal?.aborted) {
455
+ throw new TurnCancelledError();
456
+ }
457
+ this.lastUserInput = userInput;
458
+ const historyStart = this.history.length;
459
+ this.history.push({ role: "user", content: userInput });
460
+ // ── Pre-flight context estimation ─────────────────────────────────────────
461
+ // Estimate token usage before sending so the user sees a warning (or an
462
+ // auto-compact) instead of an opaque 400 error from the router.
463
+ const estimatedTokens = estimateTokens(this.history);
464
+ const compactThreshold = Math.floor(this.contextWindowTokens * 0.9);
465
+ const warnThreshold = Math.floor(this.contextWindowTokens * 0.7);
466
+ if (estimatedTokens >= compactThreshold) {
467
+ if (options.callbacks?.onToken) {
468
+ await options.callbacks.onToken(`\n[Context ~${estimatedTokens.toLocaleString()} tokens ` +
469
+ `(${Math.round((estimatedTokens / this.contextWindowTokens) * 100)}% of window). ` +
470
+ `Auto-compacting before sending...]\n`);
471
+ }
472
+ await this.compactHistory(options.signal);
473
+ }
474
+ else if (estimatedTokens >= warnThreshold) {
475
+ if (options.callbacks?.onToken) {
476
+ await options.callbacks.onToken(`\n[Context ~${estimatedTokens.toLocaleString()} tokens ` +
477
+ `(${Math.round((estimatedTokens / this.contextWindowTokens) * 100)}% of window). ` +
478
+ `Consider /compact to free space.]\n`);
479
+ }
480
+ }
481
+ let finalResponse = "";
482
+ let finalUsage;
483
+ let finalModel = getRouterConfig().model;
484
+ let finishReason = "stop";
485
+ let toolCallCount = 0;
486
+ try {
487
+ let iterations = 0;
488
+ const maxIterations = 20;
489
+ while (iterations < maxIterations) {
490
+ iterations += 1;
491
+ const streamed = await callRouterWithRetry(this.history, options.signal, {
492
+ onToken: options.callbacks?.onToken,
493
+ onRetry: options.callbacks?.onRetry,
494
+ });
495
+ const assistantMessage = streamed.message;
496
+ finalModel = streamed.model ?? finalModel;
497
+ finishReason = streamed.finishReason;
498
+ this.history.push({
499
+ role: "assistant",
500
+ content: assistantMessage.content ?? null,
501
+ tool_calls: assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0
502
+ ? assistantMessage.tool_calls
503
+ : undefined,
504
+ });
505
+ if (streamed.finishReason === "tool_calls" &&
506
+ assistantMessage.tool_calls?.length) {
507
+ const total = assistantMessage.tool_calls.length;
508
+ for (let index = 0; index < total; index += 1) {
509
+ if (options.signal?.aborted) {
510
+ throw new TurnCancelledError();
511
+ }
512
+ const toolCall = assistantMessage.tool_calls[index];
513
+ toolCallCount += 1;
514
+ const toolName = toolCall.function.name;
515
+ const toolFn = TOOL_MAP[toolName];
516
+ const argsPreview = previewArgs(toolCall.function.arguments);
517
+ if (options.callbacks?.onToolStart) {
518
+ await options.callbacks.onToolStart({
519
+ toolName,
520
+ argsPreview,
521
+ index: index + 1,
522
+ total,
523
+ });
524
+ }
525
+ let toolResult = "";
526
+ let parsedArgs = {};
527
+ if (!toolFn) {
528
+ toolResult = `Error: unknown tool \"${toolName}\"`;
529
+ }
530
+ else {
531
+ try {
532
+ parsedArgs = JSON.parse(toolCall.function.arguments);
533
+ }
534
+ catch {
535
+ parsedArgs = {};
536
+ toolResult =
537
+ "Error: could not parse tool arguments as JSON; received raw argument string.";
538
+ }
539
+ if (!toolResult) {
540
+ const toolContext = {
541
+ abortSignal: options.signal,
542
+ };
543
+ toolResult = await toolFn(parsedArgs, toolContext);
544
+ }
545
+ }
546
+ const truncated = truncateToolResult(toolResult);
547
+ if (this.auditLogger) {
548
+ await this.auditLogger.logTool(toolName, parsedArgs, truncated.output, isApprovedFromToolResult(toolName, toolResult));
549
+ }
550
+ if (options.callbacks?.onToolEnd) {
551
+ const preview = this.verboseTools
552
+ ? truncated.output
553
+ : truncated.output.slice(0, 300);
554
+ await options.callbacks.onToolEnd({
555
+ toolName,
556
+ index: index + 1,
557
+ total,
558
+ truncated: truncated.truncated,
559
+ resultPreview: preview,
560
+ });
561
+ }
562
+ this.history.push({
563
+ role: "tool",
564
+ tool_call_id: toolCall.id,
565
+ name: toolName,
566
+ content: truncated.output,
567
+ });
568
+ }
569
+ continue;
570
+ }
571
+ finalResponse = assistantMessage.content ?? "";
572
+ finalUsage = streamed.usage;
573
+ break;
574
+ }
575
+ if (!finalResponse && finishReason !== "tool_calls") {
576
+ finalResponse = "[Agent produced an empty response]";
577
+ }
578
+ if (!finalResponse && finishReason === "tool_calls") {
579
+ finalResponse =
580
+ "[Agent stopped: reached maximum iteration limit of 20 iterations]";
581
+ }
582
+ return {
583
+ response: finalResponse,
584
+ usage: finalUsage,
585
+ model: finalModel,
586
+ finishReason,
587
+ toolCalls: toolCallCount,
588
+ };
589
+ }
590
+ catch (err) {
591
+ this.history = this.history.slice(0, historyStart);
592
+ throw err;
593
+ }
594
+ }
595
+ async compactHistory(signal) {
596
+ if (this.history.length <= 2) {
597
+ return "Conversation is already compact.";
598
+ }
599
+ // ── Tiered compaction ────────────────────────────────────────────────────
600
+ // Recent exchanges are preserved verbatim so the model never loses sight
601
+ // of what was just asked / just returned. Everything older is compressed
602
+ // into a structured summary block.
603
+ //
604
+ // Structure after compaction:
605
+ // [system, assistant(summary), ...last-N-turns-verbatim]
606
+ //
607
+ const KEEP_TURNS = 3; // number of complete user→assistant exchanges to keep
608
+ const keepFromIndex = this._findKeepBoundary(KEEP_TURNS);
609
+ // Nothing old enough to summarise — all turns fall within the keep window.
610
+ if (keepFromIndex <= 1) {
611
+ return "Recent context fits within the keep window — nothing to compact.";
612
+ }
613
+ const toSummarize = this.history.slice(1, keepFromIndex);
614
+ const toKeep = this.history.slice(keepFromIndex);
615
+ // ── Build transcript of the portion being compressed ────────────────────
616
+ const rawTranscript = toSummarize
617
+ .map((msg, i) => {
618
+ let header = `${i + 1}. [${msg.role}]`;
619
+ if (msg.role === "assistant" && msg.tool_calls?.length) {
620
+ const calls = msg.tool_calls.map((tc) => tc.function.name).join(", ");
621
+ header += ` → calls: ${calls}`;
622
+ }
623
+ if (msg.role === "tool" && msg.name) {
624
+ header += ` result for: ${msg.name}`;
625
+ }
626
+ // 5 000 chars per message — enough signal without overwhelming the
627
+ // summariser for large file reads.
628
+ const body = (msg.content ?? "").slice(0, 5_000);
629
+ return `${header}\n${body}`;
630
+ })
631
+ .join("\n\n");
632
+ // Hard cap: 120 k chars ≈ 30 k tokens keeps the summarisation call safely
633
+ // inside even a 32 k-token context window.
634
+ const MAX_TRANSCRIPT = 120_000;
635
+ const transcript = rawTranscript.length > MAX_TRANSCRIPT
636
+ ? `[... ${(rawTranscript.length - MAX_TRANSCRIPT).toLocaleString()} earlier chars omitted ...]\n\n` +
637
+ rawTranscript.slice(-MAX_TRANSCRIPT)
638
+ : rawTranscript;
639
+ // ── Structured summarisation prompt ──────────────────────────────────────
640
+ // Sections match what the agent needs to resume work cleanly. Be explicit
641
+ // about requiring file paths, line numbers, and exact values so the model
642
+ // doesn't produce vague prose.
643
+ const prompt = `Compress the following conversation transcript into a structured summary.
644
+ Use these exact section headers. Be specific: include file paths, line numbers, function names, and exact values where they matter. Omit sections that have no content.
645
+
646
+ ## Objective
647
+ What the user is trying to achieve overall.
648
+
649
+ ## Work Completed
650
+ Bullet list of finished tasks. Include file paths and what changed.
651
+
652
+ ## Files Touched
653
+ For each file: path, what was read/modified, key contents if critical to remember.
654
+
655
+ ## Commands Run
656
+ Shell commands executed and their notable output or outcome.
657
+
658
+ ## Decisions Made
659
+ Architecture, design, or implementation decisions with rationale.
660
+
661
+ ## In Progress
662
+ What was actively being worked on at the point of compaction.
663
+
664
+ ## Remaining Work
665
+ Tasks not yet started, bugs not yet fixed, open questions.
666
+
667
+ ## Key Context
668
+ Constants, API shapes, naming conventions, configuration values, or constraints the agent must remember.
669
+
670
+ ---
671
+ TRANSCRIPT TO COMPRESS:
672
+
673
+ ${transcript}`;
674
+ const summaryResult = await callRouterOnce([
675
+ this.history[0],
676
+ { role: "user", content: prompt },
677
+ ], { signal, toolsEnabled: false, stream: false });
678
+ const summary = summaryResult.message.content?.trim() ?? "Conversation summary unavailable.";
679
+ // ── Rebuild history ───────────────────────────────────────────────────────
680
+ // [system, assistant(structured summary), ...last-N-turns-verbatim]
681
+ //
682
+ // Storing the summary as "assistant" keeps the sequence valid for all
683
+ // providers (no double system messages). The verbatim recent turns follow
684
+ // immediately so the model has full fidelity on the current task.
685
+ this.history = [
686
+ this.history[0],
687
+ {
688
+ role: "assistant",
689
+ content: `[Prior context — compacted]\n\n${summary}`,
690
+ },
691
+ ...toKeep,
692
+ ];
693
+ return summary;
694
+ }
695
+ /**
696
+ * Find the history index of the start of the Nth-from-last complete exchange
697
+ * (where an exchange begins with a user message). Returns 1 if there are
698
+ * fewer than keepTurns user messages — meaning nothing should be compacted.
699
+ */
700
+ _findKeepBoundary(keepTurns) {
701
+ let userCount = 0;
702
+ for (let i = this.history.length - 1; i >= 1; i -= 1) {
703
+ if (this.history[i]?.role === "user") {
704
+ userCount += 1;
705
+ if (userCount === keepTurns) {
706
+ return i;
707
+ }
708
+ }
709
+ }
710
+ return 1;
711
+ }
712
+ async listModels(signal) {
713
+ const { baseUrl, apiKey } = getRouterConfig();
714
+ const response = await fetch(`${baseUrl}/v1/models`, {
715
+ method: "GET",
716
+ signal,
717
+ headers: {
718
+ Authorization: `Bearer ${apiKey}`,
719
+ "X-Client-Type": "cli-agent",
720
+ },
721
+ });
722
+ if (!response.ok) {
723
+ const body = await response.text();
724
+ throw new Error(`Failed to fetch models (${response.status}): ${body}`);
725
+ }
726
+ const payload = (await response.json());
727
+ return payload.data ?? [];
728
+ }
729
+ }
730
+ //# sourceMappingURL=agent.js.map