codeep 1.1.36 → 1.2.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 (36) hide show
  1. package/README.md +90 -4
  2. package/dist/api/index.js +64 -2
  3. package/dist/renderer/App.d.ts +5 -0
  4. package/dist/renderer/App.js +164 -227
  5. package/dist/renderer/components/Export.d.ts +22 -0
  6. package/dist/renderer/components/Export.js +64 -0
  7. package/dist/renderer/components/Help.js +5 -1
  8. package/dist/renderer/components/Logout.d.ts +29 -0
  9. package/dist/renderer/components/Logout.js +91 -0
  10. package/dist/renderer/components/Search.d.ts +30 -0
  11. package/dist/renderer/components/Search.js +83 -0
  12. package/dist/renderer/components/Settings.js +20 -0
  13. package/dist/renderer/components/Status.d.ts +6 -0
  14. package/dist/renderer/components/Status.js +20 -1
  15. package/dist/renderer/main.js +296 -142
  16. package/dist/utils/agent.d.ts +5 -0
  17. package/dist/utils/agent.js +238 -3
  18. package/dist/utils/agent.test.d.ts +1 -0
  19. package/dist/utils/agent.test.js +250 -0
  20. package/dist/utils/diffPreview.js +104 -35
  21. package/dist/utils/gitignore.d.ts +24 -0
  22. package/dist/utils/gitignore.js +161 -0
  23. package/dist/utils/gitignore.test.d.ts +1 -0
  24. package/dist/utils/gitignore.test.js +167 -0
  25. package/dist/utils/skills.d.ts +21 -0
  26. package/dist/utils/skills.js +51 -0
  27. package/dist/utils/smartContext.js +8 -0
  28. package/dist/utils/smartContext.test.d.ts +1 -0
  29. package/dist/utils/smartContext.test.js +382 -0
  30. package/dist/utils/tokenTracker.d.ts +52 -0
  31. package/dist/utils/tokenTracker.js +86 -0
  32. package/dist/utils/tools.d.ts +16 -0
  33. package/dist/utils/tools.js +146 -19
  34. package/dist/utils/tools.test.d.ts +1 -0
  35. package/dist/utils/tools.test.js +664 -0
  36. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Agent loop - autonomous task execution
3
3
  */
4
+ import { existsSync, readFileSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { recordTokenUsage, extractOpenAIUsage, extractAnthropicUsage } from './tokenTracker.js';
4
7
  // Debug logging helper - only logs when CODEEP_DEBUG=1
5
8
  const debug = (...args) => {
6
9
  if (process.env.CODEEP_DEBUG === '1') {
@@ -47,6 +50,31 @@ const DEFAULT_OPTIONS = {
47
50
  maxDuration: 20 * 60 * 1000, // 20 minutes
48
51
  usePlanning: false, // Disable task planning - causes more problems than it solves
49
52
  };
53
+ /**
54
+ * Load project rules from .codeep/rules.md or CODEEP.md
55
+ * Returns the rules content formatted for system prompt, or empty string if no rules found
56
+ */
57
+ export function loadProjectRules(projectRoot) {
58
+ const candidates = [
59
+ join(projectRoot, '.codeep', 'rules.md'),
60
+ join(projectRoot, 'CODEEP.md'),
61
+ ];
62
+ for (const filePath of candidates) {
63
+ if (existsSync(filePath)) {
64
+ try {
65
+ const content = readFileSync(filePath, 'utf-8').trim();
66
+ if (content) {
67
+ debug('Loaded project rules from', filePath);
68
+ return `\n\n## Project Rules\nThe following rules are defined by the project owner. You MUST follow these rules:\n\n${content}`;
69
+ }
70
+ }
71
+ catch (err) {
72
+ debug('Failed to read project rules from', filePath, err);
73
+ }
74
+ }
75
+ }
76
+ return '';
77
+ }
50
78
  /**
51
79
  * Generate system prompt for agent mode (used with native tool calling)
52
80
  */
@@ -202,6 +230,7 @@ async function agentChat(messages, systemPrompt, onChunk, abortSignal, dynamicTi
202
230
  try {
203
231
  let endpoint;
204
232
  let body;
233
+ const useStreaming = Boolean(onChunk);
205
234
  if (protocol === 'openai') {
206
235
  endpoint = `${baseUrl}/chat/completions`;
207
236
  body = {
@@ -212,8 +241,9 @@ async function agentChat(messages, systemPrompt, onChunk, abortSignal, dynamicTi
212
241
  ],
213
242
  tools: getOpenAITools(),
214
243
  tool_choice: 'auto',
244
+ stream: useStreaming,
215
245
  temperature: config.get('temperature'),
216
- max_tokens: Math.max(config.get('maxTokens'), 16384), // Ensure enough tokens for large file generation
246
+ max_tokens: Math.max(config.get('maxTokens'), 16384),
217
247
  };
218
248
  }
219
249
  else {
@@ -223,8 +253,9 @@ async function agentChat(messages, systemPrompt, onChunk, abortSignal, dynamicTi
223
253
  system: systemPrompt,
224
254
  messages: messages,
225
255
  tools: getAnthropicTools(),
256
+ stream: useStreaming,
226
257
  temperature: config.get('temperature'),
227
- max_tokens: Math.max(config.get('maxTokens'), 16384), // Ensure enough tokens for large file generation
258
+ max_tokens: Math.max(config.get('maxTokens'), 16384),
228
259
  };
229
260
  }
230
261
  const response = await fetch(endpoint, {
@@ -241,7 +272,22 @@ async function agentChat(messages, systemPrompt, onChunk, abortSignal, dynamicTi
241
272
  }
242
273
  throw new Error(`API error: ${response.status} - ${errorText}`);
243
274
  }
275
+ // Streaming path — parse tool calls from SSE deltas
276
+ if (useStreaming && response.body) {
277
+ if (protocol === 'openai') {
278
+ return await handleOpenAIAgentStream(response.body, onChunk, model, providerId);
279
+ }
280
+ else {
281
+ return await handleAnthropicAgentStream(response.body, onChunk, model, providerId);
282
+ }
283
+ }
284
+ // Non-streaming path (fallback if no body)
244
285
  const data = await response.json();
286
+ // Track token usage
287
+ const usageExtractor = protocol === 'openai' ? extractOpenAIUsage : extractAnthropicUsage;
288
+ const usage = usageExtractor(data);
289
+ if (usage)
290
+ recordTokenUsage(usage, model, providerId);
245
291
  debug('Raw API response:', JSON.stringify(data, null, 2).substring(0, 1500));
246
292
  if (protocol === 'openai') {
247
293
  const message = data.choices?.[0]?.message;
@@ -388,6 +434,11 @@ async function agentChatFallback(messages, systemPrompt, onChunk, abortSignal, d
388
434
  }
389
435
  else {
390
436
  const data = await response.json();
437
+ // Track token usage
438
+ const fallbackUsageExtractor = protocol === 'openai' ? extractOpenAIUsage : extractAnthropicUsage;
439
+ const fallbackUsage = fallbackUsageExtractor(data);
440
+ if (fallbackUsage)
441
+ recordTokenUsage(fallbackUsage, model, providerId);
391
442
  if (protocol === 'openai') {
392
443
  content = data.choices?.[0]?.message?.content || '';
393
444
  }
@@ -416,7 +467,186 @@ async function agentChatFallback(messages, systemPrompt, onChunk, abortSignal, d
416
467
  }
417
468
  }
418
469
  /**
419
- * Handle streaming response
470
+ * Handle OpenAI streaming response with tool call accumulation
471
+ */
472
+ async function handleOpenAIAgentStream(body, onChunk, model, providerId) {
473
+ const reader = body.getReader();
474
+ const decoder = new TextDecoder();
475
+ let buffer = '';
476
+ let content = '';
477
+ // Accumulate tool calls from deltas
478
+ const toolCallMap = new Map();
479
+ let usageData = null;
480
+ while (true) {
481
+ const { done, value } = await reader.read();
482
+ if (done)
483
+ break;
484
+ buffer += decoder.decode(value, { stream: true });
485
+ const lines = buffer.split('\n');
486
+ buffer = lines.pop() || '';
487
+ for (const line of lines) {
488
+ if (!line.startsWith('data: '))
489
+ continue;
490
+ const data = line.slice(6);
491
+ if (data === '[DONE]')
492
+ continue;
493
+ try {
494
+ const parsed = JSON.parse(data);
495
+ // Track usage from final chunk
496
+ if (parsed.usage) {
497
+ usageData = parsed;
498
+ }
499
+ const delta = parsed.choices?.[0]?.delta;
500
+ if (!delta)
501
+ continue;
502
+ // Accumulate text content
503
+ if (delta.content) {
504
+ content += delta.content;
505
+ onChunk(delta.content);
506
+ }
507
+ // Accumulate tool calls
508
+ if (delta.tool_calls) {
509
+ for (const tc of delta.tool_calls) {
510
+ const idx = tc.index ?? 0;
511
+ if (!toolCallMap.has(idx)) {
512
+ toolCallMap.set(idx, {
513
+ id: tc.id || '',
514
+ name: tc.function?.name || '',
515
+ arguments: '',
516
+ });
517
+ }
518
+ const entry = toolCallMap.get(idx);
519
+ if (tc.id)
520
+ entry.id = tc.id;
521
+ if (tc.function?.name)
522
+ entry.name = tc.function.name;
523
+ if (tc.function?.arguments)
524
+ entry.arguments += tc.function.arguments;
525
+ }
526
+ }
527
+ }
528
+ catch {
529
+ // Ignore parse errors
530
+ }
531
+ }
532
+ }
533
+ // Track token usage if available
534
+ if (usageData) {
535
+ const usage = extractOpenAIUsage(usageData);
536
+ if (usage)
537
+ recordTokenUsage(usage, model, providerId);
538
+ }
539
+ // Convert accumulated tool calls
540
+ const rawToolCalls = Array.from(toolCallMap.values()).map(tc => ({
541
+ id: tc.id,
542
+ type: 'function',
543
+ function: { name: tc.name, arguments: tc.arguments },
544
+ }));
545
+ const toolCalls = parseOpenAIToolCalls(rawToolCalls);
546
+ debug('Stream parsed tool calls:', toolCalls.length, toolCalls.map(t => t.tool));
547
+ // If no native tool calls, try text-based parsing
548
+ if (toolCalls.length === 0 && content) {
549
+ const textToolCalls = parseToolCalls(content);
550
+ if (textToolCalls.length > 0) {
551
+ return { content, toolCalls: textToolCalls, usedNativeTools: false };
552
+ }
553
+ }
554
+ return { content, toolCalls, usedNativeTools: true };
555
+ }
556
+ /**
557
+ * Handle Anthropic streaming response with tool call accumulation
558
+ */
559
+ async function handleAnthropicAgentStream(body, onChunk, model, providerId) {
560
+ const reader = body.getReader();
561
+ const decoder = new TextDecoder();
562
+ let buffer = '';
563
+ let content = '';
564
+ // Accumulate content blocks for tool use
565
+ const contentBlocks = [];
566
+ let currentBlockIndex = -1;
567
+ let currentBlockType = '';
568
+ let currentToolName = '';
569
+ let currentToolId = '';
570
+ let currentToolInput = '';
571
+ let usageData = null;
572
+ while (true) {
573
+ const { done, value } = await reader.read();
574
+ if (done)
575
+ break;
576
+ buffer += decoder.decode(value, { stream: true });
577
+ const lines = buffer.split('\n');
578
+ buffer = lines.pop() || '';
579
+ for (const line of lines) {
580
+ if (!line.startsWith('data: '))
581
+ continue;
582
+ const data = line.slice(6);
583
+ try {
584
+ const parsed = JSON.parse(data);
585
+ // Track usage
586
+ if (parsed.usage) {
587
+ usageData = parsed;
588
+ }
589
+ if (parsed.type === 'message_delta' && parsed.usage) {
590
+ usageData = parsed;
591
+ }
592
+ if (parsed.type === 'content_block_start') {
593
+ currentBlockIndex = parsed.index;
594
+ const block = parsed.content_block;
595
+ if (block.type === 'text') {
596
+ currentBlockType = 'text';
597
+ }
598
+ else if (block.type === 'tool_use') {
599
+ currentBlockType = 'tool_use';
600
+ currentToolName = block.name || '';
601
+ currentToolId = block.id || '';
602
+ currentToolInput = '';
603
+ }
604
+ }
605
+ else if (parsed.type === 'content_block_delta') {
606
+ if (currentBlockType === 'text' && parsed.delta?.text) {
607
+ content += parsed.delta.text;
608
+ onChunk(parsed.delta.text);
609
+ }
610
+ else if (currentBlockType === 'tool_use' && parsed.delta?.partial_json) {
611
+ currentToolInput += parsed.delta.partial_json;
612
+ }
613
+ }
614
+ else if (parsed.type === 'content_block_stop') {
615
+ if (currentBlockType === 'tool_use') {
616
+ contentBlocks.push({
617
+ type: 'tool_use',
618
+ id: currentToolId,
619
+ name: currentToolName,
620
+ input: tryParseJSON(currentToolInput),
621
+ });
622
+ }
623
+ currentBlockType = '';
624
+ }
625
+ }
626
+ catch {
627
+ // Ignore parse errors
628
+ }
629
+ }
630
+ }
631
+ // Track token usage
632
+ if (usageData) {
633
+ const usage = extractAnthropicUsage(usageData);
634
+ if (usage)
635
+ recordTokenUsage(usage, model, providerId);
636
+ }
637
+ const toolCalls = parseAnthropicToolCalls(contentBlocks);
638
+ return { content, toolCalls, usedNativeTools: true };
639
+ }
640
+ function tryParseJSON(str) {
641
+ try {
642
+ return JSON.parse(str);
643
+ }
644
+ catch {
645
+ return {};
646
+ }
647
+ }
648
+ /**
649
+ * Handle streaming response (text-based fallback)
420
650
  */
421
651
  async function handleStream(body, protocol, onChunk) {
422
652
  const reader = body.getReader();
@@ -515,6 +745,11 @@ export async function runAgent(prompt, projectContext, options = {}) {
515
745
  let systemPrompt = useNativeTools
516
746
  ? getAgentSystemPrompt(projectContext)
517
747
  : getFallbackSystemPrompt(projectContext);
748
+ // Inject project rules (from .codeep/rules.md or CODEEP.md)
749
+ const projectRules = loadProjectRules(projectContext.root);
750
+ if (projectRules) {
751
+ systemPrompt += projectRules;
752
+ }
518
753
  if (smartContextStr) {
519
754
  systemPrompt += '\n\n' + smartContextStr;
520
755
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,250 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ // Mock 'fs' before importing the module under test - use importOriginal to
3
+ // preserve all fs exports that transitive dependencies need (e.g. mkdirSync).
4
+ vi.mock('fs', async (importOriginal) => {
5
+ const actual = await importOriginal();
6
+ return {
7
+ ...actual,
8
+ existsSync: vi.fn(),
9
+ readFileSync: vi.fn(),
10
+ };
11
+ });
12
+ import { existsSync, readFileSync } from 'fs';
13
+ import { join } from 'path';
14
+ import { loadProjectRules, formatAgentResult } from './agent.js';
15
+ // Cast mocked functions for convenience
16
+ const mockExistsSync = existsSync;
17
+ const mockReadFileSync = readFileSync;
18
+ describe('loadProjectRules', () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+ it('should return formatted rules when .codeep/rules.md exists', () => {
23
+ const projectRoot = '/my/project';
24
+ const rulesContent = 'Always use TypeScript.\nNo console.log in production.';
25
+ mockExistsSync.mockImplementation((filePath) => {
26
+ return filePath === join(projectRoot, '.codeep', 'rules.md');
27
+ });
28
+ mockReadFileSync.mockReturnValue(rulesContent);
29
+ const result = loadProjectRules(projectRoot);
30
+ expect(result).toContain('## Project Rules');
31
+ expect(result).toContain('Always use TypeScript.');
32
+ expect(result).toContain('No console.log in production.');
33
+ expect(mockExistsSync).toHaveBeenCalledWith(join(projectRoot, '.codeep', 'rules.md'));
34
+ expect(mockReadFileSync).toHaveBeenCalledWith(join(projectRoot, '.codeep', 'rules.md'), 'utf-8');
35
+ });
36
+ it('should fall back to CODEEP.md when .codeep/rules.md does not exist', () => {
37
+ const projectRoot = '/my/project';
38
+ const rulesContent = 'Follow the style guide.';
39
+ mockExistsSync.mockImplementation((filePath) => {
40
+ return filePath === join(projectRoot, 'CODEEP.md');
41
+ });
42
+ mockReadFileSync.mockReturnValue(rulesContent);
43
+ const result = loadProjectRules(projectRoot);
44
+ expect(result).toContain('## Project Rules');
45
+ expect(result).toContain('Follow the style guide.');
46
+ // Should have checked .codeep/rules.md first
47
+ expect(mockExistsSync).toHaveBeenCalledWith(join(projectRoot, '.codeep', 'rules.md'));
48
+ // Then checked CODEEP.md
49
+ expect(mockExistsSync).toHaveBeenCalledWith(join(projectRoot, 'CODEEP.md'));
50
+ expect(mockReadFileSync).toHaveBeenCalledWith(join(projectRoot, 'CODEEP.md'), 'utf-8');
51
+ });
52
+ it('should return empty string when neither rules file exists', () => {
53
+ const projectRoot = '/my/project';
54
+ mockExistsSync.mockReturnValue(false);
55
+ const result = loadProjectRules(projectRoot);
56
+ expect(result).toBe('');
57
+ expect(mockExistsSync).toHaveBeenCalledTimes(2);
58
+ expect(mockReadFileSync).not.toHaveBeenCalled();
59
+ });
60
+ it('should return empty string when rules file exists but is empty', () => {
61
+ const projectRoot = '/my/project';
62
+ mockExistsSync.mockImplementation((filePath) => {
63
+ return filePath === join(projectRoot, '.codeep', 'rules.md');
64
+ });
65
+ mockReadFileSync.mockReturnValue(' \n \n ');
66
+ const result = loadProjectRules(projectRoot);
67
+ // Empty after trim, so should skip and check next candidate
68
+ expect(mockExistsSync).toHaveBeenCalledWith(join(projectRoot, '.codeep', 'rules.md'));
69
+ // Since the first file was empty (whitespace-only), it checks the second
70
+ expect(mockExistsSync).toHaveBeenCalledWith(join(projectRoot, 'CODEEP.md'));
71
+ });
72
+ it('should return empty string when both files exist but are empty', () => {
73
+ const projectRoot = '/my/project';
74
+ mockExistsSync.mockReturnValue(true);
75
+ mockReadFileSync.mockReturnValue(' ');
76
+ const result = loadProjectRules(projectRoot);
77
+ expect(result).toBe('');
78
+ });
79
+ it('should return empty string when readFileSync throws an error', () => {
80
+ const projectRoot = '/my/project';
81
+ mockExistsSync.mockReturnValue(true);
82
+ mockReadFileSync.mockImplementation(() => {
83
+ throw new Error('EACCES: permission denied');
84
+ });
85
+ const result = loadProjectRules(projectRoot);
86
+ // Both candidates exist but both throw on read, so should return ''
87
+ expect(result).toBe('');
88
+ // Should have attempted to read both files
89
+ expect(mockReadFileSync).toHaveBeenCalledTimes(2);
90
+ });
91
+ it('should fall back to CODEEP.md when .codeep/rules.md read throws', () => {
92
+ const projectRoot = '/my/project';
93
+ mockExistsSync.mockReturnValue(true);
94
+ mockReadFileSync.mockImplementation((filePath) => {
95
+ if (filePath === join(projectRoot, '.codeep', 'rules.md')) {
96
+ throw new Error('EACCES: permission denied');
97
+ }
98
+ return 'Fallback rules content';
99
+ });
100
+ const result = loadProjectRules(projectRoot);
101
+ expect(result).toContain('## Project Rules');
102
+ expect(result).toContain('Fallback rules content');
103
+ expect(mockReadFileSync).toHaveBeenCalledTimes(2);
104
+ });
105
+ it('should prefer .codeep/rules.md over CODEEP.md when both exist', () => {
106
+ const projectRoot = '/my/project';
107
+ mockExistsSync.mockReturnValue(true);
108
+ mockReadFileSync.mockImplementation((filePath) => {
109
+ if (filePath === join(projectRoot, '.codeep', 'rules.md')) {
110
+ return 'Primary rules';
111
+ }
112
+ return 'Secondary rules';
113
+ });
114
+ const result = loadProjectRules(projectRoot);
115
+ expect(result).toContain('Primary rules');
116
+ expect(result).not.toContain('Secondary rules');
117
+ // Should only read the first file since it had content
118
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
119
+ });
120
+ it('should include the MUST follow preamble in the returned string', () => {
121
+ const projectRoot = '/my/project';
122
+ mockExistsSync.mockImplementation((filePath) => {
123
+ return filePath === join(projectRoot, '.codeep', 'rules.md');
124
+ });
125
+ mockReadFileSync.mockReturnValue('Some rules');
126
+ const result = loadProjectRules(projectRoot);
127
+ expect(result).toContain('You MUST follow these rules');
128
+ });
129
+ });
130
+ describe('formatAgentResult', () => {
131
+ it('should format a successful result with iterations', () => {
132
+ const result = {
133
+ success: true,
134
+ iterations: 3,
135
+ actions: [],
136
+ finalResponse: 'All done',
137
+ };
138
+ const formatted = formatAgentResult(result);
139
+ expect(formatted).toContain('Agent completed in 3 iteration(s)');
140
+ });
141
+ it('should format a failed result with error message', () => {
142
+ const result = {
143
+ success: false,
144
+ iterations: 5,
145
+ actions: [],
146
+ finalResponse: '',
147
+ error: 'Exceeded maximum duration',
148
+ };
149
+ const formatted = formatAgentResult(result);
150
+ expect(formatted).toContain('Agent failed: Exceeded maximum duration');
151
+ });
152
+ it('should format an aborted result', () => {
153
+ const result = {
154
+ success: false,
155
+ iterations: 2,
156
+ actions: [],
157
+ finalResponse: 'Agent was stopped by user',
158
+ aborted: true,
159
+ };
160
+ const formatted = formatAgentResult(result);
161
+ expect(formatted).toContain('Agent was stopped by user');
162
+ });
163
+ it('should list actions when present', () => {
164
+ const result = {
165
+ success: true,
166
+ iterations: 2,
167
+ actions: [
168
+ { type: 'read', target: 'src/index.ts', result: 'success', timestamp: Date.now() },
169
+ { type: 'write', target: 'src/new-file.ts', result: 'success', timestamp: Date.now() },
170
+ { type: 'command', target: 'npm install', result: 'error', timestamp: Date.now() },
171
+ ],
172
+ finalResponse: 'Done',
173
+ };
174
+ const formatted = formatAgentResult(result);
175
+ expect(formatted).toContain('Actions performed:');
176
+ expect(formatted).toContain('read: src/index.ts');
177
+ expect(formatted).toContain('write: src/new-file.ts');
178
+ expect(formatted).toContain('command: npm install');
179
+ });
180
+ it('should show check mark for successful actions and cross for errors', () => {
181
+ const result = {
182
+ success: true,
183
+ iterations: 1,
184
+ actions: [
185
+ { type: 'write', target: 'file.ts', result: 'success', timestamp: Date.now() },
186
+ { type: 'edit', target: 'other.ts', result: 'error', timestamp: Date.now() },
187
+ ],
188
+ finalResponse: 'Done',
189
+ };
190
+ const formatted = formatAgentResult(result);
191
+ const lines = formatted.split('\n');
192
+ const successLine = lines.find(l => l.includes('write: file.ts'));
193
+ const errorLine = lines.find(l => l.includes('edit: other.ts'));
194
+ expect(successLine).toMatch(/✓/);
195
+ expect(errorLine).toMatch(/✗/);
196
+ });
197
+ it('should not show actions section when there are no actions', () => {
198
+ const result = {
199
+ success: true,
200
+ iterations: 1,
201
+ actions: [],
202
+ finalResponse: 'Nothing to do',
203
+ };
204
+ const formatted = formatAgentResult(result);
205
+ expect(formatted).not.toContain('Actions performed:');
206
+ });
207
+ it('should format a failed result without aborted flag', () => {
208
+ const result = {
209
+ success: false,
210
+ iterations: 10,
211
+ actions: [
212
+ { type: 'read', target: 'config.json', result: 'success', timestamp: Date.now() },
213
+ ],
214
+ finalResponse: '',
215
+ error: 'Exceeded maximum of 10 iterations',
216
+ };
217
+ const formatted = formatAgentResult(result);
218
+ expect(formatted).toContain('Agent failed: Exceeded maximum of 10 iterations');
219
+ expect(formatted).toContain('Actions performed:');
220
+ expect(formatted).toContain('read: config.json');
221
+ });
222
+ it('should format result with single iteration correctly', () => {
223
+ const result = {
224
+ success: true,
225
+ iterations: 1,
226
+ actions: [],
227
+ finalResponse: 'Quick task',
228
+ };
229
+ const formatted = formatAgentResult(result);
230
+ expect(formatted).toContain('Agent completed in 1 iteration(s)');
231
+ });
232
+ it('should handle all action types', () => {
233
+ const actionTypes = ['read', 'write', 'edit', 'delete', 'command', 'search', 'list', 'mkdir', 'fetch'];
234
+ const result = {
235
+ success: true,
236
+ iterations: 5,
237
+ actions: actionTypes.map(type => ({
238
+ type,
239
+ target: `target-for-${type}`,
240
+ result: 'success',
241
+ timestamp: Date.now(),
242
+ })),
243
+ finalResponse: 'Done',
244
+ };
245
+ const formatted = formatAgentResult(result);
246
+ for (const type of actionTypes) {
247
+ expect(formatted).toContain(`${type}: target-for-${type}`);
248
+ }
249
+ });
250
+ });