codemini-cli 0.2.2 → 0.2.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
@@ -18,20 +18,34 @@ Routing:
18
18
  - execute directly
19
19
  - do not force brainstorming
20
20
 
21
- 2. If the goal is clear but there are multiple reasonable implementation paths:
21
+ 2. If the task is a non-trivial implementation that likely needs codebase exploration, touches multiple areas, changes shared behavior, or needs explicit review/testing before coding:
22
+ - prefer `auto plan`
23
+ - inspect first, then present a short implementation plan for approval
24
+ - do not jump straight into coding
25
+ - do not use `brainstorm` as a substitute for implementation planning
26
+
27
+ 3. If the goal is clear but there are multiple reasonable implementation paths and the missing piece is mainly user preference, tradeoff choice, or one key constraint:
22
28
  - use `brainstorm`
23
- - give 2-3 short options
24
- - do not choose for the user unless the user explicitly asks for a recommendation
29
+ - ask exactly one clarifying question first
30
+ - do not give options, recommendations, or a tentative solution in the same response
31
+ - stop after the question and wait for the user's answer before continuing
25
32
 
26
- 3. If the request is still missing a key constraint or success condition:
33
+ 4. If the request is still missing a key constraint or success condition:
27
34
  - ask exactly one clarifying question
28
35
  - do not give options yet
29
36
  - do not write code yet
37
+ - stop after the question and wait for the user's answer
30
38
 
31
- 4. If the request is greenfield and underspecified, such as "build a page", "make a site", "generate an app", or similar:
39
+ 5. If the request is greenfield and underspecified, such as "build a page", "make a site", "generate an app", or similar:
32
40
  - treat it as missing key constraints by default
33
41
  - ask one high-value question before coding
34
42
  - do not assume features, storage model, or scope unless the user already gave them
43
+ - stop after the question and wait for the user's answer
44
+
45
+ Decision boundary:
46
+ - Use `brainstorm` when one focused user answer will determine the direction.
47
+ - Use `auto plan` when the task is already implementation-shaped but the work is large enough that you should explore first and get sign-off on the plan.
48
+ - If both could apply, prefer `brainstorm` first when the core uncertainty is user intent; prefer `auto plan` first when the core uncertainty is codebase impact and execution shape.
35
49
 
36
50
  Tool order:
37
51
  - prefer `grep` first for content search and candidate discovery
@@ -71,7 +85,7 @@ Run the relevant test, check, or command before saying work is fixed or complete
71
85
  Default workflow:
72
86
  - Search with `grep`
73
87
  - Inspect local context with `read`
74
- - If the request is unclear, first decide: ask one question, brainstorm, or proceed
88
+ - If the request is unclear, first decide: ask one question, brainstorm, auto plan, or proceed
75
89
  - Plan the next smallest step
76
90
  - Delegate if the work is independent
77
91
  - Edit with `edit`
package/src/cli.js CHANGED
@@ -4,7 +4,7 @@ import { handleConfig } from './commands/config.js';
4
4
  import { handleDoctor } from './commands/doctor.js';
5
5
  import { handleSkill } from './commands/skill.js';
6
6
 
7
- const VERSION = '0.2.2';
7
+ const VERSION = '0.2.4';
8
8
 
9
9
  function printHelp() {
10
10
  console.log(`codemini ${VERSION}
@@ -35,7 +35,7 @@ export async function handleRun(args) {
35
35
  }
36
36
 
37
37
  const config = await loadConfig();
38
- const { definitions, handlers } = getBuiltinTools({
38
+ const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
39
39
  workspaceRoot: process.cwd(),
40
40
  config
41
41
  });
@@ -47,6 +47,8 @@ export async function handleRun(args) {
47
47
  model: parsed.model || config.model.name,
48
48
  toolDefinitions: definitions,
49
49
  toolHandlers: handlers,
50
+ toolFormatters: formatters,
51
+ deferredDefinitions,
50
52
  maxSteps: parsed.maxSteps,
51
53
  requestCompletion: async ({ messages, tools, model }) =>
52
54
  createChatCompletion({
@@ -1,3 +1,7 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+
1
5
  function safeJsonParse(raw) {
2
6
  if (!raw || typeof raw !== 'string') return {};
3
7
  try {
@@ -13,7 +17,198 @@ function clipToolResult(result, maxChars = 12000) {
13
17
  return `${raw.slice(0, maxChars)}\n... [tool result truncated ${raw.length - maxChars} chars]`;
14
18
  }
15
19
 
16
- function summarizeToolResult(result) {
20
+ function compactToolResult(result, toolName, args, maxChars = 12000) {
21
+ if (result === null || result === undefined) return 'no output';
22
+ if (typeof result === 'string') {
23
+ if (result.length <= maxChars) return result;
24
+ return `${result.slice(0, maxChars)}\n... [tool result truncated ${result.length - maxChars} chars, original: ${result.length}]`;
25
+ }
26
+ if (typeof result !== 'object') return String(result);
27
+
28
+ const obj = result;
29
+ const rawLen = JSON.stringify(obj).length;
30
+
31
+ // Read file result: { path, phase, content, ... }
32
+ if ('path' in obj && 'phase' in obj && obj.phase === 'content') {
33
+ const header = `[File: ${obj.path}, lines ${obj.start_line || 1}-${obj.end_line || '?'}${obj.total_lines ? ` of ${obj.total_lines}` : ''}${obj.truncated ? ', truncated' : ''}]`;
34
+ const content = obj.content || obj.text || '';
35
+ if (typeof content !== 'string' || content.length <= maxChars) {
36
+ const body = typeof content === 'string' ? content : JSON.stringify(content);
37
+ return body.length <= maxChars ? `${header}\n${body}` : `${header}\n${body.slice(0, maxChars)}\n... [omitted ${body.length - maxChars} chars, original: ${rawLen}]`;
38
+ }
39
+ // Keep head + tail
40
+ const headLen = Math.floor(maxChars * 0.6);
41
+ const tailLen = Math.floor(maxChars * 0.3);
42
+ return `${header}\n${content.slice(0, headLen)}\n... [omitted ${content.length - headLen - tailLen} chars] ...\n${content.slice(-tailLen)}\n[original: ${rawLen} chars]`;
43
+ }
44
+
45
+ // File edit/write result: { path, action, ... }
46
+ if ('path' in obj && 'action' in obj) {
47
+ const summary = summarizeToolResult(obj);
48
+ const diff = obj.diff || obj.patch || obj.content_preview || '';
49
+ if (diff && typeof diff === 'string' && diff.length <= 800) {
50
+ return `${summary}\n${diff}`;
51
+ }
52
+ if (diff) {
53
+ return `${summary}\n${diff.slice(0, 800)}\n... [diff truncated, original: ${rawLen}]`;
54
+ }
55
+ return `${summary} [original: ${rawLen} chars]`;
56
+ }
57
+
58
+ // Shell command result: { stdout, stderr, code, ... }
59
+ if ('stdout' in obj || 'stderr' in obj || 'code' in obj) {
60
+ const command = String(obj.command || '').slice(0, 200);
61
+ const stdout = String(obj.stdout || '').slice(0, 500);
62
+ const stderr = String(obj.stderr || '').slice(0, 500);
63
+ const code = obj.code ?? 0;
64
+ const parts = [`[exit: ${code}]`];
65
+ if (command) parts.push(`command: ${command}`);
66
+ if (stdout) parts.push(`stdout:\n${stdout}`);
67
+ if (stderr) parts.push(`stderr:\n${stderr}`);
68
+ if (rawLen > 2000) parts.push(`[original: ${rawLen} chars]`);
69
+ return parts.join('\n');
70
+ }
71
+
72
+ // Array results (file lists, grep results, etc.)
73
+ if (Array.isArray(obj)) {
74
+ const maxItems = 50;
75
+ if (obj.length <= maxItems) {
76
+ const serialized = JSON.stringify(obj);
77
+ return serialized.length <= maxChars ? serialized : clipToolResult(obj, maxChars);
78
+ }
79
+ const kept = obj.slice(0, maxItems);
80
+ const items = typeof kept[0] === 'string'
81
+ ? kept.join('\n')
82
+ : kept.map((item) => JSON.stringify(item)).join('\n');
83
+ return `${items}\n... and ${obj.length - maxItems} more items [total: ${obj.length}, original: ${rawLen} chars]`;
84
+ }
85
+
86
+ // Patch result: { files: [...] }
87
+ if ('files' in obj && Array.isArray(obj.files)) {
88
+ return `patched ${obj.files.length} file(s): ${obj.files.slice(0, 10).join(', ')}${obj.files.length > 10 ? ` ... and ${obj.files.length - 10} more` : ''} [original: ${rawLen}]`;
89
+ }
90
+
91
+ // Task results
92
+ if ('created' in obj && Array.isArray(obj.created)) {
93
+ return `created ${obj.created.length} task(s)`;
94
+ }
95
+ if ('tasks' in obj && Array.isArray(obj.tasks)) {
96
+ return `${obj.tasks.length} task(s)`;
97
+ }
98
+
99
+ // Fallback: clip with reduced limit
100
+ return clipToolResult(obj, Math.min(maxChars, 4000));
101
+ }
102
+
103
+ // ─── P0: Large result disk store ─────────────────────────────────────
104
+
105
+ const TOOL_RESULT_DISK_THRESHOLD = 6000;
106
+ const PREVIEW_SIZE_BYTES = 2000;
107
+ const TOOL_RESULTS_SUBDIR = 'tool-results';
108
+
109
+ let currentResultDir = null;
110
+ let resultDirReady = false;
111
+ const storedResults = new Map(); // callId -> { filePath, summary }
112
+ const readCache = new Map(); // "path:startLine:endLine:mtimeMs" -> true
113
+
114
+ function generatePreview(content) {
115
+ if (content.length <= PREVIEW_SIZE_BYTES) {
116
+ return { preview: content, hasMore: false };
117
+ }
118
+ const truncated = content.slice(0, PREVIEW_SIZE_BYTES);
119
+ const lastNewline = truncated.lastIndexOf('\n');
120
+ const cutPoint = lastNewline > PREVIEW_SIZE_BYTES * 0.5 ? lastNewline : PREVIEW_SIZE_BYTES;
121
+ return { preview: content.slice(0, cutPoint), hasMore: true };
122
+ }
123
+
124
+ function formatFileSize(chars) {
125
+ if (chars < 1024) return `${chars} B`;
126
+ return `${(chars / 1024).toFixed(1)} KB`;
127
+ }
128
+
129
+ export function setResultDir(dir) {
130
+ currentResultDir = dir ? path.join(dir, TOOL_RESULTS_SUBDIR) : null;
131
+ resultDirReady = false;
132
+ }
133
+
134
+ async function ensureResultDir() {
135
+ if (!currentResultDir) return false;
136
+ if (!resultDirReady) {
137
+ await fs.mkdir(currentResultDir, { recursive: true });
138
+ resultDirReady = true;
139
+ }
140
+ return true;
141
+ }
142
+
143
+ async function storeResultIfNeeded(callId, formattedContent, rawResult) {
144
+ if (formattedContent.length <= TOOL_RESULT_DISK_THRESHOLD) {
145
+ return formattedContent;
146
+ }
147
+ try {
148
+ const ready = await ensureResultDir();
149
+ const dir = ready ? currentResultDir : path.join(os.tmpdir(), 'codemini-results');
150
+ if (!resultDirReady && dir === currentResultDir) {
151
+ await fs.mkdir(dir, { recursive: true });
152
+ } else if (!resultDirReady) {
153
+ await fs.mkdir(dir, { recursive: true });
154
+ }
155
+ const filePath = path.join(dir, `${callId}.txt`);
156
+ const payload = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
157
+ await fs.writeFile(filePath, payload, 'utf-8');
158
+ const summary = summarizeToolResult(rawResult);
159
+ const { preview, hasMore } = generatePreview(payload);
160
+ storedResults.set(callId, { filePath, summary });
161
+
162
+ return `<persisted-output>
163
+ Output too large (${formatFileSize(payload.length)}). Full output saved to: ${filePath}
164
+
165
+ Preview (first ${formatFileSize(PREVIEW_SIZE_BYTES)}):
166
+ ${preview}${hasMore ? '\n...' : ''}
167
+
168
+ Summary: ${summary}
169
+ </persisted-output>`;
170
+ } catch {
171
+ return formattedContent;
172
+ }
173
+ }
174
+
175
+ export function clearResultStore() {
176
+ const files = [];
177
+ for (const [, val] of storedResults) {
178
+ files.push(val.filePath);
179
+ }
180
+ storedResults.clear();
181
+ readCache.clear();
182
+ return Promise.allSettled(files.map((f) => fs.unlink(f).catch(() => {})));
183
+ }
184
+
185
+ // ─── Read deduplication ─────────────────────────────────────────────
186
+
187
+ export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
188
+ const key = `${filePath}:${startLine || 0}:${endLine || 0}:${mtimeMs}`;
189
+ if (readCache.has(key)) {
190
+ return true;
191
+ }
192
+ readCache.set(key, true);
193
+ // Keep cache bounded
194
+ if (readCache.size > 100) {
195
+ const firstKey = readCache.keys().next().value;
196
+ readCache.delete(firstKey);
197
+ }
198
+ return false;
199
+ }
200
+
201
+ // ─── P1a: Read-only tool classification ──────────────────────────────
202
+
203
+ const READ_ONLY_TOOLS = new Set([
204
+ 'read', 'grep', 'glob', 'list',
205
+ 'ast_query', 'read_ast_node', 'generate_diff',
206
+ 'list_services', 'get_service_status', 'get_service_logs'
207
+ ]);
208
+
209
+ // ─── Exported helpers ────────────────────────────────────────────────
210
+
211
+ export function summarizeToolResult(result) {
17
212
  if (result === null || result === undefined) return 'no output';
18
213
  if (typeof result === 'string') {
19
214
  const oneLine = result.replace(/\s+/g, ' ').trim();
@@ -106,7 +301,7 @@ function summarizeToolResult(result) {
106
301
  return String(result);
107
302
  }
108
303
 
109
- function trimInline(value, maxLen = 72) {
304
+ export function trimInline(value, maxLen = 72) {
110
305
  const s = String(value || '').replace(/\s+/g, ' ').trim();
111
306
  if (!s) return '';
112
307
  if (s.length <= maxLen) return s;
@@ -171,6 +366,18 @@ function formatToolDisplayName(name, args) {
171
366
  return name;
172
367
  }
173
368
 
369
+ // ─── Format a single tool result using per-tool formatter or fallback ──
370
+
371
+ function formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars) {
372
+ if (toolFormatters && typeof toolFormatters[toolName] === 'function') {
373
+ const formatted = toolFormatters[toolName](toolResult, args);
374
+ if (typeof formatted === 'string') return formatted;
375
+ }
376
+ return compactToolResult(toolResult, toolName, args, toolResultMaxChars);
377
+ }
378
+
379
+ // ─── Main agent loop ────────────────────────────────────────────────
380
+
174
381
  export async function runAgentLoop({
175
382
  systemPrompt,
176
383
  userPrompt,
@@ -184,7 +391,9 @@ export async function runAgentLoop({
184
391
  executionMode = 'auto',
185
392
  alwaysAllowTools = [],
186
393
  requestToolApproval,
187
- toolResultMaxChars = 12000
394
+ toolResultMaxChars = 12000,
395
+ toolFormatters = {},
396
+ deferredDefinitions = {}
188
397
  }) {
189
398
  const messages = [];
190
399
  if (systemPrompt) {
@@ -201,12 +410,15 @@ export async function runAgentLoop({
201
410
  let lastAssistantText = '';
202
411
  const alwaysAllowSet = new Set((Array.isArray(alwaysAllowTools) ? alwaysAllowTools : []).map((t) => String(t)));
203
412
 
413
+ // Mutable tool list — grows as tool_search loads deferred tools
414
+ const activeTools = [...toolDefinitions];
415
+
204
416
  for (let step = 0; step < maxSteps; step += 1) {
205
417
  if (onEvent) onEvent({ type: 'step:start', step: step + 1 });
206
418
  const completion = await requestCompletion({
207
419
  model,
208
420
  messages,
209
- tools: toolDefinitions
421
+ tools: activeTools
210
422
  });
211
423
 
212
424
  const toolCalls = Array.isArray(completion.toolCalls) ? completion.toolCalls : [];
@@ -238,15 +450,32 @@ export async function runAgentLoop({
238
450
  }
239
451
 
240
452
  if (executionMode === 'plan') {
241
- finalText = `${assistantText || ''}\n\n[plan mode] ${toolCalls.length} tool call(s) were planned but not executed.`;
453
+ const plannedLines = callsToPlanSummary(toolCalls);
454
+ finalText = [
455
+ assistantText || '',
456
+ '',
457
+ `[plan mode] ${toolCalls.length} tool call(s) were planned but not executed.`,
458
+ plannedLines.length > 0 ? 'Planned exploration:' : '',
459
+ ...plannedLines
460
+ ]
461
+ .filter(Boolean)
462
+ .join('\n');
242
463
  return { text: finalText.trim(), messages, steps: step + 1 };
243
464
  }
244
465
 
245
- for (const call of toolCalls) {
466
+ // ─── P1a: Partition into read-only (parallel) and write (serial) ──
467
+
468
+ const callsWithMeta = toolCalls.map((call) => {
246
469
  const args = safeJsonParse(call.arguments);
247
470
  const toolName = normalizeToolCallName(call.name);
248
471
  const displayName = formatToolDisplayName(toolName, args);
249
- const startedAt = Date.now();
472
+ const isReadOnly = READ_ONLY_TOOLS.has(toolName);
473
+ return { call, args, toolName, displayName, isReadOnly };
474
+ });
475
+
476
+ // Approval checks first — must be done synchronously before any execution
477
+ const approvalResults = new Map();
478
+ for (const { call, toolName, displayName, args } of callsWithMeta) {
250
479
  let approved = true;
251
480
  if (executionMode === 'normal' && !alwaysAllowSet.has(toolName)) {
252
481
  approved = false;
@@ -260,26 +489,23 @@ export async function runAgentLoop({
260
489
  approved = Boolean(decision?.approved);
261
490
  }
262
491
  }
492
+ approvalResults.set(call.id, approved);
493
+ }
494
+
495
+ // Collect results keyed by call.id, then write to messages in original order
496
+ const resultEntries = new Map(); // call.id -> { content, error? }
263
497
 
264
- if (!approved) {
498
+ // Helper to execute a single tool call
499
+ async function executeOne({ call, args, toolName, displayName, isReadOnly }) {
500
+ const startedAt = Date.now();
501
+
502
+ if (!approvalResults.get(call.id)) {
265
503
  if (onEvent) onEvent({ type: 'tool:blocked', name: displayName, id: call.id, arguments: args });
266
- const blockedMessage = {
267
- role: 'tool',
268
- tool_call_id: call.id,
269
- content: JSON.stringify({ blocked: true, reason: 'Tool call requires approval in normal mode' })
504
+ return {
505
+ callId: call.id,
506
+ content: JSON.stringify({ blocked: true, reason: 'Tool call requires approval in normal mode' }),
507
+ blocked: true
270
508
  };
271
- messages.push(blockedMessage);
272
- if (onEvent) {
273
- onEvent({
274
- type: 'tool:result',
275
- name: displayName,
276
- id: call.id,
277
- arguments: args,
278
- content: blockedMessage.content,
279
- blocked: true
280
- });
281
- }
282
- continue;
283
509
  }
284
510
 
285
511
  if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id, arguments: args });
@@ -287,6 +513,7 @@ export async function runAgentLoop({
287
513
  if (!handler) {
288
514
  throw new Error(`Unknown tool: ${call.name}`);
289
515
  }
516
+
290
517
  let toolResult;
291
518
  try {
292
519
  toolResult = await handler(args);
@@ -294,58 +521,81 @@ export async function runAgentLoop({
294
521
  const durationMs = Date.now() - startedAt;
295
522
  const message = error instanceof Error ? error.message : String(error);
296
523
  if (onEvent) {
297
- onEvent({
298
- type: 'tool:error',
299
- name: displayName,
300
- id: call.id,
301
- arguments: args,
302
- durationMs,
303
- summary: trimInline(message, 120)
304
- });
524
+ onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: args, durationMs, summary: trimInline(message, 120) });
305
525
  }
306
- const toolMessage = {
307
- role: 'tool',
308
- tool_call_id: call.id,
309
- content: clipToolResult({ error: message }, toolResultMaxChars)
526
+ return {
527
+ callId: call.id,
528
+ content: clipToolResult({ error: message }, toolResultMaxChars),
529
+ error: true
310
530
  };
311
- messages.push(toolMessage);
531
+ }
532
+
533
+ const durationMs = Date.now() - startedAt;
534
+ if (onEvent) {
535
+ onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: args, durationMs, summary: summarizeToolResult(toolResult) });
536
+ }
537
+
538
+ // P1b: Use per-tool formatter if available, else fallback
539
+ let formatted = formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars);
540
+
541
+ // P2: If tool_search loaded deferred tools, inject their schemas into activeTools
542
+ if (toolName === 'tool_search' && toolResult && Array.isArray(toolResult.schemas)) {
543
+ for (const schema of toolResult.schemas) {
544
+ const name = schema?.function?.name;
545
+ if (name && !activeTools.some((t) => t?.function?.name === name)) {
546
+ activeTools.push(schema);
547
+ }
548
+ }
549
+ }
550
+
551
+ // P0: Persist to disk if still large
552
+ formatted = await storeResultIfNeeded(call.id, formatted, toolResult);
553
+
554
+ return { callId: call.id, content: formatted };
555
+ }
556
+
557
+ // Separate read-only and write calls, preserving order
558
+ const readOnlyCalls = callsWithMeta.filter((c) => c.isReadOnly && approvalResults.get(c.call.id));
559
+ const writeCalls = callsWithMeta.filter((c) => !c.isReadOnly || !approvalResults.get(c.call.id));
560
+
561
+ // Execute read-only calls in parallel
562
+ if (readOnlyCalls.length > 0) {
563
+ const readOnlyResults = await Promise.all(readOnlyCalls.map((c) => executeOne(c)));
564
+ for (const r of readOnlyResults) {
565
+ resultEntries.set(r.callId, r);
566
+ }
567
+ }
568
+
569
+ // Execute write calls serially
570
+ for (const c of writeCalls) {
571
+ const r = await executeOne(c);
572
+ resultEntries.set(r.callId, r);
573
+ }
574
+
575
+ // Write results to messages in original tool call order
576
+ for (const { call, displayName, args } of callsWithMeta) {
577
+ const entry = resultEntries.get(call.id);
578
+ if (!entry) continue;
579
+
580
+ if (entry.blocked) {
581
+ messages.push({ role: 'tool', tool_call_id: call.id, content: entry.content });
312
582
  if (onEvent) {
313
- onEvent({
314
- type: 'tool:result',
315
- name: displayName,
316
- id: call.id,
317
- arguments: args,
318
- content: toolMessage.content,
319
- error: true
320
- });
583
+ onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content, blocked: true });
321
584
  }
322
585
  continue;
323
586
  }
324
- const durationMs = Date.now() - startedAt;
325
- if (onEvent) {
326
- onEvent({
327
- type: 'tool:end',
328
- name: displayName,
329
- id: call.id,
330
- arguments: args,
331
- durationMs,
332
- summary: summarizeToolResult(toolResult)
333
- });
587
+
588
+ if (entry.error) {
589
+ messages.push({ role: 'tool', tool_call_id: call.id, content: entry.content });
590
+ if (onEvent) {
591
+ onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content, error: true });
592
+ }
593
+ continue;
334
594
  }
335
- const toolMessage = {
336
- role: 'tool',
337
- tool_call_id: call.id,
338
- content: clipToolResult(toolResult, toolResultMaxChars)
339
- };
340
- messages.push(toolMessage);
595
+
596
+ messages.push({ role: 'tool', tool_call_id: call.id, content: entry.content });
341
597
  if (onEvent) {
342
- onEvent({
343
- type: 'tool:result',
344
- name: displayName,
345
- id: call.id,
346
- arguments: args,
347
- content: toolMessage.content
348
- });
598
+ onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content });
349
599
  }
350
600
  }
351
601
  }
@@ -357,3 +607,12 @@ export async function runAgentLoop({
357
607
  steps: maxSteps
358
608
  };
359
609
  }
610
+
611
+ function callsToPlanSummary(toolCalls = []) {
612
+ return toolCalls
613
+ .slice(0, 8)
614
+ .map((call) => {
615
+ const args = safeJsonParse(call?.arguments);
616
+ return `- ${formatToolDisplayName(normalizeToolCallName(call?.name), args)}`;
617
+ });
618
+ }