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.
Files changed (107) hide show
  1. package/dist/agent/loop.d.ts +2 -0
  2. package/dist/agent/loop.d.ts.map +1 -1
  3. package/dist/agent/loop.js +270 -238
  4. package/dist/agent/loop.js.map +1 -1
  5. package/dist/agent/system-prompt.d.ts +2 -1
  6. package/dist/agent/system-prompt.d.ts.map +1 -1
  7. package/dist/agent/system-prompt.js +87 -21
  8. package/dist/agent/system-prompt.js.map +1 -1
  9. package/dist/async-map.d.ts +6 -0
  10. package/dist/async-map.d.ts.map +1 -0
  11. package/dist/async-map.js +16 -0
  12. package/dist/async-map.js.map +1 -0
  13. package/dist/config/config.js +1 -1
  14. package/dist/config/config.js.map +1 -1
  15. package/dist/config/types.d.ts +2 -0
  16. package/dist/config/types.d.ts.map +1 -1
  17. package/dist/examples.d.ts +22 -0
  18. package/dist/examples.d.ts.map +1 -0
  19. package/dist/examples.js +45 -0
  20. package/dist/examples.js.map +1 -0
  21. package/dist/fibonacci.d.ts +19 -0
  22. package/dist/fibonacci.d.ts.map +1 -0
  23. package/dist/fibonacci.js +61 -0
  24. package/dist/fibonacci.js.map +1 -0
  25. package/dist/llm/openai-compat.d.ts +1 -1
  26. package/dist/llm/openai-compat.d.ts.map +1 -1
  27. package/dist/llm/openai-compat.js.map +1 -1
  28. package/dist/logger.d.ts +2 -2
  29. package/dist/logger.d.ts.map +1 -1
  30. package/dist/logger.js +7 -5
  31. package/dist/logger.js.map +1 -1
  32. package/dist/repl/commands.d.ts.map +1 -1
  33. package/dist/repl/commands.js +2 -0
  34. package/dist/repl/commands.js.map +1 -1
  35. package/dist/repl/history.d.ts +2 -0
  36. package/dist/repl/history.d.ts.map +1 -1
  37. package/dist/repl/history.js.map +1 -1
  38. package/dist/repl/index.d.ts.map +1 -1
  39. package/dist/repl/index.js +46 -11
  40. package/dist/repl/index.js.map +1 -1
  41. package/dist/skills/builtin/agent-test/SKILL.md +68 -0
  42. package/dist/skills/builtin/grill-me/SKILL.md +46 -0
  43. package/dist/sort.d.ts +8 -0
  44. package/dist/sort.d.ts.map +1 -0
  45. package/dist/sort.js +36 -0
  46. package/dist/sort.js.map +1 -0
  47. package/dist/stack.d.ts +9 -0
  48. package/dist/stack.d.ts.map +1 -0
  49. package/dist/stack.js +22 -0
  50. package/dist/stack.js.map +1 -0
  51. package/dist/test-run.d.ts +2 -0
  52. package/dist/test-run.d.ts.map +1 -0
  53. package/dist/test-run.js +13 -0
  54. package/dist/test-run.js.map +1 -0
  55. package/dist/tools/bash.d.ts.map +1 -1
  56. package/dist/tools/bash.js +9 -7
  57. package/dist/tools/bash.js.map +1 -1
  58. package/dist/tools/fs/grep-search.js +1 -1
  59. package/dist/tools/fs/grep-search.js.map +1 -1
  60. package/dist/tools/registry.d.ts.map +1 -1
  61. package/dist/tools/registry.js +19 -1
  62. package/dist/tools/registry.js.map +1 -1
  63. package/dist/tools/subagent.d.ts +25 -0
  64. package/dist/tools/subagent.d.ts.map +1 -0
  65. package/dist/tools/subagent.js +412 -0
  66. package/dist/tools/subagent.js.map +1 -0
  67. package/dist/tools/syntax-check.d.ts.map +1 -1
  68. package/dist/tools/syntax-check.js +7 -2
  69. package/dist/tools/syntax-check.js.map +1 -1
  70. package/dist/ui/ink/components/App.d.ts +13 -1
  71. package/dist/ui/ink/components/App.d.ts.map +1 -1
  72. package/dist/ui/ink/components/App.js +50 -4
  73. package/dist/ui/ink/components/App.js.map +1 -1
  74. package/dist/ui/ink/components/PromptInput.d.ts +3 -1
  75. package/dist/ui/ink/components/PromptInput.d.ts.map +1 -1
  76. package/dist/ui/ink/components/PromptInput.js +39 -2
  77. package/dist/ui/ink/components/PromptInput.js.map +1 -1
  78. package/dist/ui/ink/components/SelectInput.d.ts +9 -0
  79. package/dist/ui/ink/components/SelectInput.d.ts.map +1 -0
  80. package/dist/ui/ink/components/SelectInput.js +30 -0
  81. package/dist/ui/ink/components/SelectInput.js.map +1 -0
  82. package/dist/ui/ink/components/ToolCallBlock.d.ts.map +1 -1
  83. package/dist/ui/ink/components/ToolCallBlock.js +10 -1
  84. package/dist/ui/ink/components/ToolCallBlock.js.map +1 -1
  85. package/dist/ui/ink/renderer.d.ts +7 -2
  86. package/dist/ui/ink/renderer.d.ts.map +1 -1
  87. package/dist/ui/ink/renderer.js +25 -3
  88. package/dist/ui/ink/renderer.js.map +1 -1
  89. package/dist/ui/readline-renderer.d.ts +2 -1
  90. package/dist/ui/readline-renderer.d.ts.map +1 -1
  91. package/dist/ui/readline-renderer.js +7 -1
  92. package/dist/ui/readline-renderer.js.map +1 -1
  93. package/dist/ui/renderer.d.ts +5 -1
  94. package/dist/ui/renderer.d.ts.map +1 -1
  95. package/dist/utils/memoize.d.ts +14 -0
  96. package/dist/utils/memoize.d.ts.map +1 -0
  97. package/dist/utils/memoize.js +51 -0
  98. package/dist/utils/memoize.js.map +1 -0
  99. package/dist/utils/types.d.ts +6 -0
  100. package/dist/utils/types.d.ts.map +1 -0
  101. package/dist/utils/types.js +2 -0
  102. package/dist/utils/types.js.map +1 -0
  103. package/dist/utils.d.ts +22 -0
  104. package/dist/utils.d.ts.map +1 -0
  105. package/dist/utils.js +81 -0
  106. package/dist/utils.js.map +1 -0
  107. package/package.json +3 -3
@@ -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, logDebug, logLLMRequest, logLLMResponse, logToolCall, setStatusWriter, writeStatus } from '../logger.js';
21
- function extractAllToolCalls(response) {
22
- const results = [];
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
- // Auto-load skills if model suggested them in response
311
- const suggestedSkillRefs = response.matchAll(/@(\w[\w-]*)/g);
312
- const newSkillNames = [];
313
- for (const match of suggestedSkillRefs) {
314
- const name = match[1];
315
- if (['email', 'api', 'id', 'json', 'url', 'http', 'https', 'localhost'].includes(name))
316
- continue;
317
- if (!loadedSkillNames.has(name)) {
318
- const skill = loadSkill(name, process.cwd());
319
- if (skill) {
320
- newSkillNames.push(name);
321
- loadedSkills.push(skill);
322
- loadedSkillNames.add(name);
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
- if (newSkillNames.length > 0) {
327
- skillsAutoLoaded = true;
328
- renderer?.writeInfo(` Auto-loaded ${newSkillNames.length} skill(s): ${newSkillNames.join(', ')}`);
329
- messages[0] = { role: 'system', content: buildSystemPrompt(loadedSkills) };
330
- messages.push({ role: 'user', content: `[System: Skills loaded: ${newSkillNames.join(', ')}. They are in your context now. IMPORTANT: Use filesystem tools (write_file, create_dir, edit_file, read_file, etc.) for all file operations — NOT bash. Skills show bash examples for reference only. Create a plan and start working NOW — do NOT wait for user confirmation.]` });
331
- // Force model to continue working after skills are loaded
332
- continue;
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 === 'tool' && consecutiveEmpty <= 3) {
339
- const retryPrompt = consecutiveEmpty === 1
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 #${consecutiveEmpty}, retrying]`);
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
- 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.`;
391
- if (!quiet) {
392
- renderer?.writeError(` [invalid tool call: ${invalidCalls[0]}]`);
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
- messages.push({ role: 'tool', content: feedback, tool_call_id: `invalid-${Date.now()}` });
395
- continue;
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
- if (hasRecentToolResult) {
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. Continue: call the next tool or write the final summary. Do NOT just repeat the plan — take action.]' });
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
- if (isWriteOp && String(parsed.arguments.path ?? '') !== pendingSyntaxError) {
439
- const blocked = `BLOCKED: You have a syntax error in ${pendingSyntaxError}. Fix it first before writing new files.`;
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}\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"}}`;
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, 4000),
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
  }