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 +1 -1
- package/skills/superpowers-lite/SKILL.md +20 -6
- package/src/cli.js +1 -1
- package/src/commands/run.js +3 -1
- package/src/core/agent-loop.js +327 -68
- package/src/core/chat-runtime.js +336 -104
- package/src/core/context-compact.js +32 -2
- package/src/core/default-system-prompt.js +22 -1
- package/src/core/session-store.js +19 -0
- package/src/core/shell-profile.js +47 -1
- package/src/core/tools.js +323 -82
package/package.json
CHANGED
|
@@ -18,20 +18,34 @@ Routing:
|
|
|
18
18
|
- execute directly
|
|
19
19
|
- do not force brainstorming
|
|
20
20
|
|
|
21
|
-
2. If the
|
|
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
|
-
-
|
|
24
|
-
- do not
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
7
|
+
const VERSION = '0.2.4';
|
|
8
8
|
|
|
9
9
|
function printHelp() {
|
|
10
10
|
console.log(`codemini ${VERSION}
|
package/src/commands/run.js
CHANGED
|
@@ -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({
|
package/src/core/agent-loop.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
526
|
+
return {
|
|
527
|
+
callId: call.id,
|
|
528
|
+
content: clipToolResult({ error: message }, toolResultMaxChars),
|
|
529
|
+
error: true
|
|
310
530
|
};
|
|
311
|
-
|
|
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
|
-
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
name: displayName,
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
+
}
|