codemini-cli 0.2.1 → 0.2.3

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.1",
3
+ "version": "0.2.3",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
@@ -20,18 +20,21 @@ Routing:
20
20
 
21
21
  2. If the goal is clear but there are multiple reasonable implementation paths:
22
22
  - use `brainstorm`
23
- - give 2-3 short options
24
- - do not choose for the user unless the user explicitly asks for a recommendation
23
+ - ask exactly one clarifying question first
24
+ - do not give options, recommendations, or a tentative solution in the same response
25
+ - stop after the question and wait for the user's answer before continuing
25
26
 
26
27
  3. If the request is still missing a key constraint or success condition:
27
28
  - ask exactly one clarifying question
28
29
  - do not give options yet
29
30
  - do not write code yet
31
+ - stop after the question and wait for the user's answer
30
32
 
31
33
  4. If the request is greenfield and underspecified, such as "build a page", "make a site", "generate an app", or similar:
32
34
  - treat it as missing key constraints by default
33
35
  - ask one high-value question before coding
34
36
  - do not assume features, storage model, or scope unless the user already gave them
37
+ - stop after the question and wait for the user's answer
35
38
 
36
39
  Tool order:
37
40
  - prefer `grep` first for content search and candidate discovery
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.1';
7
+ const VERSION = '0.2.3';
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 : [];
@@ -242,11 +454,19 @@ export async function runAgentLoop({
242
454
  return { text: finalText.trim(), messages, steps: step + 1 };
243
455
  }
244
456
 
245
- for (const call of toolCalls) {
457
+ // ─── P1a: Partition into read-only (parallel) and write (serial) ──
458
+
459
+ const callsWithMeta = toolCalls.map((call) => {
246
460
  const args = safeJsonParse(call.arguments);
247
461
  const toolName = normalizeToolCallName(call.name);
248
462
  const displayName = formatToolDisplayName(toolName, args);
249
- const startedAt = Date.now();
463
+ const isReadOnly = READ_ONLY_TOOLS.has(toolName);
464
+ return { call, args, toolName, displayName, isReadOnly };
465
+ });
466
+
467
+ // Approval checks first — must be done synchronously before any execution
468
+ const approvalResults = new Map();
469
+ for (const { call, toolName, displayName, args } of callsWithMeta) {
250
470
  let approved = true;
251
471
  if (executionMode === 'normal' && !alwaysAllowSet.has(toolName)) {
252
472
  approved = false;
@@ -260,26 +480,23 @@ export async function runAgentLoop({
260
480
  approved = Boolean(decision?.approved);
261
481
  }
262
482
  }
483
+ approvalResults.set(call.id, approved);
484
+ }
485
+
486
+ // Collect results keyed by call.id, then write to messages in original order
487
+ const resultEntries = new Map(); // call.id -> { content, error? }
488
+
489
+ // Helper to execute a single tool call
490
+ async function executeOne({ call, args, toolName, displayName, isReadOnly }) {
491
+ const startedAt = Date.now();
263
492
 
264
- if (!approved) {
493
+ if (!approvalResults.get(call.id)) {
265
494
  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' })
495
+ return {
496
+ callId: call.id,
497
+ content: JSON.stringify({ blocked: true, reason: 'Tool call requires approval in normal mode' }),
498
+ blocked: true
270
499
  };
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
500
  }
284
501
 
285
502
  if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id, arguments: args });
@@ -287,6 +504,7 @@ export async function runAgentLoop({
287
504
  if (!handler) {
288
505
  throw new Error(`Unknown tool: ${call.name}`);
289
506
  }
507
+
290
508
  let toolResult;
291
509
  try {
292
510
  toolResult = await handler(args);
@@ -294,58 +512,81 @@ export async function runAgentLoop({
294
512
  const durationMs = Date.now() - startedAt;
295
513
  const message = error instanceof Error ? error.message : String(error);
296
514
  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
- });
515
+ onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: args, durationMs, summary: trimInline(message, 120) });
305
516
  }
306
- const toolMessage = {
307
- role: 'tool',
308
- tool_call_id: call.id,
309
- content: clipToolResult({ error: message }, toolResultMaxChars)
517
+ return {
518
+ callId: call.id,
519
+ content: clipToolResult({ error: message }, toolResultMaxChars),
520
+ error: true
310
521
  };
311
- messages.push(toolMessage);
522
+ }
523
+
524
+ const durationMs = Date.now() - startedAt;
525
+ if (onEvent) {
526
+ onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: args, durationMs, summary: summarizeToolResult(toolResult) });
527
+ }
528
+
529
+ // P1b: Use per-tool formatter if available, else fallback
530
+ let formatted = formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars);
531
+
532
+ // P2: If tool_search loaded deferred tools, inject their schemas into activeTools
533
+ if (toolName === 'tool_search' && toolResult && Array.isArray(toolResult.schemas)) {
534
+ for (const schema of toolResult.schemas) {
535
+ const name = schema?.function?.name;
536
+ if (name && !activeTools.some((t) => t?.function?.name === name)) {
537
+ activeTools.push(schema);
538
+ }
539
+ }
540
+ }
541
+
542
+ // P0: Persist to disk if still large
543
+ formatted = await storeResultIfNeeded(call.id, formatted, toolResult);
544
+
545
+ return { callId: call.id, content: formatted };
546
+ }
547
+
548
+ // Separate read-only and write calls, preserving order
549
+ const readOnlyCalls = callsWithMeta.filter((c) => c.isReadOnly && approvalResults.get(c.call.id));
550
+ const writeCalls = callsWithMeta.filter((c) => !c.isReadOnly || !approvalResults.get(c.call.id));
551
+
552
+ // Execute read-only calls in parallel
553
+ if (readOnlyCalls.length > 0) {
554
+ const readOnlyResults = await Promise.all(readOnlyCalls.map((c) => executeOne(c)));
555
+ for (const r of readOnlyResults) {
556
+ resultEntries.set(r.callId, r);
557
+ }
558
+ }
559
+
560
+ // Execute write calls serially
561
+ for (const c of writeCalls) {
562
+ const r = await executeOne(c);
563
+ resultEntries.set(r.callId, r);
564
+ }
565
+
566
+ // Write results to messages in original tool call order
567
+ for (const { call, displayName, args } of callsWithMeta) {
568
+ const entry = resultEntries.get(call.id);
569
+ if (!entry) continue;
570
+
571
+ if (entry.blocked) {
572
+ messages.push({ role: 'tool', tool_call_id: call.id, content: entry.content });
312
573
  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
- });
574
+ onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content, blocked: true });
321
575
  }
322
576
  continue;
323
577
  }
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
- });
578
+
579
+ if (entry.error) {
580
+ messages.push({ role: 'tool', tool_call_id: call.id, content: entry.content });
581
+ if (onEvent) {
582
+ onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content, error: true });
583
+ }
584
+ continue;
334
585
  }
335
- const toolMessage = {
336
- role: 'tool',
337
- tool_call_id: call.id,
338
- content: clipToolResult(toolResult, toolResultMaxChars)
339
- };
340
- messages.push(toolMessage);
586
+
587
+ messages.push({ role: 'tool', tool_call_id: call.id, content: entry.content });
341
588
  if (onEvent) {
342
- onEvent({
343
- type: 'tool:result',
344
- name: displayName,
345
- id: call.id,
346
- arguments: args,
347
- content: toolMessage.content
348
- });
589
+ onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content });
349
590
  }
350
591
  }
351
592
  }
@@ -1,6 +1,6 @@
1
1
  import { parseInput } from './input-parser.js';
2
2
  import { loadCommandsAndSkills, renderCommandPrompt } from './command-loader.js';
3
- import { runAgentLoop } from './agent-loop.js';
3
+ import { runAgentLoop, setResultDir, clearResultStore } from './agent-loop.js';
4
4
  import fs from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import {
@@ -28,7 +28,7 @@ import {
28
28
  } from './context-compact.js';
29
29
  import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
30
30
  import { buildSystemPromptWithSoul } from './soul.js';
31
- import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir } from './paths.js';
31
+ import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSessionsDir } from './paths.js';
32
32
  import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
33
33
 
34
34
  function toOpenAIMessages(sessionMessages) {
@@ -1314,7 +1314,7 @@ async function askModel({
1314
1314
  ? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance. Prefer tools for fresh verification before assuming details.`
1315
1315
  : systemPrompt;
1316
1316
 
1317
- const { definitions, handlers } = getBuiltinTools({
1317
+ const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
1318
1318
  workspaceRoot: process.cwd(),
1319
1319
  config,
1320
1320
  sessionId: session.id,
@@ -1376,6 +1376,8 @@ async function askModel({
1376
1376
  alwaysAllowTools:
1377
1377
  alwaysAllowTools || config.execution?.always_allow_tools || ['run', 'read', 'write'],
1378
1378
  toolResultMaxChars: config.context?.tool_result_max_chars || 12000,
1379
+ toolFormatters: formatters,
1380
+ deferredDefinitions,
1379
1381
  requestCompletion: async ({ messages, tools, model: selectedModel }) => {
1380
1382
  if (onAgentEvent) onAgentEvent({ type: 'assistant:start' });
1381
1383
  return createChatCompletionStream({
@@ -1727,6 +1729,10 @@ export async function createChatRuntime({
1727
1729
  const baseSystemPrompt = systemPrompt;
1728
1730
  let executionMode = config.execution?.mode || 'auto';
1729
1731
  const commands = await loadCommandsAndSkills();
1732
+
1733
+ // Set up tool result store under session directory
1734
+ const sessionResultsDir = path.join(getSessionsDir(), String(currentSession.id));
1735
+ setResultDir(sessionResultsDir);
1730
1736
  const compactState = {
1731
1737
  backupMessages: null,
1732
1738
  autoEnabled: true,
@@ -2492,6 +2498,7 @@ export async function createChatRuntime({
2492
2498
  if (!targetId) return { type: 'system', text: 'Usage: /history resume <session_id>' };
2493
2499
  const loaded = await loadSession(targetId);
2494
2500
  currentSession = loaded;
2501
+ setResultDir(path.join(getSessionsDir(), String(targetId)));
2495
2502
  if (!historyIdCache.includes(targetId)) historyIdCache.unshift(targetId);
2496
2503
  historySessionCache = [
2497
2504
  { id: targetId, messageCount: Array.isArray(loaded.messages) ? loaded.messages.length : 0 },
@@ -1,3 +1,5 @@
1
+ import { summarizeToolResult, trimInline } from './agent-loop.js';
2
+
1
3
  function textFromContent(content) {
2
4
  if (typeof content === 'string') return content;
3
5
  if (Array.isArray(content)) {
@@ -30,11 +32,39 @@ function modeToKeepRecent(mode) {
30
32
 
31
33
  function buildLocalSummary(messages) {
32
34
  const lines = [];
33
- const limit = 12;
35
+ const limit = 16;
34
36
  for (const msg of messages.slice(-limit)) {
37
+ if (msg.role === 'tool') {
38
+ // Try to parse tool result as JSON for semantic summary
39
+ const text = textFromContent(msg.content);
40
+ let parsed;
41
+ try { parsed = JSON.parse(text); } catch { parsed = null; }
42
+ if (parsed && typeof parsed === 'object') {
43
+ const summary = summarizeToolResult(parsed);
44
+ lines.push(`- tool_result: ${summary}`);
45
+ } else {
46
+ const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
47
+ lines.push(`- tool_result: ${clipped}`);
48
+ }
49
+ continue;
50
+ }
51
+ if (msg.role === 'assistant') {
52
+ const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
53
+ const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
54
+ const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
55
+ const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
56
+ lines.push(`- assistant: ${clipped}${toolInfo}`);
57
+ continue;
58
+ }
59
+ if (msg.role === 'user') {
60
+ const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
61
+ const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
62
+ lines.push(`- user: ${clipped}`);
63
+ continue;
64
+ }
35
65
  const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
36
66
  if (!text) continue;
37
- const clipped = text.length > 160 ? `${text.slice(0, 160)}...` : text;
67
+ const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
38
68
  lines.push(`- ${msg.role}: ${clipped}`);
39
69
  }
40
70
  return `Context Summary\n${lines.join('\n')}`.trim();
@@ -1,5 +1,26 @@
1
+ import os from 'node:os';
2
+ import fs from 'node:fs';
1
3
  import { getShellSystemPrompt } from './shell-profile.js';
2
4
 
5
+ function getEnvBlock() {
6
+ const cwd = process.cwd();
7
+ let isGitRepo = false;
8
+ try {
9
+ fs.accessSync(`${cwd}/.git`);
10
+ isGitRepo = true;
11
+ } catch {}
12
+
13
+ return `<env>
14
+ Working directory: ${cwd}
15
+ Is directory a git repo: ${isGitRepo ? 'Yes' : 'No'}
16
+ Platform: ${process.platform}
17
+ Shell: ${os.userInfo().shell || 'unknown'}
18
+ OS Version: ${os.version || os.release()}
19
+ </env>`;
20
+ }
21
+
3
22
  export function buildDefaultSystemPrompt(config = {}) {
4
- return `${getShellSystemPrompt(config?.shell?.default)} If a command or tool is blocked or fails, inspect the error and retry with allowed commands or tools. For AST-scoped edits, if edit rejects a call because kind=replace_block or ast_target is missing or stale, fix the tool arguments and retry instead of switching to a broader text edit. Do not claim filesystem access is impossible unless the allowed search/read tools also fail.`;
23
+ return `${getShellSystemPrompt(config?.shell?.default)}
24
+
25
+ ${getEnvBlock()}`;
5
26
  }