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.
Files changed (79) hide show
  1. package/dist/agent/loop.d.ts.map +1 -1
  2. package/dist/agent/loop.js +230 -219
  3. package/dist/agent/loop.js.map +1 -1
  4. package/dist/agent/system-prompt.d.ts.map +1 -1
  5. package/dist/agent/system-prompt.js +62 -23
  6. package/dist/agent/system-prompt.js.map +1 -1
  7. package/dist/async-map.d.ts +6 -0
  8. package/dist/async-map.d.ts.map +1 -0
  9. package/dist/async-map.js +16 -0
  10. package/dist/async-map.js.map +1 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +15 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/examples.d.ts +22 -0
  15. package/dist/examples.d.ts.map +1 -0
  16. package/dist/examples.js +45 -0
  17. package/dist/examples.js.map +1 -0
  18. package/dist/fibonacci.d.ts +19 -0
  19. package/dist/fibonacci.d.ts.map +1 -0
  20. package/dist/fibonacci.js +61 -0
  21. package/dist/fibonacci.js.map +1 -0
  22. package/dist/logger.d.ts +2 -2
  23. package/dist/logger.d.ts.map +1 -1
  24. package/dist/logger.js +7 -5
  25. package/dist/logger.js.map +1 -1
  26. package/dist/skills/builtin/agent-test/SKILL.md +68 -0
  27. package/dist/sort.d.ts +8 -0
  28. package/dist/sort.d.ts.map +1 -0
  29. package/dist/sort.js +36 -0
  30. package/dist/sort.js.map +1 -0
  31. package/dist/stack.d.ts +9 -0
  32. package/dist/stack.d.ts.map +1 -0
  33. package/dist/stack.js +22 -0
  34. package/dist/stack.js.map +1 -0
  35. package/dist/test-run.d.ts +2 -0
  36. package/dist/test-run.d.ts.map +1 -0
  37. package/dist/test-run.js +13 -0
  38. package/dist/test-run.js.map +1 -0
  39. package/dist/tools/bash.d.ts.map +1 -1
  40. package/dist/tools/bash.js +9 -7
  41. package/dist/tools/bash.js.map +1 -1
  42. package/dist/tools/fs/grep-search.js +1 -1
  43. package/dist/tools/fs/grep-search.js.map +1 -1
  44. package/dist/tools/registry.d.ts.map +1 -1
  45. package/dist/tools/registry.js +19 -1
  46. package/dist/tools/registry.js.map +1 -1
  47. package/dist/tools/subagent.d.ts +25 -0
  48. package/dist/tools/subagent.d.ts.map +1 -0
  49. package/dist/tools/subagent.js +412 -0
  50. package/dist/tools/subagent.js.map +1 -0
  51. package/dist/tools/syntax-check.d.ts.map +1 -1
  52. package/dist/tools/syntax-check.js +7 -2
  53. package/dist/tools/syntax-check.js.map +1 -1
  54. package/dist/ui/ink/components/ToolCallBlock.d.ts.map +1 -1
  55. package/dist/ui/ink/components/ToolCallBlock.js +10 -1
  56. package/dist/ui/ink/components/ToolCallBlock.js.map +1 -1
  57. package/dist/ui/ink/renderer.d.ts +1 -1
  58. package/dist/ui/ink/renderer.d.ts.map +1 -1
  59. package/dist/ui/ink/renderer.js +2 -2
  60. package/dist/ui/ink/renderer.js.map +1 -1
  61. package/dist/ui/readline-renderer.d.ts +1 -1
  62. package/dist/ui/readline-renderer.d.ts.map +1 -1
  63. package/dist/ui/readline-renderer.js +4 -1
  64. package/dist/ui/readline-renderer.js.map +1 -1
  65. package/dist/ui/renderer.d.ts +1 -1
  66. package/dist/ui/renderer.d.ts.map +1 -1
  67. package/dist/utils/memoize.d.ts +14 -0
  68. package/dist/utils/memoize.d.ts.map +1 -0
  69. package/dist/utils/memoize.js +51 -0
  70. package/dist/utils/memoize.js.map +1 -0
  71. package/dist/utils/types.d.ts +6 -0
  72. package/dist/utils/types.d.ts.map +1 -0
  73. package/dist/utils/types.js +2 -0
  74. package/dist/utils/types.js.map +1 -0
  75. package/dist/utils.d.ts +22 -0
  76. package/dist/utils.d.ts.map +1 -0
  77. package/dist/utils.js +81 -0
  78. package/dist/utils.js.map +1 -0
  79. package/package.json +3 -3
@@ -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;AA8OpD,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,CAqnB1B"}
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"}
@@ -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,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 model suggested them in response (disabled in fast mode)
330
- if (options?.mode !== 'fast') {
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(', ')}. 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.]` });
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 === 'tool' && consecutiveEmpty <= 3) {
360
- const retryPrompt = consecutiveEmpty === 1
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 #${consecutiveEmpty}, retrying]`);
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
- 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.`;
412
- if (!quiet) {
413
- 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;
414
227
  }
415
- messages.push({ role: 'tool', content: feedback, tool_call_id: `invalid-${Date.now()}` });
416
- 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`);
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
- if (hasRecentToolResult) {
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. 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.]' });
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
- if (isWriteOp && String(parsed.arguments.path ?? '') !== pendingSyntaxError) {
460
- 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).`;
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}\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
+ }
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, 4000),
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
  }