micro-models-agent 0.2.39 → 0.2.41
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/dist/agent/loop.d.ts +2 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +270 -238
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/system-prompt.d.ts +2 -1
- package/dist/agent/system-prompt.d.ts.map +1 -1
- package/dist/agent/system-prompt.js +87 -21
- package/dist/agent/system-prompt.js.map +1 -1
- package/dist/async-map.d.ts +6 -0
- package/dist/async-map.d.ts.map +1 -0
- package/dist/async-map.js +16 -0
- package/dist/async-map.js.map +1 -0
- package/dist/config/config.js +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/examples.d.ts +22 -0
- package/dist/examples.d.ts.map +1 -0
- package/dist/examples.js +45 -0
- package/dist/examples.js.map +1 -0
- package/dist/fibonacci.d.ts +19 -0
- package/dist/fibonacci.d.ts.map +1 -0
- package/dist/fibonacci.js +61 -0
- package/dist/fibonacci.js.map +1 -0
- package/dist/llm/openai-compat.d.ts +1 -1
- package/dist/llm/openai-compat.d.ts.map +1 -1
- package/dist/llm/openai-compat.js.map +1 -1
- package/dist/logger.d.ts +2 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +7 -5
- package/dist/logger.js.map +1 -1
- package/dist/repl/commands.d.ts.map +1 -1
- package/dist/repl/commands.js +2 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/history.d.ts +2 -0
- package/dist/repl/history.d.ts.map +1 -1
- package/dist/repl/history.js.map +1 -1
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +46 -11
- package/dist/repl/index.js.map +1 -1
- package/dist/skills/builtin/agent-test/SKILL.md +68 -0
- package/dist/skills/builtin/grill-me/SKILL.md +46 -0
- package/dist/sort.d.ts +8 -0
- package/dist/sort.d.ts.map +1 -0
- package/dist/sort.js +36 -0
- package/dist/sort.js.map +1 -0
- package/dist/stack.d.ts +9 -0
- package/dist/stack.d.ts.map +1 -0
- package/dist/stack.js +22 -0
- package/dist/stack.js.map +1 -0
- package/dist/test-run.d.ts +2 -0
- package/dist/test-run.d.ts.map +1 -0
- package/dist/test-run.js +13 -0
- package/dist/test-run.js.map +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +9 -7
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/fs/grep-search.js +1 -1
- package/dist/tools/fs/grep-search.js.map +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +19 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/subagent.d.ts +25 -0
- package/dist/tools/subagent.d.ts.map +1 -0
- package/dist/tools/subagent.js +412 -0
- package/dist/tools/subagent.js.map +1 -0
- package/dist/tools/syntax-check.d.ts.map +1 -1
- package/dist/tools/syntax-check.js +7 -2
- package/dist/tools/syntax-check.js.map +1 -1
- package/dist/ui/ink/components/App.d.ts +13 -1
- package/dist/ui/ink/components/App.d.ts.map +1 -1
- package/dist/ui/ink/components/App.js +50 -4
- package/dist/ui/ink/components/App.js.map +1 -1
- package/dist/ui/ink/components/PromptInput.d.ts +3 -1
- package/dist/ui/ink/components/PromptInput.d.ts.map +1 -1
- package/dist/ui/ink/components/PromptInput.js +39 -2
- package/dist/ui/ink/components/PromptInput.js.map +1 -1
- package/dist/ui/ink/components/SelectInput.d.ts +9 -0
- package/dist/ui/ink/components/SelectInput.d.ts.map +1 -0
- package/dist/ui/ink/components/SelectInput.js +30 -0
- package/dist/ui/ink/components/SelectInput.js.map +1 -0
- package/dist/ui/ink/components/ToolCallBlock.d.ts.map +1 -1
- package/dist/ui/ink/components/ToolCallBlock.js +10 -1
- package/dist/ui/ink/components/ToolCallBlock.js.map +1 -1
- package/dist/ui/ink/renderer.d.ts +7 -2
- package/dist/ui/ink/renderer.d.ts.map +1 -1
- package/dist/ui/ink/renderer.js +25 -3
- package/dist/ui/ink/renderer.js.map +1 -1
- package/dist/ui/readline-renderer.d.ts +2 -1
- package/dist/ui/readline-renderer.d.ts.map +1 -1
- package/dist/ui/readline-renderer.js +7 -1
- package/dist/ui/readline-renderer.js.map +1 -1
- package/dist/ui/renderer.d.ts +5 -1
- package/dist/ui/renderer.d.ts.map +1 -1
- package/dist/utils/memoize.d.ts +14 -0
- package/dist/utils/memoize.d.ts.map +1 -0
- package/dist/utils/memoize.js +51 -0
- package/dist/utils/memoize.js.map +1 -0
- package/dist/utils/types.d.ts +6 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +2 -0
- package/dist/utils/types.js.map +1 -0
- package/dist/utils.d.ts +22 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +81 -0
- package/dist/utils.js.map +1 -0
- package/package.json +3 -3
package/dist/agent/loop.js
CHANGED
|
@@ -17,65 +17,9 @@ import { listThemes, switchTheme } from '../styling/index.js';
|
|
|
17
17
|
import { loadThemeConfig, installTheme, removeTheme } from '../styling/loader.js';
|
|
18
18
|
import { loadConfig, saveConfig } from '../config/config.js';
|
|
19
19
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
20
|
-
import { logInfo, logWarn,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let text = response;
|
|
24
|
-
while (true) {
|
|
25
|
-
const tcStart = text.indexOf('<tool_call>');
|
|
26
|
-
if (tcStart === -1)
|
|
27
|
-
break;
|
|
28
|
-
const tcEnd = text.indexOf('</tool_call>', tcStart);
|
|
29
|
-
let block;
|
|
30
|
-
if (tcEnd !== -1) {
|
|
31
|
-
block = text.slice(tcStart + '<tool_call>'.length, tcEnd);
|
|
32
|
-
text = text.slice(tcEnd + '</tool_call>'.length);
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
block = text.slice(tcStart + '<tool_call>'.length);
|
|
36
|
-
text = '';
|
|
37
|
-
}
|
|
38
|
-
// Strip markdown code fences
|
|
39
|
-
block = block.replace(/```(?:json)?\s*/g, '').replace(/```\s*/g, '');
|
|
40
|
-
// Find JSON object
|
|
41
|
-
const jsonStart = block.indexOf('{');
|
|
42
|
-
if (jsonStart === -1)
|
|
43
|
-
continue;
|
|
44
|
-
let depth = 0;
|
|
45
|
-
let inStr = false;
|
|
46
|
-
let esc = false;
|
|
47
|
-
for (let i = jsonStart; i < block.length; i++) {
|
|
48
|
-
const ch = block[i];
|
|
49
|
-
if (esc) {
|
|
50
|
-
esc = false;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (ch === '\\' && inStr) {
|
|
54
|
-
esc = true;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (ch === '"') {
|
|
58
|
-
inStr = !inStr;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (inStr)
|
|
62
|
-
continue;
|
|
63
|
-
if (ch === '{')
|
|
64
|
-
depth++;
|
|
65
|
-
if (ch === '}')
|
|
66
|
-
depth--;
|
|
67
|
-
if (depth === 0) {
|
|
68
|
-
results.push(block.slice(jsonStart, i + 1));
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// If no closing brace, take everything
|
|
73
|
-
if (depth > 0 && jsonStart < block.length) {
|
|
74
|
-
results.push(block.slice(jsonStart));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return results;
|
|
78
|
-
}
|
|
20
|
+
import { logInfo, logWarn, logError, logDebug, logToolCall, setStatusWriter, writeStatus } from '../logger.js';
|
|
21
|
+
import { buildSubagentContext, runIsolatedLoop, extractAllToolCalls, validateToolCall, doChat } from '../tools/subagent.js';
|
|
22
|
+
// extractAllToolCalls moved to src/tools/subagent.ts
|
|
79
23
|
function extractToolCallFallback(response) {
|
|
80
24
|
const patterns = [
|
|
81
25
|
/"name"\s*:\s*"bash"\s*,\s*"arguments"\s*:\s*\{\s*"cmd"\s*:\s*"((?:[^"\\]|\\.)*)"/,
|
|
@@ -92,146 +36,9 @@ function extractToolCallFallback(response) {
|
|
|
92
36
|
}
|
|
93
37
|
return null;
|
|
94
38
|
}
|
|
95
|
-
function validateToolCall(parsed) {
|
|
96
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
97
|
-
return { valid: false, error: 'Not an object' };
|
|
98
|
-
}
|
|
99
|
-
const VALID_TOOLS = new Set(['bash', 'plan', 'todo', 'question', 'approve', 'theme', 'user_profile', 'read_file', 'write_file', 'edit_file', 'list_dir', 'create_dir', 'delete_file', 'copy_file', 'move_file', 'file_info', 'glob_search', 'grep_search', 'tree', 'web_search', 'web_fetch']);
|
|
100
|
-
if (!parsed.name || !VALID_TOOLS.has(parsed.name)) {
|
|
101
|
-
return { valid: false, error: `Unknown tool: ${parsed.name}` };
|
|
102
|
-
}
|
|
103
|
-
if (!parsed.arguments || typeof parsed.arguments !== 'object') {
|
|
104
|
-
return { valid: false, error: 'Missing arguments' };
|
|
105
|
-
}
|
|
106
|
-
if (parsed.name === 'bash') {
|
|
107
|
-
const cmd = String(parsed.arguments.cmd ?? '').trim();
|
|
108
|
-
if (!cmd) {
|
|
109
|
-
return { valid: false, error: 'Empty command' };
|
|
110
|
-
}
|
|
111
|
-
return { valid: true, name: parsed.name, cmd };
|
|
112
|
-
}
|
|
113
|
-
const REQUIRED_ARGS = {
|
|
114
|
-
read_file: ['path'],
|
|
115
|
-
write_file: ['path', 'content'],
|
|
116
|
-
edit_file: ['path', 'old_string', 'new_string'],
|
|
117
|
-
list_dir: [],
|
|
118
|
-
create_dir: ['path'],
|
|
119
|
-
delete_file: ['path'],
|
|
120
|
-
copy_file: ['source', 'destination'],
|
|
121
|
-
move_file: ['source', 'destination'],
|
|
122
|
-
file_info: ['path'],
|
|
123
|
-
glob_search: ['pattern'],
|
|
124
|
-
grep_search: ['pattern'],
|
|
125
|
-
tree: [],
|
|
126
|
-
web_search: ['query'],
|
|
127
|
-
web_fetch: ['url'],
|
|
128
|
-
user_profile: [],
|
|
129
|
-
};
|
|
130
|
-
const required = REQUIRED_ARGS[parsed.name] ?? [];
|
|
131
|
-
const missing = required.filter(k => {
|
|
132
|
-
const v = parsed.arguments[k];
|
|
133
|
-
return v === undefined || v === null || v === '';
|
|
134
|
-
});
|
|
135
|
-
if (missing.length > 0) {
|
|
136
|
-
return { valid: false, error: `Missing required argument(s): ${missing.join(', ')}. Tool "${parsed.name}" requires: ${required.join(', ')}` };
|
|
137
|
-
}
|
|
138
|
-
return { valid: true, name: parsed.name };
|
|
139
|
-
}
|
|
140
|
-
async function doChat(provider, messages, renderer, signal, retries = 1, showThinking = false) {
|
|
141
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
142
|
-
const startTime = performance.now();
|
|
143
|
-
const fullText = [];
|
|
144
|
-
let started = false;
|
|
145
|
-
let reasoningBuf = false;
|
|
146
|
-
if (signal?.aborted)
|
|
147
|
-
return { response: '', genTimeMs: 0 };
|
|
148
|
-
const lastMsg = messages[messages.length - 1];
|
|
149
|
-
logLLMRequest(provider.model, messages.length, lastMsg?.content || '');
|
|
150
|
-
renderer.writeSpinner();
|
|
151
|
-
try {
|
|
152
|
-
let buf = '';
|
|
153
|
-
for await (const chunk of provider.chat(messages, { temperature: 0.7, max_tokens: 8192, signal, showThinking })) {
|
|
154
|
-
if (!chunk.content && !chunk.reasoning)
|
|
155
|
-
continue;
|
|
156
|
-
if (chunk.content)
|
|
157
|
-
fullText.push(chunk.content);
|
|
158
|
-
if (!started) {
|
|
159
|
-
renderer.clearSpinner();
|
|
160
|
-
started = true;
|
|
161
|
-
}
|
|
162
|
-
if (chunk.reasoning) {
|
|
163
|
-
if (showThinking) {
|
|
164
|
-
if (reasoningBuf === false) {
|
|
165
|
-
renderer.writeNewline();
|
|
166
|
-
renderer.writeDim('┈ reasoning ┈');
|
|
167
|
-
reasoningBuf = '';
|
|
168
|
-
}
|
|
169
|
-
reasoningBuf += chunk.reasoning;
|
|
170
|
-
}
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (reasoningBuf !== false) {
|
|
174
|
-
renderer.writeNewline();
|
|
175
|
-
renderer.writeDim(reasoningBuf);
|
|
176
|
-
reasoningBuf = false;
|
|
177
|
-
}
|
|
178
|
-
buf += chunk.content;
|
|
179
|
-
while (buf.length > 0) {
|
|
180
|
-
const tcStart = buf.indexOf('<tool_call>');
|
|
181
|
-
const tcEnd = buf.indexOf('</tool_call>');
|
|
182
|
-
if (tcStart === -1 && tcEnd === -1) {
|
|
183
|
-
if (buf)
|
|
184
|
-
renderer.writeMarkdownChunk(buf);
|
|
185
|
-
buf = '';
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
if (tcStart !== -1 && (tcEnd === -1 || tcStart < tcEnd)) {
|
|
189
|
-
if (tcStart > 0) {
|
|
190
|
-
renderer.writeMarkdownChunk(buf.slice(0, tcStart));
|
|
191
|
-
}
|
|
192
|
-
buf = buf.slice(tcStart);
|
|
193
|
-
const closeIdx = buf.indexOf('</tool_call>');
|
|
194
|
-
if (closeIdx !== -1) {
|
|
195
|
-
buf = buf.slice(closeIdx + '</tool_call>'.length);
|
|
196
|
-
}
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
if (tcEnd !== -1 && (tcStart === -1 || tcEnd < tcStart)) {
|
|
200
|
-
renderer.writeMarkdownChunk(buf.slice(0, tcEnd));
|
|
201
|
-
buf = buf.slice(tcEnd + '</tool_call>'.length);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (reasoningBuf !== false) {
|
|
206
|
-
renderer.writeNewline();
|
|
207
|
-
renderer.writeDim(reasoningBuf);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch (e) {
|
|
211
|
-
if (e.name === 'AbortError') {
|
|
212
|
-
logLLMResponse(provider.model, 0, performance.now() - startTime, 'Aborted');
|
|
213
|
-
return { response: '', genTimeMs: 0 };
|
|
214
|
-
}
|
|
215
|
-
logLLMResponse(provider.model, 0, performance.now() - startTime, e.message);
|
|
216
|
-
renderer.writeError(`\n[LLM] ${e.message}`);
|
|
217
|
-
renderer.writeWarning('[LLM] Check that your server is running (LM Studio on localhost:1234 or Ollama)');
|
|
218
|
-
return { response: '', genTimeMs: 0 };
|
|
219
|
-
}
|
|
220
|
-
const genTimeMs = performance.now() - startTime;
|
|
221
|
-
const response = fullText.join('');
|
|
222
|
-
renderer.commitMarkdown();
|
|
223
|
-
if (response.trim()) {
|
|
224
|
-
logLLMResponse(provider.model, response.length, genTimeMs);
|
|
225
|
-
return { response, genTimeMs };
|
|
226
|
-
}
|
|
227
|
-
if (attempt < retries) {
|
|
228
|
-
renderer.writeDim(`[retry ${attempt + 1}/${retries}]`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return { response: '', genTimeMs: 0 };
|
|
232
|
-
}
|
|
233
39
|
export async function runAgentLoop(provider, registry, prompt, contextManager, previousMessages, renderer, providerName, options) {
|
|
234
40
|
logInfo('LOOP', `runAgentLoop started: "${prompt.slice(0, 50)}"`);
|
|
41
|
+
renderer?.resetAbort?.();
|
|
235
42
|
const { prompt: enrichedPrompt, files } = await parseFileRefs(prompt);
|
|
236
43
|
logDebug('LOOP', `parseFileRefs: ${files.length} files, prompt len: ${enrichedPrompt.length}`);
|
|
237
44
|
const quiet = options?.quiet ?? false;
|
|
@@ -241,6 +48,19 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
241
48
|
logInfo('LOOP', `loaded ${loadedSkills.length} skills: ${loadedSkills.map(s => s.name).join(', ')}`);
|
|
242
49
|
renderer?.writeInfo(` Loaded ${loadedSkills.length} skill(s): ${loadedSkills.map(s => s.name).join(', ')}`);
|
|
243
50
|
}
|
|
51
|
+
if (options?.mode === 'pro' && !loadedSkillNames.has('grill-me')) {
|
|
52
|
+
const grill = loadSkill('grill-me', process.cwd());
|
|
53
|
+
if (grill) {
|
|
54
|
+
loadedSkills.push(grill);
|
|
55
|
+
loadedSkillNames.add('grill-me');
|
|
56
|
+
logInfo('LOOP', 'auto-loaded grill-me skill for pro mode');
|
|
57
|
+
renderer?.writeInfo(' Auto-loaded @grill-me skill for pro mode');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
logWarn('LOOP', 'grill-me skill not found for pro mode');
|
|
61
|
+
renderer?.writeWarning(' Warning: @grill-me skill not found (required for pro mode)');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
244
64
|
if (files.length > 0) {
|
|
245
65
|
const totalTokens = files.reduce((sum, f) => sum + f.tokens, 0);
|
|
246
66
|
const totalLines = files.reduce((sum, f) => sum + f.content.split('\n').length, 0);
|
|
@@ -251,7 +71,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
251
71
|
setStatusWriter(renderer ? (msg) => renderer.writeDim(` ${msg}`) : null);
|
|
252
72
|
writeStatus('Initializing...');
|
|
253
73
|
const sysStart = performance.now();
|
|
254
|
-
const systemPrompt = buildSystemPrompt(loadedSkills.length > 0 ? loadedSkills : undefined);
|
|
74
|
+
const systemPrompt = buildSystemPrompt(loadedSkills.length > 0 ? loadedSkills : undefined, options?.mode);
|
|
255
75
|
const sysElapsed = ((performance.now() - sysStart) / 1000).toFixed(1);
|
|
256
76
|
writeStatus(`Done (${sysElapsed}s)`);
|
|
257
77
|
setStatusWriter(null);
|
|
@@ -267,6 +87,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
267
87
|
let pendingSyntaxError = '';
|
|
268
88
|
let aborted = false;
|
|
269
89
|
let continuationNudges = 0;
|
|
90
|
+
let directWriteStreak = 0; // consecutive write_file calls without subagent (across steps)
|
|
270
91
|
// Reset abort signal for this run
|
|
271
92
|
if (renderer?.abortSignal === undefined) {
|
|
272
93
|
// Renderer doesn't support abort — fine
|
|
@@ -284,6 +105,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
284
105
|
break;
|
|
285
106
|
}
|
|
286
107
|
let skillsAutoLoaded = false;
|
|
108
|
+
// Track files created/modified in this session for completion reporting
|
|
109
|
+
const createdFiles = [];
|
|
287
110
|
logDebug('LOOP', `step ${step + 1}/${maxSteps} starting, ${messages.length} messages`);
|
|
288
111
|
await ctx.updateTokens(messages);
|
|
289
112
|
const skillNames = loadedSkills.map(s => s.name).join(', ');
|
|
@@ -307,41 +130,45 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
307
130
|
}
|
|
308
131
|
logInfo('LOOP', `calling doChat, messages: ${messages.length}, options.showThinking: ${options?.showThinking}`);
|
|
309
132
|
const { response } = await doChat(provider, messages, renderer, renderer?.abortSignal, 1, options?.showThinking);
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
133
|
+
// Check if aborted during generation
|
|
134
|
+
if (renderer?.abortSignal?.aborted) {
|
|
135
|
+
aborted = true;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
// Auto-load skills if the response contains @skill mentions
|
|
139
|
+
// Works with or without tool calls — agent can mention skills while working
|
|
140
|
+
if (options?.mode !== 'fast' && !skillsAutoLoaded) {
|
|
141
|
+
const suggestedSkillRefs = response.matchAll(/@(\w[\w-]*)/g);
|
|
142
|
+
const newSkillNames = [];
|
|
143
|
+
for (const match of suggestedSkillRefs) {
|
|
144
|
+
const name = match[1];
|
|
145
|
+
if (['email', 'api', 'id', 'json', 'url', 'http', 'https', 'localhost'].includes(name))
|
|
146
|
+
continue;
|
|
147
|
+
if (!loadedSkillNames.has(name)) {
|
|
148
|
+
const skill = loadSkill(name, process.cwd());
|
|
149
|
+
if (skill) {
|
|
150
|
+
newSkillNames.push(name);
|
|
151
|
+
loadedSkills.push(skill);
|
|
152
|
+
loadedSkillNames.add(name);
|
|
153
|
+
}
|
|
323
154
|
}
|
|
324
155
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
156
|
+
if (newSkillNames.length > 0) {
|
|
157
|
+
skillsAutoLoaded = true;
|
|
158
|
+
renderer?.writeInfo(` Auto-loaded ${newSkillNames.length} skill(s): ${newSkillNames.join(', ')}`);
|
|
159
|
+
messages[0] = { role: 'system', content: buildSystemPrompt(loadedSkills, options?.mode) };
|
|
160
|
+
messages.push({ role: 'user', content: `[System: Skills loaded: ${newSkillNames.join(', ')}. Continue your work using these skills where relevant.]` });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
} // end fast-mode skill block
|
|
334
164
|
if (!response.trim()) {
|
|
335
165
|
logWarn('LOOP', `empty response #${consecutiveEmpty + 1}`);
|
|
336
166
|
consecutiveEmpty++;
|
|
337
167
|
const lastMsg = messages[messages.length - 1];
|
|
338
|
-
if (lastMsg?.role === '
|
|
339
|
-
|
|
340
|
-
? 'The tool result is above. Your turn now. Either: (1) call the next tool using <tool_call>{"name":"...","arguments":{...}}</tool_call>, or (2) write a text reply to the user summarizing progress. Do NOT output nothing.'
|
|
341
|
-
: 'You returned an empty response again. You MUST write something now. Example of what to write: "File created. Now I will create the next file..." or call another tool. Silence is not allowed.';
|
|
342
|
-
messages.push({ role: 'user', content: retryPrompt });
|
|
168
|
+
if (lastMsg?.role === 'user' && consecutiveEmpty <= 2) {
|
|
169
|
+
messages.push({ role: 'user', content: 'Continue.' });
|
|
343
170
|
if (!quiet) {
|
|
344
|
-
renderer?.writeDim(` [empty response
|
|
171
|
+
renderer?.writeDim(` [empty response, retrying]`);
|
|
345
172
|
}
|
|
346
173
|
continue;
|
|
347
174
|
}
|
|
@@ -387,31 +214,133 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
387
214
|
}
|
|
388
215
|
if (validCalls.length === 0) {
|
|
389
216
|
if (invalidCalls.length > 0) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
217
|
+
// If the response has substantial text outside tool_call tags,
|
|
218
|
+
// it's a description with examples — not a failed tool call attempt.
|
|
219
|
+
const textOutside = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim();
|
|
220
|
+
if (textOutside.length < 100) {
|
|
221
|
+
const feedback = `[Tool call error(s)]\n${invalidCalls.join('\n')}\n\nFix the tool call format and try again. Use ONLY valid tool names: bash, read_file, write_file, edit_file, list_dir, create_dir, delete_file, copy_file, move_file, file_info, glob_search, grep_search, tree, web_search, web_fetch, plan, todo, question, approve, subagent.`;
|
|
222
|
+
if (!quiet) {
|
|
223
|
+
renderer?.writeError(` [invalid tool call: ${invalidCalls[0]}]`);
|
|
224
|
+
}
|
|
225
|
+
messages.push({ role: 'tool', content: feedback, tool_call_id: `invalid-${Date.now()}` });
|
|
226
|
+
continue;
|
|
393
227
|
}
|
|
394
|
-
|
|
395
|
-
|
|
228
|
+
// Text response with example tool calls — just log and proceed
|
|
229
|
+
logWarn('LOOP', `ignored ${invalidCalls.length} invalid tool call(s) in descriptive text`);
|
|
396
230
|
}
|
|
397
|
-
// Nudge model to continue working after a tool result
|
|
231
|
+
// Nudge model to continue working after a tool result — but only if
|
|
232
|
+
// the agent didn't already write a meaningful text response.
|
|
398
233
|
if (continuationNudges < 2) {
|
|
399
234
|
const lastThree = messages.slice(-3);
|
|
400
235
|
const hasRecentToolResult = lastThree.some(m => m.role === 'tool');
|
|
401
|
-
|
|
236
|
+
const alreadyResponded = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim().length > 20;
|
|
237
|
+
if (hasRecentToolResult && !alreadyResponded) {
|
|
402
238
|
continuationNudges++;
|
|
403
239
|
if (!quiet) {
|
|
404
240
|
renderer?.writeDim(` [nudge ${continuationNudges}/2: continue working]`);
|
|
405
241
|
}
|
|
406
|
-
messages.push({ role: 'user', content: '[Tool result received.
|
|
242
|
+
messages.push({ role: 'user', content: '[Tool result received. Write a response to the user. Do NOT call additional tools unless there is actual work to do.]' });
|
|
407
243
|
continue;
|
|
408
244
|
}
|
|
409
245
|
}
|
|
410
246
|
if (!skillsAutoLoaded) {
|
|
247
|
+
// If model claims done but has created files, remind about completeness
|
|
248
|
+
if (createdFiles.length > 0) {
|
|
249
|
+
const textOutside = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim();
|
|
250
|
+
const looksLikeDone = /done|готово|создан|выполн|completed|finished/i.test(textOutside);
|
|
251
|
+
if (looksLikeDone) {
|
|
252
|
+
messages.push({
|
|
253
|
+
role: 'user',
|
|
254
|
+
content: `Files created: ${createdFiles.join(', ')}. Before finishing — verify all files are complete and connected to each other. If any file is missing or not imported, continue working.`,
|
|
255
|
+
});
|
|
256
|
+
continuationNudges = 0;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
411
260
|
return { messages };
|
|
412
261
|
}
|
|
413
262
|
continue;
|
|
414
263
|
}
|
|
264
|
+
// Track consecutive read_file calls to the same file for consolidation
|
|
265
|
+
let lastReadFile = null;
|
|
266
|
+
// ENFORCEMENT: if plan has 3+ steps, block write_file and force subagent
|
|
267
|
+
const planSteps = showPlan();
|
|
268
|
+
const hasMultiStepPlan = !planSteps.startsWith('No active plan') && (planSteps.match(/\n/g) || []).length >= 3;
|
|
269
|
+
if (hasMultiStepPlan) {
|
|
270
|
+
const directWrites = validCalls.filter(c => c.parsed.name === 'write_file' || c.parsed.name === 'edit_file');
|
|
271
|
+
const hasSubagent = validCalls.some(c => c.parsed.name === 'subagent');
|
|
272
|
+
if (directWrites.length > 0 && !hasSubagent) {
|
|
273
|
+
// Block direct writes — model must use subagent
|
|
274
|
+
for (const wc of directWrites) {
|
|
275
|
+
const idx = validCalls.indexOf(wc);
|
|
276
|
+
if (idx !== -1)
|
|
277
|
+
validCalls.splice(idx, 1);
|
|
278
|
+
const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
|
|
279
|
+
messages.push({
|
|
280
|
+
role: 'tool',
|
|
281
|
+
content: `BLOCKED: You have a plan with multiple steps. Use subagent tool instead of write_file. Example: subagent("Create file X. Context: Y already exists.") Each subagent gets fresh context and works independently.`,
|
|
282
|
+
tool_call_id: blockId,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (!quiet) {
|
|
286
|
+
renderer?.writeWarning(` [blocked ${directWrites.length} direct writes — use subagent for multi-step plans]`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// ESCALATION: block write_file when creating 3+ files without subagent (no plan case)
|
|
291
|
+
if (!hasMultiStepPlan) {
|
|
292
|
+
const hasWrite = validCalls.some(c => c.parsed.name === 'write_file');
|
|
293
|
+
const hasSubagentNow = validCalls.some(c => c.parsed.name === 'subagent');
|
|
294
|
+
if (hasSubagentNow) {
|
|
295
|
+
directWriteStreak = 0;
|
|
296
|
+
}
|
|
297
|
+
else if (hasWrite) {
|
|
298
|
+
const writeCount = validCalls.filter(c => c.parsed.name === 'write_file').length;
|
|
299
|
+
directWriteStreak += writeCount;
|
|
300
|
+
if (directWriteStreak >= 3) {
|
|
301
|
+
const directWrites = validCalls.filter(c => c.parsed.name === 'write_file');
|
|
302
|
+
for (const wc of directWrites) {
|
|
303
|
+
const idx = validCalls.indexOf(wc);
|
|
304
|
+
if (idx !== -1)
|
|
305
|
+
validCalls.splice(idx, 1);
|
|
306
|
+
const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
|
|
307
|
+
messages.push({
|
|
308
|
+
role: 'tool',
|
|
309
|
+
content: `BLOCKED: You're creating file #${directWriteStreak} without subagent. For 3+ new files, use subagent for each one to maintain fresh context. Example: subagent("Create file X. Context: Y already exists.")`,
|
|
310
|
+
tool_call_id: blockId,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (!quiet) {
|
|
314
|
+
renderer?.writeWarning(` [blocked ${directWrites.length} writes: ${directWriteStreak} files without subagent — use subagent for multi-file projects]`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// Non-write tool calls don't reset the counter but also don't increment
|
|
320
|
+
const hasNonWriteTool = validCalls.length > 0 && !hasWrite;
|
|
321
|
+
if (hasNonWriteTool && directWriteStreak > 0) {
|
|
322
|
+
// New file creations should not be penalized across different operation types
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ENFORCEMENT: one file write at a time — block extra write_file/edit_file calls
|
|
327
|
+
const writeCalls = validCalls.filter(c => c.parsed.name === 'write_file' || c.parsed.name === 'edit_file');
|
|
328
|
+
if (writeCalls.length > 1) {
|
|
329
|
+
// Keep only the first write call, block the rest
|
|
330
|
+
const blocked = writeCalls.slice(1);
|
|
331
|
+
validCalls.splice(validCalls.indexOf(blocked[0]), blocked.length);
|
|
332
|
+
for (const bc of blocked) {
|
|
333
|
+
const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
|
|
334
|
+
messages.push({
|
|
335
|
+
role: 'tool',
|
|
336
|
+
content: `BLOCKED: Only one file write per response is allowed. You wrote ${writeCalls.length} files at once. Complete the first file, then write the next one.`,
|
|
337
|
+
tool_call_id: blockId,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
if (!quiet) {
|
|
341
|
+
renderer?.writeWarning(` [blocked ${blocked.length} extra file writes — one at a time]`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
415
344
|
for (const call of validCalls) {
|
|
416
345
|
const { parsed, cmd } = call;
|
|
417
346
|
const isBash = parsed.name === 'bash';
|
|
@@ -435,8 +364,12 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
435
364
|
}
|
|
436
365
|
if (pendingSyntaxError && !isBash) {
|
|
437
366
|
const isWriteOp = parsed.name === 'write_file' || parsed.name === 'edit_file';
|
|
438
|
-
|
|
439
|
-
|
|
367
|
+
const newPath = String(parsed.arguments.path ?? '');
|
|
368
|
+
// Allow editing config files (tsconfig.json, package.json) since they
|
|
369
|
+
// can fix syntax errors (e.g. missing jsx flag in tsconfig)
|
|
370
|
+
const isConfig = newPath === 'tsconfig.json' || newPath === 'package.json';
|
|
371
|
+
if (isWriteOp && newPath !== pendingSyntaxError && !isConfig) {
|
|
372
|
+
const blocked = `BLOCKED: You have a syntax error in ${pendingSyntaxError}. Fix it first before writing new files. If the error is about jsx, module resolution, or missing types — edit tsconfig.json (it is exempt from this block).`;
|
|
440
373
|
if (!quiet) {
|
|
441
374
|
renderer?.writeError(` [blocked: fix syntax error in ${pendingSyntaxError} first]`);
|
|
442
375
|
}
|
|
@@ -456,6 +389,14 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
456
389
|
renderer?.writeBracketTag('plan', 'create');
|
|
457
390
|
}
|
|
458
391
|
toolOutput = createPlan(planName, steps);
|
|
392
|
+
// If plan has 3+ steps, suggest using subagent for isolation
|
|
393
|
+
if (steps.length >= 3) {
|
|
394
|
+
toolOutput += `\n\nIMPORTANT: Plan has ${steps.length} steps. Use subagent tool for EACH step to work in isolation:\n`;
|
|
395
|
+
for (let i = 0; i < steps.length; i++) {
|
|
396
|
+
toolOutput += `subagent("Step ${i + 1}: ${steps[i]}. List existing files and packages.")\n`;
|
|
397
|
+
}
|
|
398
|
+
toolOutput += `This prevents context loss. Do NOT use write_file directly — delegate to subagents.`;
|
|
399
|
+
}
|
|
459
400
|
if (!quiet && toolOutput) {
|
|
460
401
|
renderer?.writeNewline();
|
|
461
402
|
renderer?.writeLine(toolOutput);
|
|
@@ -624,6 +565,38 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
624
565
|
toolExitCode = 1;
|
|
625
566
|
}
|
|
626
567
|
}
|
|
568
|
+
else if (parsed.name === 'subagent') {
|
|
569
|
+
// Subagent: isolated context for a single task
|
|
570
|
+
const task = String(parsed.arguments.task || '');
|
|
571
|
+
if (!task) {
|
|
572
|
+
toolOutput = 'Error: task is required';
|
|
573
|
+
toolExitCode = 1;
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
const autoContext = buildSubagentContext(process.cwd());
|
|
577
|
+
logInfo('AGENT', `spawning subagent | task: "${task.slice(0, 150)}"`);
|
|
578
|
+
try {
|
|
579
|
+
const childResult = await runIsolatedLoop(provider, registry, task, autoContext, process.cwd(), quiet, renderer);
|
|
580
|
+
toolOutput = childResult.summary;
|
|
581
|
+
logInfo('AGENT', `subagent returned | files: ${childResult.createdFiles.length} | turns: ${childResult.turns} | errors: ${childResult.errors.length}`);
|
|
582
|
+
if (childResult.createdFiles.length > 0) {
|
|
583
|
+
for (const f of childResult.createdFiles) {
|
|
584
|
+
if (!createdFiles.includes(f))
|
|
585
|
+
createdFiles.push(f);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch (e) {
|
|
590
|
+
toolOutput = `Subagent error: ${e.message}`;
|
|
591
|
+
toolExitCode = 1;
|
|
592
|
+
logError('AGENT', `subagent error: ${e.message}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const subTime = ((performance.now() - toolStart) / 1000).toFixed(1);
|
|
596
|
+
if (!quiet) {
|
|
597
|
+
renderer?.writeToolResult(subTime, toolExitCode, toolOutput.slice(0, 300));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
627
600
|
else if (isBash && cmd.startsWith('plan ')) {
|
|
628
601
|
const planArgs = cmd.slice(5).trim();
|
|
629
602
|
const { action, params } = parsePlanCommand(planArgs);
|
|
@@ -744,6 +717,40 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
744
717
|
autoLearnFromError(hookCmd, toolExitCode !== 0 ? toolOutput : '', '');
|
|
745
718
|
}
|
|
746
719
|
runPostToolUseHooks(process.cwd(), parsed.name, hookCmd, toolOutput, toolExitCode);
|
|
720
|
+
// Track sequential read_file calls for consolidation
|
|
721
|
+
if (!isBash && parsed.name === 'read_file' && toolExitCode === 0) {
|
|
722
|
+
const filePath = String(parsed.arguments.path ?? '');
|
|
723
|
+
const offset = Math.max(1, Number(parsed.arguments.offset ?? 1));
|
|
724
|
+
const limit = Math.max(1, Number(parsed.arguments.limit ?? 500));
|
|
725
|
+
if (lastReadFile && lastReadFile.path === filePath && offset === lastReadFile.offset + lastReadFile.limit) {
|
|
726
|
+
// Sequential read — merge with previous result
|
|
727
|
+
lastReadFile.content += '\n' + toolOutput;
|
|
728
|
+
lastReadFile.offset = offset;
|
|
729
|
+
lastReadFile.limit = limit;
|
|
730
|
+
// Replace the last tool message content with merged content
|
|
731
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
732
|
+
if (messages[i].role === 'tool' && messages[i].tool_call_id === lastReadFile.toolId) {
|
|
733
|
+
messages[i].content = lastReadFile.content;
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// Remove the trailing "Continue." nudge + the new tool result that would be pushed
|
|
738
|
+
while (messages.length > 0 && messages[messages.length - 1].role === 'user' && messages[messages.length - 1].content === 'Continue.') {
|
|
739
|
+
messages.pop();
|
|
740
|
+
}
|
|
741
|
+
if (!quiet) {
|
|
742
|
+
renderer?.writeBracketTag('consolidate', `merged read_file chunks for ${filePath}`);
|
|
743
|
+
}
|
|
744
|
+
continue; // skip pushing this result separately
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// New file or non-sequential — start fresh
|
|
748
|
+
lastReadFile = { path: filePath, offset, limit, content: toolOutput, toolId };
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
else if (!isBash && parsed.name !== 'read_file') {
|
|
752
|
+
lastReadFile = null;
|
|
753
|
+
}
|
|
747
754
|
// Syntax check after file write operations
|
|
748
755
|
let hasSyntaxError = false;
|
|
749
756
|
if (toolExitCode === 0) {
|
|
@@ -786,7 +793,19 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
786
793
|
}
|
|
787
794
|
}
|
|
788
795
|
catch { /* ignore read errors */ }
|
|
789
|
-
toolOutput = `SYNTAX ERROR in ${filePath}:\n${syntaxResult}${contextLines}
|
|
796
|
+
toolOutput = `SYNTAX ERROR in ${filePath}:\n${syntaxResult}${contextLines}`;
|
|
797
|
+
// Detect config-related TS errors (missing jsx, module issues, etc.)
|
|
798
|
+
const isConfigError = syntaxResult.includes('TS17004')
|
|
799
|
+
|| syntaxResult.includes('TS2307')
|
|
800
|
+
|| syntaxResult.includes('Cannot find module')
|
|
801
|
+
|| syntaxResult.includes('jsx')
|
|
802
|
+
|| syntaxResult.includes('moduleResolution');
|
|
803
|
+
if (isConfigError) {
|
|
804
|
+
toolOutput += `\n\nThis looks like a tsconfig.json configuration issue, NOT a code syntax error.\nCheck tsconfig.json — you may need to add "jsx": "react-jsx" or fix module settings.\nDo NOT modify the source code to work around config errors. Edit tsconfig.json instead.`;
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
toolOutput += `\n\nDo NOT rewrite the entire file. Use edit_file to fix the specific broken line:\n{"name":"edit_file","arguments":{"path":"${filePath}","old_string":"broken code here","new_string":"fixed code here"}}`;
|
|
808
|
+
}
|
|
790
809
|
hasSyntaxError = true;
|
|
791
810
|
pendingSyntaxError = filePath;
|
|
792
811
|
}
|
|
@@ -802,6 +821,10 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
802
821
|
collectCodingStyle(filePath, fullContent);
|
|
803
822
|
}
|
|
804
823
|
catch { /* ignore */ }
|
|
824
|
+
// Track created/modified files for completion reporting
|
|
825
|
+
if (!hasSyntaxError && !createdFiles.includes(filePath)) {
|
|
826
|
+
createdFiles.push(filePath);
|
|
827
|
+
}
|
|
805
828
|
}
|
|
806
829
|
}
|
|
807
830
|
}
|
|
@@ -822,9 +845,18 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
822
845
|
}
|
|
823
846
|
messages.push({
|
|
824
847
|
role: 'tool',
|
|
825
|
-
content: toolOutput.slice(0,
|
|
848
|
+
content: toolOutput.slice(0, 8000),
|
|
826
849
|
tool_call_id: toolId,
|
|
827
850
|
});
|
|
851
|
+
// Push a continuation nudge after every tool result
|
|
852
|
+
// Include created files list so model doesn't lose track
|
|
853
|
+
const nudge = createdFiles.length > 0
|
|
854
|
+
? `Continue. Created: ${createdFiles.join(', ')}`
|
|
855
|
+
: 'Continue.';
|
|
856
|
+
messages.push({
|
|
857
|
+
role: 'user',
|
|
858
|
+
content: nudge,
|
|
859
|
+
});
|
|
828
860
|
if (hasSyntaxError) {
|
|
829
861
|
break;
|
|
830
862
|
}
|