micro-models-agent 0.2.40 → 0.2.42
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.map +1 -1
- package/dist/agent/loop.js +230 -219
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/system-prompt.d.ts.map +1 -1
- package/dist/agent/system-prompt.js +62 -23
- 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/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -1
- package/dist/cli.js.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/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/skills/builtin/agent-test/SKILL.md +68 -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/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 +1 -1
- package/dist/ui/ink/renderer.d.ts.map +1 -1
- package/dist/ui/ink/renderer.js +2 -2
- package/dist/ui/ink/renderer.js.map +1 -1
- package/dist/ui/readline-renderer.d.ts +1 -1
- package/dist/ui/readline-renderer.d.ts.map +1 -1
- package/dist/ui/readline-renderer.js +4 -1
- package/dist/ui/readline-renderer.js.map +1 -1
- package/dist/ui/renderer.d.ts +1 -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.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAc9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAc9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA2BpD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAC5D;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,WAAW,EACrB,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,MAAM,EACd,cAAc,CAAC,EAAE,cAAc,EAC/B,gBAAgB,CAAC,EAAE,WAAW,EAAE,EAChC,QAAQ,CAAC,EAAE,cAAc,EACzB,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,GACtE,OAAO,CAAC,eAAe,CAAC,CAm0B1B"}
|
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,144 +36,6 @@ 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)}"`);
|
|
235
41
|
renderer?.resetAbort?.();
|
|
@@ -281,6 +87,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
281
87
|
let pendingSyntaxError = '';
|
|
282
88
|
let aborted = false;
|
|
283
89
|
let continuationNudges = 0;
|
|
90
|
+
let directWriteStreak = 0; // consecutive write_file calls without subagent (across steps)
|
|
284
91
|
// Reset abort signal for this run
|
|
285
92
|
if (renderer?.abortSignal === undefined) {
|
|
286
93
|
// Renderer doesn't support abort — fine
|
|
@@ -298,6 +105,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
298
105
|
break;
|
|
299
106
|
}
|
|
300
107
|
let skillsAutoLoaded = false;
|
|
108
|
+
// Track files created/modified in this session for completion reporting
|
|
109
|
+
const createdFiles = [];
|
|
301
110
|
logDebug('LOOP', `step ${step + 1}/${maxSteps} starting, ${messages.length} messages`);
|
|
302
111
|
await ctx.updateTokens(messages);
|
|
303
112
|
const skillNames = loadedSkills.map(s => s.name).join(', ');
|
|
@@ -326,8 +135,9 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
326
135
|
aborted = true;
|
|
327
136
|
break;
|
|
328
137
|
}
|
|
329
|
-
// Auto-load skills if
|
|
330
|
-
|
|
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) {
|
|
331
141
|
const suggestedSkillRefs = response.matchAll(/@(\w[\w-]*)/g);
|
|
332
142
|
const newSkillNames = [];
|
|
333
143
|
for (const match of suggestedSkillRefs) {
|
|
@@ -347,8 +157,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
347
157
|
skillsAutoLoaded = true;
|
|
348
158
|
renderer?.writeInfo(` Auto-loaded ${newSkillNames.length} skill(s): ${newSkillNames.join(', ')}`);
|
|
349
159
|
messages[0] = { role: 'system', content: buildSystemPrompt(loadedSkills, options?.mode) };
|
|
350
|
-
messages.push({ role: 'user', content: `[System: Skills loaded: ${newSkillNames.join(', ')}.
|
|
351
|
-
// Force model to continue working after skills are loaded
|
|
160
|
+
messages.push({ role: 'user', content: `[System: Skills loaded: ${newSkillNames.join(', ')}. Continue your work using these skills where relevant.]` });
|
|
352
161
|
continue;
|
|
353
162
|
}
|
|
354
163
|
} // end fast-mode skill block
|
|
@@ -356,13 +165,10 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
356
165
|
logWarn('LOOP', `empty response #${consecutiveEmpty + 1}`);
|
|
357
166
|
consecutiveEmpty++;
|
|
358
167
|
const lastMsg = messages[messages.length - 1];
|
|
359
|
-
if (lastMsg?.role === '
|
|
360
|
-
|
|
361
|
-
? '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.'
|
|
362
|
-
: '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.';
|
|
363
|
-
messages.push({ role: 'user', content: retryPrompt });
|
|
168
|
+
if (lastMsg?.role === 'user' && consecutiveEmpty <= 2) {
|
|
169
|
+
messages.push({ role: 'user', content: 'Continue.' });
|
|
364
170
|
if (!quiet) {
|
|
365
|
-
renderer?.writeDim(` [empty response
|
|
171
|
+
renderer?.writeDim(` [empty response, retrying]`);
|
|
366
172
|
}
|
|
367
173
|
continue;
|
|
368
174
|
}
|
|
@@ -408,31 +214,133 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
408
214
|
}
|
|
409
215
|
if (validCalls.length === 0) {
|
|
410
216
|
if (invalidCalls.length > 0) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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;
|
|
414
227
|
}
|
|
415
|
-
|
|
416
|
-
|
|
228
|
+
// Text response with example tool calls — just log and proceed
|
|
229
|
+
logWarn('LOOP', `ignored ${invalidCalls.length} invalid tool call(s) in descriptive text`);
|
|
417
230
|
}
|
|
418
|
-
// 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.
|
|
419
233
|
if (continuationNudges < 2) {
|
|
420
234
|
const lastThree = messages.slice(-3);
|
|
421
235
|
const hasRecentToolResult = lastThree.some(m => m.role === 'tool');
|
|
422
|
-
|
|
236
|
+
const alreadyResponded = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim().length > 20;
|
|
237
|
+
if (hasRecentToolResult && !alreadyResponded) {
|
|
423
238
|
continuationNudges++;
|
|
424
239
|
if (!quiet) {
|
|
425
240
|
renderer?.writeDim(` [nudge ${continuationNudges}/2: continue working]`);
|
|
426
241
|
}
|
|
427
|
-
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.]' });
|
|
428
243
|
continue;
|
|
429
244
|
}
|
|
430
245
|
}
|
|
431
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
|
+
}
|
|
432
260
|
return { messages };
|
|
433
261
|
}
|
|
434
262
|
continue;
|
|
435
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
|
+
}
|
|
436
344
|
for (const call of validCalls) {
|
|
437
345
|
const { parsed, cmd } = call;
|
|
438
346
|
const isBash = parsed.name === 'bash';
|
|
@@ -456,8 +364,12 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
456
364
|
}
|
|
457
365
|
if (pendingSyntaxError && !isBash) {
|
|
458
366
|
const isWriteOp = parsed.name === 'write_file' || parsed.name === 'edit_file';
|
|
459
|
-
|
|
460
|
-
|
|
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).`;
|
|
461
373
|
if (!quiet) {
|
|
462
374
|
renderer?.writeError(` [blocked: fix syntax error in ${pendingSyntaxError} first]`);
|
|
463
375
|
}
|
|
@@ -477,6 +389,14 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
477
389
|
renderer?.writeBracketTag('plan', 'create');
|
|
478
390
|
}
|
|
479
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
|
+
}
|
|
480
400
|
if (!quiet && toolOutput) {
|
|
481
401
|
renderer?.writeNewline();
|
|
482
402
|
renderer?.writeLine(toolOutput);
|
|
@@ -645,6 +565,38 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
645
565
|
toolExitCode = 1;
|
|
646
566
|
}
|
|
647
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
|
+
}
|
|
648
600
|
else if (isBash && cmd.startsWith('plan ')) {
|
|
649
601
|
const planArgs = cmd.slice(5).trim();
|
|
650
602
|
const { action, params } = parsePlanCommand(planArgs);
|
|
@@ -765,6 +717,40 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
765
717
|
autoLearnFromError(hookCmd, toolExitCode !== 0 ? toolOutput : '', '');
|
|
766
718
|
}
|
|
767
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
|
+
}
|
|
768
754
|
// Syntax check after file write operations
|
|
769
755
|
let hasSyntaxError = false;
|
|
770
756
|
if (toolExitCode === 0) {
|
|
@@ -807,7 +793,19 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
807
793
|
}
|
|
808
794
|
}
|
|
809
795
|
catch { /* ignore read errors */ }
|
|
810
|
-
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
|
+
}
|
|
811
809
|
hasSyntaxError = true;
|
|
812
810
|
pendingSyntaxError = filePath;
|
|
813
811
|
}
|
|
@@ -823,6 +821,10 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
823
821
|
collectCodingStyle(filePath, fullContent);
|
|
824
822
|
}
|
|
825
823
|
catch { /* ignore */ }
|
|
824
|
+
// Track created/modified files for completion reporting
|
|
825
|
+
if (!hasSyntaxError && !createdFiles.includes(filePath)) {
|
|
826
|
+
createdFiles.push(filePath);
|
|
827
|
+
}
|
|
826
828
|
}
|
|
827
829
|
}
|
|
828
830
|
}
|
|
@@ -843,9 +845,18 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
|
|
|
843
845
|
}
|
|
844
846
|
messages.push({
|
|
845
847
|
role: 'tool',
|
|
846
|
-
content: toolOutput.slice(0,
|
|
848
|
+
content: toolOutput.slice(0, 8000),
|
|
847
849
|
tool_call_id: toolId,
|
|
848
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
|
+
});
|
|
849
860
|
if (hasSyntaxError) {
|
|
850
861
|
break;
|
|
851
862
|
}
|