micro-models-agent 0.4.0 → 0.6.0

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 (119) hide show
  1. package/dist/agent/context.d.ts +3 -6
  2. package/dist/agent/context.d.ts.map +1 -1
  3. package/dist/agent/context.js +8 -41
  4. package/dist/agent/context.js.map +1 -1
  5. package/dist/agent/error-escalation.d.ts +6 -6
  6. package/dist/agent/error-escalation.d.ts.map +1 -1
  7. package/dist/agent/error-escalation.js +67 -34
  8. package/dist/agent/error-escalation.js.map +1 -1
  9. package/dist/agent/loop.d.ts.map +1 -1
  10. package/dist/agent/loop.js +14 -79
  11. package/dist/agent/loop.js.map +1 -1
  12. package/dist/agent/memory.d.ts +0 -29
  13. package/dist/agent/memory.d.ts.map +1 -1
  14. package/dist/agent/memory.js +1 -78
  15. package/dist/agent/memory.js.map +1 -1
  16. package/dist/agent/modes/registry.d.ts +1 -1
  17. package/dist/agent/modes/registry.d.ts.map +1 -1
  18. package/dist/agent/modes/registry.js +26 -27
  19. package/dist/agent/modes/registry.js.map +1 -1
  20. package/dist/agent/modes/types.d.ts +0 -5
  21. package/dist/agent/modes/types.d.ts.map +1 -1
  22. package/dist/agent/system-prompt.d.ts +1 -1
  23. package/dist/agent/system-prompt.d.ts.map +1 -1
  24. package/dist/agent/system-prompt.js +95 -147
  25. package/dist/agent/system-prompt.js.map +1 -1
  26. package/dist/agent/tool-executor.d.ts +2 -6
  27. package/dist/agent/tool-executor.d.ts.map +1 -1
  28. package/dist/agent/tool-executor.js +79 -65
  29. package/dist/agent/tool-executor.js.map +1 -1
  30. package/dist/agent/user-profile.d.ts +1 -21
  31. package/dist/agent/user-profile.d.ts.map +1 -1
  32. package/dist/agent/user-profile.js +41 -174
  33. package/dist/agent/user-profile.js.map +1 -1
  34. package/dist/agent/write-enforcer.d.ts +3 -7
  35. package/dist/agent/write-enforcer.d.ts.map +1 -1
  36. package/dist/agent/write-enforcer.js +7 -71
  37. package/dist/agent/write-enforcer.js.map +1 -1
  38. package/dist/cli/config.d.ts.map +1 -1
  39. package/dist/cli/config.js +2 -25
  40. package/dist/cli/config.js.map +1 -1
  41. package/dist/cli.d.ts +1 -1
  42. package/dist/cli.d.ts.map +1 -1
  43. package/dist/cli.js +102 -139
  44. package/dist/cli.js.map +1 -1
  45. package/dist/config/config.d.ts +15 -0
  46. package/dist/config/config.d.ts.map +1 -1
  47. package/dist/config/config.js +4 -38
  48. package/dist/config/config.js.map +1 -1
  49. package/dist/config/types.d.ts +0 -2
  50. package/dist/config/types.d.ts.map +1 -1
  51. package/dist/logger.d.ts +10 -3
  52. package/dist/logger.d.ts.map +1 -1
  53. package/dist/logger.js +89 -38
  54. package/dist/logger.js.map +1 -1
  55. package/dist/repl/commands.d.ts +16 -16
  56. package/dist/repl/commands.d.ts.map +1 -1
  57. package/dist/repl/commands.js +474 -321
  58. package/dist/repl/commands.js.map +1 -1
  59. package/dist/repl/index.d.ts +4 -4
  60. package/dist/repl/index.d.ts.map +1 -1
  61. package/dist/repl/index.js +60 -56
  62. package/dist/repl/index.js.map +1 -1
  63. package/dist/session/store.d.ts +4 -7
  64. package/dist/session/store.d.ts.map +1 -1
  65. package/dist/session/store.js +35 -71
  66. package/dist/session/store.js.map +1 -1
  67. package/dist/setup-wizard.d.ts.map +1 -1
  68. package/dist/setup-wizard.js +5 -23
  69. package/dist/setup-wizard.js.map +1 -1
  70. package/dist/styling/engine.d.ts +7 -12
  71. package/dist/styling/engine.d.ts.map +1 -1
  72. package/dist/styling/engine.js +85 -134
  73. package/dist/styling/engine.js.map +1 -1
  74. package/dist/styling/index.d.ts +2 -2
  75. package/dist/styling/index.d.ts.map +1 -1
  76. package/dist/styling/index.js +0 -1
  77. package/dist/styling/index.js.map +1 -1
  78. package/dist/styling/loader.d.ts +2 -2
  79. package/dist/styling/loader.d.ts.map +1 -1
  80. package/dist/styling/loader.js +99 -47
  81. package/dist/styling/loader.js.map +1 -1
  82. package/dist/subagent/chat.d.ts +19 -0
  83. package/dist/subagent/chat.d.ts.map +1 -0
  84. package/dist/subagent/chat.js +279 -0
  85. package/dist/subagent/chat.js.map +1 -0
  86. package/dist/subagent/context.d.ts +9 -0
  87. package/dist/subagent/context.d.ts.map +1 -0
  88. package/dist/subagent/context.js +75 -0
  89. package/dist/subagent/context.js.map +1 -0
  90. package/dist/subagent/definition.d.ts +7 -0
  91. package/dist/subagent/definition.d.ts.map +1 -0
  92. package/dist/subagent/definition.js +26 -0
  93. package/dist/subagent/definition.js.map +1 -0
  94. package/dist/subagent/errors.d.ts +24 -0
  95. package/dist/subagent/errors.d.ts.map +1 -0
  96. package/dist/subagent/errors.js +85 -0
  97. package/dist/subagent/errors.js.map +1 -0
  98. package/dist/subagent/validation.d.ts +17 -0
  99. package/dist/subagent/validation.d.ts.map +1 -0
  100. package/dist/subagent/validation.js +83 -0
  101. package/dist/subagent/validation.js.map +1 -0
  102. package/dist/tools/hooks.d.ts +2 -0
  103. package/dist/tools/hooks.d.ts.map +1 -1
  104. package/dist/tools/hooks.js +118 -16
  105. package/dist/tools/hooks.js.map +1 -1
  106. package/dist/tools/parse-tool-call.d.ts.map +1 -1
  107. package/dist/tools/parse-tool-call.js +16 -14
  108. package/dist/tools/parse-tool-call.js.map +1 -1
  109. package/dist/tools/registry.d.ts +5 -3
  110. package/dist/tools/registry.d.ts.map +1 -1
  111. package/dist/tools/registry.js +47 -28
  112. package/dist/tools/registry.js.map +1 -1
  113. package/dist/tools/subagent.d.ts +4 -57
  114. package/dist/tools/subagent.d.ts.map +1 -1
  115. package/dist/tools/subagent.js +112 -556
  116. package/dist/tools/subagent.js.map +1 -1
  117. package/dist/tools/types.d.ts +2 -10
  118. package/dist/tools/types.d.ts.map +1 -1
  119. package/package.json +1 -1
@@ -1,466 +1,90 @@
1
- import { generateSubagentToolCallId } from './id-gen.js';
2
- import { parseToolCalls } from './parse-tool-call.js';
3
- import { LMStudioNative } from '../llm/lmstudio-native.js';
4
- import { OpenAICompat } from '../llm/openai-compat.js';
5
- import { ContextManager } from '../agent/context.js';
6
- import { buildSystemPrompt } from '../agent/system-prompt.js';
7
- import { checkFileSyntax } from './syntax-check.js';
8
- import { logInfo, logDebug, logWarn, logLLMRequest, logLLMResponse, logToolCall } from '../logger.js';
9
- import { getLLMConfig } from '../config/config.js';
10
- import { readdirSync, readFileSync, existsSync } from 'node:fs';
11
- import { join } from 'node:path';
1
+ import { generateSubagentToolCallId } from "./id-gen.js";
2
+ import { parseToolCalls } from "./parse-tool-call.js";
3
+ import { LMStudioNative } from "../llm/lmstudio-native.js";
4
+ import { ContextManager } from "../agent/context.js";
5
+ import { buildSystemPrompt } from "../agent/system-prompt.js";
6
+ import { checkFileSyntax } from "./syntax-check.js";
7
+ import { logInfo, logDebug, logWarn, logToolCall, logToolOutput, } from "../logger.js";
8
+ import { getLLMConfig } from "../config/config.js";
12
9
  // ─── Context builder ──────────────────────────────────────
13
- /**
14
- * Собирает контекст для субагента: список файлов, зависимости, tsconfig.
15
- * Сканирует все директории проекта (не только src/).
16
- *
17
- * @param cwd - корневая директория проекта
18
- * @returns Текстовый контекст для system prompt субагента
19
- */
20
- export function buildSubagentContext(cwd) {
21
- const lines = [];
22
- // Scan ALL directories for existing source files (not just src/)
23
- try {
24
- const entries = readdirSync(cwd, { withFileTypes: true });
25
- for (const entry of entries) {
26
- if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules')
27
- continue;
28
- const dirPath = join(cwd, entry.name);
29
- try {
30
- const files = readdirSync(dirPath, { recursive: true })
31
- .filter((f) => /\.(ts|tsx|js|jsx)$/.test(f))
32
- .slice(0, 20);
33
- if (files.length > 0) {
34
- lines.push(`Files in ${entry.name}/: ${files.join(', ')}`);
35
- }
36
- }
37
- catch { /* skip unreadable dirs */ }
38
- }
39
- }
40
- catch { /* no entries */ }
41
- // Include root-level .ts/.js files too
42
- try {
43
- const rootFiles = readdirSync(cwd)
44
- .filter((f) => /\.(ts|js)$/.test(f) && !f.startsWith('.'));
45
- if (rootFiles.length > 0) {
46
- lines.push('Root files: ' + rootFiles.join(', '));
47
- }
48
- }
49
- catch { /* skip */ }
50
- try {
51
- const pkgPath = join(cwd, 'package.json');
52
- if (existsSync(pkgPath)) {
53
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
54
- const deps = Object.keys(pkg.dependencies || {});
55
- const devDeps = Object.keys(pkg.devDependencies || {});
56
- if (deps.length > 0)
57
- lines.push('Dependencies: ' + deps.join(', '));
58
- if (devDeps.length > 0)
59
- lines.push('DevDependencies: ' + devDeps.join(', '));
60
- lines.push('Module type: ' + (pkg.type || 'commonjs'));
61
- }
62
- }
63
- catch { /* no package.json */ }
64
- try {
65
- const tsconfigPath = join(cwd, 'tsconfig.json');
66
- if (existsSync(tsconfigPath)) {
67
- lines.push('Has tsconfig.json');
68
- }
69
- }
70
- catch { /* no tsconfig */ }
71
- return lines.join('\n');
72
- }
73
- /**
74
- * Контекст для specialist'а — фокусированное описание ошибки.
75
- *
76
- * @param toolName - имя упавшего инструмента
77
- * @param toolArgs - аргументы которые были переданы
78
- * @param errorOutput - текст ошибки
79
- * @param userTask - исходный запрос пользователя (краткое описание)
80
- * @param attemptCount - сколько раз ошибка повторилась
81
- * @returns Текст задачи для specialist'а
82
- */
83
- export function buildSpecialistTask(toolName, toolArgs, errorOutput, userTask, attemptCount) {
84
- const argsStr = JSON.stringify(toolArgs, null, 2);
85
- // Специфичные подсказки для web_browse evaluate
86
- let webHints = '';
87
- if (toolName === 'web_browse' && String(toolArgs?.action) === 'evaluate') {
88
- const code = String(toolArgs?.value ?? toolArgs?.code ?? '');
89
- webHints = [
90
- '',
91
- 'web_browse evaluate: "value" is JS code executed in browser context.',
92
- '- Expressions work: document.title, document.querySelectorAll(".item").length',
93
- '- return statements work: return document.title',
94
- '- Multi-line with return: return Array.from(document.querySelectorAll("a")).map(a => a.href)',
95
- `- Current code that failed: ${code.slice(0, 300)}`,
96
- ].join('\n');
97
- }
98
- return [
99
- `The tool "${toolName}" has failed ${attemptCount} times with the same error while working on: "${userTask.slice(0, 200)}"`,
100
- '',
101
- `Failed tool call:`,
102
- ` Tool: ${toolName}`,
103
- ` Arguments: ${argsStr}`,
104
- '',
105
- `Error output:`,
106
- ` ${errorOutput.slice(0, 500)}`,
107
- webHints,
108
- '',
109
- `Your task: Diagnose WHY this tool call fails and FIX the underlying problem.`,
110
- ` - Read the relevant files to understand the actual state`,
111
- ` - Fix the root cause (wrong path, missing file, syntax error, etc.)`,
112
- ` - If the tool call arguments are wrong, correct them`,
113
- ` - If the file doesn't exist, create it`,
114
- ` - Return a clear summary of what you found and what you fixed`,
115
- ].join('\n');
116
- }
117
- /** Паттерны ошибок которые НЕ триггерят specialist (trivial) */
118
- const TRIVIAL_ERROR_PATTERNS = [
119
- /no results? found/i,
120
- /pattern .* matched 0 results/i,
121
- /0 files? matched/i,
122
- /command not found.*grep/i,
123
- /not a directory/i,
124
- ];
125
- /** Паттерны ошибок которые ВСЕГДА требуют specialist (не trivial) */
126
- const NON_TRIVIAL_ERROR_PATTERNS = [
127
- /unexpected token/i,
128
- /syntaxerror/i,
129
- /cannot find module/i,
130
- /module not found/i,
131
- /permission denied/i,
132
- /enoent/i,
133
- /eacces/i,
134
- ];
135
- /**
136
- * Проверяет является ли ошибка тривиальной (не требует specialist).
137
- */
138
- export function isTrivialError(toolName, errorOutput) {
139
- // Синтаксические и критические ошибки — ВСЕГДА не trivial
140
- if (NON_TRIVIAL_ERROR_PATTERNS.some(p => p.test(errorOutput))) {
141
- return false;
142
- }
143
- // grep_search/glob_search с 0 результатами — нормальное поведение
144
- if (toolName === 'grep_search' || toolName === 'glob_search') {
145
- if (/0 results|0 files|no match/i.test(errorOutput) && errorOutput.length < 200) {
146
- return true;
147
- }
148
- }
149
- // Любая ошибка < 10 символов — скорее всего trivia
150
- if (errorOutput.length < 10)
151
- return true;
152
- return TRIVIAL_ERROR_PATTERNS.some(p => p.test(errorOutput));
153
- }
10
+ import { buildSubagentContext } from "../subagent/context.js";
154
11
  // ─── Tool call parsing ───────────────────────────────────
12
+ // validateToolCall moved to src/subagent/validation.ts
13
+ import { doChat } from "../subagent/chat.js";
14
+ // ─── Tool execution helper ────────────────────────────────
155
15
  /**
156
- * Валидация tool call: проверка имени инструмента и обязательных аргументов.
16
+ * Выполняет один tool call внутри цикла субагента.
17
+ * Обрабатывает bash и не-bash инструменты, проверяет синтаксис созданных файлов.
157
18
  *
158
- * @param parsed - распарсенный JSON из tool call
159
- * @returns Результат валидации с именем инструмента и командой (для bash)
19
+ * @param call - распарсенный tool call
20
+ * @param toolExecutor - исполнитель инструментов
21
+ * @param cwd - рабочая директория
22
+ * @param createdFiles - массив для отслеживания созданных файлов
23
+ * @param errors - массив для сбора ошибок
24
+ * @param step - номер шага в цикле
160
25
  */
161
- export function validateToolCall(parsed) {
162
- if (!parsed || typeof parsed !== 'object') {
163
- return { valid: false, error: 'Not an object' };
164
- }
165
- const VALID_TOOLS = new Set(['bash', 'plan', 'todo', 'question', 'approve', 'theme', 'user_profile', 'subagent', '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', 'web_browse', 'scaffold', 'project_map']);
166
- if (!parsed.name || !VALID_TOOLS.has(parsed.name)) {
167
- return { valid: false, error: `Unknown tool: ${parsed.name}` };
168
- }
169
- if (!parsed.arguments || typeof parsed.arguments !== 'object') {
170
- return { valid: false, error: 'Missing arguments' };
171
- }
172
- if (parsed.name === 'bash') {
173
- const cmd = String(parsed.arguments.cmd ?? '').trim();
174
- if (!cmd) {
175
- return { valid: false, error: 'Empty command' };
176
- }
177
- return { valid: true, name: parsed.name, cmd };
178
- }
179
- const REQUIRED_ARGS = {
180
- read_file: ['path'],
181
- write_file: ['path', 'content'],
182
- edit_file: ['path', 'old_string', 'new_string'],
183
- list_dir: [],
184
- create_dir: ['path'],
185
- delete_file: ['path'],
186
- copy_file: ['source', 'destination'],
187
- move_file: ['source', 'destination'],
188
- file_info: ['path'],
189
- glob_search: ['pattern'],
190
- grep_search: ['pattern'],
191
- tree: [],
192
- web_search: ['query'],
193
- web_fetch: ['url'],
194
- user_profile: [],
195
- question: ['question'],
26
+ async function executeToolCall(call, toolExecutor, cwd, createdFiles, errors, step) {
27
+ const { parsed, cmd } = call;
28
+ const isBash = parsed.name === "bash";
29
+ const toolId = generateSubagentToolCallId();
30
+ if (parsed.name === "subagent") {
31
+ logWarn("SUBAGENT", `nested subagent blocked at step ${step}`);
32
+ return {
33
+ output: "Subagents cannot spawn subagents (max depth 1)",
34
+ exitCode: 1,
35
+ };
36
+ }
37
+ const preview = isBash
38
+ ? String(cmd).slice(0, 80)
39
+ : parsed.name === "write_file" || parsed.name === "edit_file"
40
+ ? String(parsed.arguments.path ?? "").slice(-60)
41
+ : JSON.stringify(parsed.arguments).slice(0, 80);
42
+ logToolCall(`sub:${parsed.name}`, preview);
43
+ const hookCmd = isBash
44
+ ? String(cmd)
45
+ : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
46
+ const execResult = toolExecutor
47
+ ? await toolExecutor.execute({ id: toolId, name: parsed.name, arguments: parsed.arguments }, { cwd, quiet: false }, hookCmd)
48
+ : null;
49
+ const result = execResult?.result ?? {
50
+ stdout: "",
51
+ stderr: "ToolExecutor not available",
52
+ exitCode: 1,
53
+ truncated: false,
196
54
  };
197
- const required = REQUIRED_ARGS[parsed.name] ?? [];
198
- const missing = required.filter(k => {
199
- const v = parsed.arguments[k];
200
- return v === undefined || v === null || v === '';
201
- });
202
- if (missing.length > 0) {
203
- return { valid: false, error: `Missing required argument(s): ${missing.join(', ')}. Tool "${parsed.name}" requires: ${required.join(', ')}` };
204
- }
205
- return { valid: true, name: parsed.name };
206
- }
207
- // ─── LLM call ───────────────────────────────────────────
208
- /**
209
- * Отправка запроса к LLM с streaming-ответом.
210
- * Поддерживает retry при ошибках и отображение "мышления" модели.
211
- *
212
- * @param provider - провайдер LLM
213
- * @param messages - история сообщений
214
- * @param renderer - рендерер для отображения streaming
215
- * @param signal - сигнал прерывания
216
- * @param retries - количество повторных попыток
217
- * @param showThinking - показывать ли "мышление" модели
218
- * @param caller - кто вызвал (main/subagent)
219
- */
220
- export async function doChat(provider, messages, renderer, signal, retries = 1, showThinking = false, caller = 'main', maxTokens) {
221
- let lastReasoningText = '';
222
- // Phase 1: Normal attempts with thinking enabled
223
- for (let attempt = 0; attempt <= retries; attempt++) {
224
- const startTime = performance.now();
225
- const fullText = [];
226
- let started = false;
227
- let reasoningBuf = false;
228
- let currentReasoning = ''; // накопленный reasoning текст для этого attempt
229
- let lastFinishReason = null;
230
- let reasoningChunksCount = 0;
231
- let contentChunksCount = 0;
232
- if (signal?.aborted)
233
- return { response: '', genTimeMs: 0 };
234
- const lastMsg = messages[messages.length - 1];
235
- logLLMRequest(provider.model, messages.length, lastMsg?.content || '', caller);
236
- if (caller === 'main') {
237
- renderer.setStreamRole?.('assistant');
238
- }
239
- renderer.writeSpinner();
240
- try {
241
- let buf = '';
242
- for await (const chunk of provider.chat(messages, { temperature: 0.7, signal, showThinking, enableThinking: true, max_tokens: maxTokens })) {
243
- if (!chunk.content && !chunk.reasoning && !chunk.finish_reason)
244
- continue;
245
- if (chunk.finish_reason) {
246
- lastFinishReason = chunk.finish_reason;
247
- }
248
- if (chunk.content) {
249
- fullText.push(chunk.content);
250
- contentChunksCount++;
251
- }
252
- if (chunk.reasoning) {
253
- reasoningChunksCount++;
254
- currentReasoning += chunk.reasoning;
255
- }
256
- if (!chunk.content && !chunk.reasoning)
257
- continue;
258
- if (!started) {
259
- renderer.clearSpinner();
260
- started = true;
261
- }
262
- if (chunk.reasoning) {
263
- if (showThinking) {
264
- if (reasoningBuf === false) {
265
- renderer.writeNewline();
266
- renderer.writeDim('┈ reasoning ┈');
267
- reasoningBuf = '';
268
- }
269
- reasoningBuf += chunk.reasoning;
270
- }
271
- continue;
272
- }
273
- if (reasoningBuf !== false && typeof reasoningBuf === 'string') {
274
- renderer.writeNewline();
275
- renderer.writeDim(reasoningBuf);
276
- reasoningBuf = false;
277
- }
278
- buf += chunk.content;
279
- while (buf.length > 0) {
280
- const tcStart = buf.indexOf('<tool_call>');
281
- const tcEnd = buf.indexOf('</tool_call>');
282
- if (tcStart === -1 && tcEnd === -1) {
283
- const clean = buf.replace(/@grounding\[файл:\s*[^\]]*\]\n?/g, '');
284
- if (clean)
285
- renderer.writeMarkdownChunk(clean);
286
- buf = '';
287
- break;
288
- }
289
- if (tcStart !== -1 && (tcEnd === -1 || tcStart < tcEnd)) {
290
- if (tcStart > 0) {
291
- const clean = buf.slice(0, tcStart).replace(/@grounding\[файл:\s*[^\]]*\]\n?/g, '');
292
- if (clean)
293
- renderer.writeMarkdownChunk(clean);
294
- }
295
- buf = buf.slice(tcStart);
296
- const closeIdx = buf.indexOf('</tool_call>');
297
- if (closeIdx !== -1) {
298
- buf = buf.slice(closeIdx + '</tool_call>'.length);
299
- }
300
- break;
301
- }
302
- if (tcEnd !== -1 && (tcStart === -1 || tcEnd < tcStart)) {
303
- const clean = buf.slice(0, tcEnd).replace(/@grounding\[файл:\s*[^\]]*\]\n?/g, '');
304
- if (clean)
305
- renderer.writeMarkdownChunk(clean);
306
- buf = buf.slice(tcEnd + '</tool_call>'.length);
307
- }
308
- }
309
- }
310
- if (reasoningBuf !== false && typeof reasoningBuf === 'string') {
311
- renderer.writeNewline();
312
- renderer.writeDim(reasoningBuf);
313
- }
314
- // Сохраняем reasoning для fallback
315
- if (currentReasoning) {
316
- lastReasoningText = currentReasoning;
317
- }
318
- }
319
- catch (e) {
320
- if (e.name === 'AbortError') {
321
- logLLMResponse(provider.model, 0, performance.now() - startTime, 'Aborted', caller);
322
- return { response: '', genTimeMs: 0 };
323
- }
324
- logLLMResponse(provider.model, 0, performance.now() - startTime, e.message, caller);
325
- renderer.writeError(`\n[LLM] ${e.message}`);
326
- // Сетевые ошибки (fetch failed, ECONNREFUSED и т.д.) — retry с задержкой
327
- const isNetworkError = !e.message?.includes('API ') && !e.message?.includes('status');
328
- if (isNetworkError && attempt < retries) {
329
- const delay = Math.min(2000 * (attempt + 1), 10000);
330
- renderer.writeWarning(`[LLM] Network error, retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${retries})`);
331
- await new Promise(r => setTimeout(r, delay));
332
- continue;
333
- }
334
- const providerHint = provider instanceof LMStudioNative
335
- ? 'Check that LM Studio is running on localhost:1234'
336
- : provider instanceof OpenAICompat
337
- ? 'Check that your API server is running and the API key is correct'
338
- : 'Check that your LLM server is running';
339
- renderer.writeWarning(`[LLM] ${providerHint}`);
340
- return { response: '', genTimeMs: 0 };
341
- }
342
- const genTimeMs = performance.now() - startTime;
343
- const response = fullText.join('');
344
- renderer.commitMarkdown();
345
- if (response.trim()) {
346
- logLLMResponse(provider.model, response.length, genTimeMs, undefined, caller);
347
- return { response, genTimeMs };
348
- }
349
- // Диагностика пустого ответа
350
- const reason = lastFinishReason ?? 'unknown';
351
- logWarn('DOCHAT', `empty response #${attempt + 1}: finish_reason=${reason}, reasoning_chunks=${reasoningChunksCount}, content_chunks=${contentChunksCount}`);
352
- if (lastFinishReason === 'length') {
353
- renderer.writeWarning(` [empty response: model hit max_tokens during reasoning (${reasoningChunksCount} reasoning chunks, 0 content chunks)]`);
354
- }
355
- else if (reasoningChunksCount > 0 && contentChunksCount === 0) {
356
- renderer.writeWarning(` [empty response: model produced ${reasoningChunksCount} reasoning chunks but no content]`);
357
- }
358
- if (attempt < retries) {
359
- renderer.writeDim(`[retry ${attempt + 1}/${retries}]`);
360
- }
361
- }
362
- const llmCfg = getLLMConfig();
363
- // Phase 2: Retry WITHOUT thinking — модель генерирует content напрямую
364
- if (llmCfg.retryWithoutThinking && lastReasoningText.trim()) {
365
- logInfo('DOCHAT', `retrying without thinking (had ${lastReasoningText.length} reasoning chars)`);
366
- renderer.writeDim(`[retry without thinking]`);
367
- const retryResult = await doChatOnce(provider, messages, renderer, signal, false, showThinking, caller, maxTokens);
368
- if (retryResult.response.trim()) {
369
- logInfo('DOCHAT', `retry without thinking succeeded: ${retryResult.response.length} chars`);
370
- return retryResult;
371
- }
372
- }
373
- // Phase 2.5: Emergency — принудительно генерируем content с низкой температурой
374
- // Когда reasoning есть, но content пустой — модель "застряла" в thinking loop
375
- if (lastReasoningText.trim()) {
376
- logInfo('DOCHAT', `emergency: forcing content with low temperature`);
377
- renderer.writeDim(`[emergency: forcing content with temperature=0.1]`);
378
- const emergencyMessages = [
379
- ...messages,
380
- { role: 'user', content: '[System] You MUST produce a response now. Do NOT use thinking tags. Reply with a tool call or a direct answer. Any response is better than empty.' },
381
- ];
382
- const emergencyResult = await doChatOnce(provider, emergencyMessages, renderer, signal, false, showThinking, caller, maxTokens, 0.1);
383
- if (emergencyResult.response.trim()) {
384
- logInfo('DOCHAT', `emergency succeeded: ${emergencyResult.response.length} chars`);
385
- return emergencyResult;
386
- }
387
- }
388
- // Phase 3: Fallback — используем reasoning как content
389
- if (llmCfg.fallbackToReasoning && lastReasoningText.trim()) {
390
- logInfo('DOCHAT', `fallback: using reasoning as content (${lastReasoningText.length} chars)`);
391
- renderer.writeWarning(` [fallback: using reasoning as content]`);
392
- return { response: lastReasoningText, genTimeMs: 0 };
393
- }
394
- return { response: '', genTimeMs: 0 };
395
- }
396
- /**
397
- * Одиночный вызов LLM без retry логики.
398
- * Используется для retry с другими параметрами (например, enableThinking: false).
399
- */
400
- async function doChatOnce(provider, messages, renderer, signal, enableThinking = true, showThinking = false, caller = 'main', maxTokens, temperature = 0.7) {
401
- const startTime = performance.now();
402
- const fullText = [];
403
- let started = false;
404
- if (signal?.aborted)
405
- return { response: '', genTimeMs: 0 };
406
- if (caller === 'main') {
407
- renderer.setStreamRole?.('assistant');
408
- }
409
- renderer.writeSpinner();
410
- try {
411
- let buf = '';
412
- for await (const chunk of provider.chat(messages, { temperature, signal, showThinking, enableThinking, max_tokens: maxTokens })) {
413
- if (!chunk.content && !chunk.finish_reason)
414
- continue;
415
- if (chunk.content) {
416
- fullText.push(chunk.content);
55
+ const output = result.stdout || result.stderr || "(no output)";
56
+ const exitCode = result.exitCode;
57
+ logToolOutput(`sub:${parsed.name}`, output, exitCode);
58
+ if (exitCode !== 0 &&
59
+ isBash &&
60
+ /pwsh|powershell|not recognized|not found/i.test(output)) {
61
+ return {
62
+ output: output +
63
+ "\n\n[HINT] Shell unavailable on this system. Use write_file tool to create files manually. Do NOT retry bash commands.",
64
+ exitCode,
65
+ };
66
+ }
67
+ if (exitCode === 0 &&
68
+ (parsed.name === "write_file" || parsed.name === "edit_file")) {
69
+ const filePath = String(parsed.arguments.path ?? "");
70
+ if (filePath && !createdFiles.includes(filePath)) {
71
+ createdFiles.push(filePath);
72
+ logInfo("SUBAGENT", `created: ${filePath}`);
73
+ }
74
+ if (filePath && /\.(ts|tsx|js|jsx|json)$/.test(filePath)) {
75
+ const { isAbsolute, relative } = await import("node:path");
76
+ const relPath = isAbsolute(filePath)
77
+ ? relative(cwd, filePath).replace(/\\/g, "/")
78
+ : filePath;
79
+ const syntaxResult = checkFileSyntax(relPath, cwd);
80
+ if (syntaxResult !== "OK") {
81
+ errors.push(`${filePath}: ${syntaxResult.split("\n")[0]}`);
82
+ logWarn("SUBAGENT", `syntax error in ${filePath}: ${syntaxResult.split("\n")[0]}`);
83
+ return { output: output + `\nSYNTAX ERROR: ${syntaxResult}`, exitCode };
417
84
  }
418
- if (!chunk.content)
419
- continue;
420
- if (!started) {
421
- renderer.clearSpinner();
422
- started = true;
423
- }
424
- buf += chunk.content;
425
- while (buf.length > 0) {
426
- const tcStart = buf.indexOf('<tool_call>');
427
- const tcEnd = buf.indexOf('</tool_call>');
428
- if (tcStart === -1 && tcEnd === -1) {
429
- if (buf)
430
- renderer.writeMarkdownChunk(buf);
431
- buf = '';
432
- break;
433
- }
434
- if (tcStart !== -1 && (tcEnd === -1 || tcStart < tcEnd)) {
435
- if (tcStart > 0) {
436
- renderer.writeMarkdownChunk(buf.slice(0, tcStart));
437
- }
438
- buf = buf.slice(tcStart);
439
- const closeIdx = buf.indexOf('</tool_call>');
440
- if (closeIdx !== -1) {
441
- buf = buf.slice(closeIdx + '</tool_call>'.length);
442
- }
443
- break;
444
- }
445
- if (tcEnd !== -1 && (tcStart === -1 || tcEnd < tcStart)) {
446
- renderer.writeMarkdownChunk(buf.slice(0, tcEnd));
447
- buf = buf.slice(tcEnd + '</tool_call>'.length);
448
- }
449
- }
450
- }
451
- }
452
- catch (e) {
453
- if (e.name === 'AbortError') {
454
- return { response: '', genTimeMs: 0 };
455
85
  }
456
- renderer.writeError(`\n[LLM] ${e.message}`);
457
- return { response: '', genTimeMs: 0 };
458
86
  }
459
- const genTimeMs = performance.now() - startTime;
460
- const response = fullText.join('');
461
- renderer.commitMarkdown();
462
- logLLMResponse(provider.model, response.length, genTimeMs, undefined, caller);
463
- return { response, genTimeMs };
87
+ return { output, exitCode };
464
88
  }
465
89
  // ─── Isolated agent loop ──────────────────────────────────
466
90
  /**
@@ -483,24 +107,24 @@ export async function runIsolatedLoop(provider, registry, task, existingFiles, c
483
107
  let effectiveProvider = provider;
484
108
  if (toolModel && provider instanceof LMStudioNative) {
485
109
  effectiveProvider = new LMStudioNative(provider.baseUrl, toolModel, provider.contextWindow);
486
- logInfo('SUBAGENT', `using tool model: ${toolModel} (main: ${provider.model})`);
110
+ logInfo("SUBAGENT", `using tool model: ${toolModel} (main: ${provider.model})`);
487
111
  }
488
- logInfo('SUBAGENT', `START | task: "${task.slice(0, 200)}" | context window: ${effectiveProvider.contextWindow}`);
112
+ logInfo("SUBAGENT", `START | task: "${task.slice(0, 200)}" | context window: ${effectiveProvider.contextWindow}`);
489
113
  // Always build auto-context from project, even if model didn't pass existingFiles
490
114
  const autoContext = buildSubagentContext(cwd);
491
115
  const mergedContext = existingFiles
492
- ? existingFiles + '\n' + autoContext
116
+ ? existingFiles + "\n" + autoContext
493
117
  : autoContext;
494
118
  if (mergedContext) {
495
- logDebug('SUBAGENT', `context (${mergedContext.split('\n').length} lines):\n${mergedContext}`);
119
+ logDebug("SUBAGENT", `context (${mergedContext.split("\n").length} lines):\n${mergedContext}`);
496
120
  }
497
121
  const systemPrompt = buildSystemPrompt(undefined, (mode || "base"));
498
122
  const contextInfo = mergedContext
499
123
  ? `\n\nКонтекст проекта:\n${mergedContext}\nПосмотри package.json для списка пакетов. Используй read_file для чтения существующих файлов.`
500
- : '';
124
+ : "";
501
125
  const messages = [
502
- { role: 'system', content: systemPrompt },
503
- { role: 'user', content: task + contextInfo },
126
+ { role: "system", content: systemPrompt },
127
+ { role: "user", content: task + contextInfo },
504
128
  ];
505
129
  const ctx = new ContextManager(effectiveProvider.contextWindow);
506
130
  const maxSteps = maxStepsParam ?? 20;
@@ -508,127 +132,59 @@ export async function runIsolatedLoop(provider, registry, task, existingFiles, c
508
132
  for (let step = 0; step < maxSteps; step++) {
509
133
  await ctx.updateTokens(messages);
510
134
  if (step === 0 || step % 5 === 0) {
511
- logDebug('SUBAGENT', `step ${step} | ${messages.length} msgs | ${ctx.totalTokens} tokens (${ctx.usagePercent.toFixed(1)}%)`);
135
+ logDebug("SUBAGENT", `step ${step} | ${messages.length} msgs | ${ctx.totalTokens} tokens (${ctx.usagePercent.toFixed(1)}%)`);
512
136
  }
513
137
  if (ctx.needsCompact && messages.length > 6) {
514
- logInfo('SUBAGENT', `COMPACT at step ${step} | ${ctx.totalTokens} tokens (${ctx.usagePercent.toFixed(1)}%)`);
138
+ logInfo("SUBAGENT", `COMPACT at step ${step} | ${ctx.totalTokens} tokens (${ctx.usagePercent.toFixed(1)}%)`);
515
139
  messages.splice(0, messages.length, ...ctx.compact(messages));
516
140
  await ctx.updateTokens(messages);
517
- logInfo('SUBAGENT', `after compact | ${messages.length} msgs | ${ctx.totalTokens} tokens`);
141
+ logInfo("SUBAGENT", `after compact | ${messages.length} msgs | ${ctx.totalTokens} tokens`);
518
142
  }
519
143
  // После tool call увеличиваем max_tokens чтобы модель успела сгенерировать контент после reasoning
520
- const hasToolResult = messages.some(m => m.role === 'tool');
144
+ const hasToolResult = messages.some((m) => m.role === "tool");
521
145
  const llmCfg = getLLMConfig();
522
146
  const maxTokens = hasToolResult ? llmCfg.maxTokensAfterTool : undefined;
523
- const { response } = await doChat(effectiveProvider, messages, renderer, renderer?.abortSignal, 1, false, 'subagent', maxTokens);
147
+ const { response } = await doChat(effectiveProvider, messages, renderer, renderer?.abortSignal, 1, false, "subagent", maxTokens);
524
148
  if (!response.trim()) {
525
- logDebug('SUBAGENT', `empty response at step ${step}, stopping`);
149
+ logDebug("SUBAGENT", `empty response at step ${step}, stopping`);
526
150
  break;
527
151
  }
528
- messages.push({ role: 'assistant', content: response });
152
+ messages.push({ role: "assistant", content: response });
529
153
  const { validCalls } = parseToolCalls(response);
530
154
  if (validCalls.length === 0) {
531
- logDebug('SUBAGENT', `no valid tool calls at step ${step}, stopping`);
155
+ logDebug("SUBAGENT", `no valid tool calls at step ${step}, stopping`);
532
156
  break;
533
157
  }
534
158
  for (const call of validCalls) {
535
- const { parsed, cmd } = call;
536
- const isBash = parsed.name === 'bash';
537
- const toolId = generateSubagentToolCallId();
538
159
  toolCallCount++;
539
- let toolOutput = '';
540
- let toolExitCode = 0;
541
- if (parsed.name === 'subagent') {
542
- toolOutput = 'Subagents cannot spawn subagents (max depth 1)';
543
- toolExitCode = 1;
544
- logWarn('SUBAGENT', `nested subagent blocked at step ${step}`);
545
- }
546
- else if (!isBash) {
547
- const preview = parsed.name === 'write_file'
548
- ? String(parsed.arguments.path ?? '').slice(-60)
549
- : parsed.name === 'edit_file'
550
- ? String(parsed.arguments.path ?? '').slice(-60)
551
- : JSON.stringify(parsed.arguments).slice(0, 80);
552
- logToolCall(`sub:${parsed.name}`, preview);
553
- const hookCmd = isBash ? String(cmd) : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
554
- const execResult = toolExecutor ? await toolExecutor.execute({ id: toolId, name: parsed.name, arguments: parsed.arguments }, { cwd, quiet: false }, hookCmd) : null;
555
- const result = execResult?.result ?? { stdout: '', stderr: 'ToolExecutor not available', exitCode: 1, truncated: false };
556
- toolOutput = result.stdout || result.stderr || '(no output)';
557
- toolExitCode = result.exitCode;
558
- if (toolExitCode !== 0) {
559
- logDebug('SUBAGENT', `tool ${parsed.name} failed: exit ${toolExitCode} | ${toolOutput.slice(0, 200)}`);
560
- }
561
- if (toolExitCode === 0 && (parsed.name === 'write_file' || parsed.name === 'edit_file')) {
562
- const filePath = String(parsed.arguments.path ?? '');
563
- if (filePath && !createdFiles.includes(filePath)) {
564
- createdFiles.push(filePath);
565
- logInfo('SUBAGENT', `created: ${filePath}`);
566
- }
567
- }
568
- if (toolExitCode === 0 && (parsed.name === 'write_file' || parsed.name === 'edit_file')) {
569
- const filePath = String(parsed.arguments.path ?? '');
570
- if (filePath && /\.(ts|tsx|js|jsx|json)$/.test(filePath)) {
571
- const { isAbsolute, relative } = await import('node:path');
572
- const relPath = isAbsolute(filePath) ? relative(cwd, filePath).replace(/\\/g, '/') : filePath;
573
- const syntaxResult = checkFileSyntax(relPath, cwd);
574
- if (syntaxResult !== 'OK') {
575
- errors.push(`${filePath}: ${syntaxResult.split('\n')[0]}`);
576
- toolOutput += `\nSYNTAX ERROR: ${syntaxResult}`;
577
- logWarn('SUBAGENT', `syntax error in ${filePath}: ${syntaxResult.split('\n')[0]}`);
578
- }
579
- }
580
- }
581
- }
582
- else {
583
- logToolCall('sub:bash', String(cmd).slice(0, 80));
584
- const bashExecResult = toolExecutor ? await toolExecutor.execute({ id: toolId, name: 'bash', arguments: { cmd, timeout: parsed.arguments?.timeout ?? 30 } }, { cwd, quiet: false }, String(cmd)) : null;
585
- const result = bashExecResult?.result ?? { stdout: '', stderr: 'ToolExecutor not available', exitCode: 1, truncated: false };
586
- toolOutput = result.stdout || result.stderr || '(no output)';
587
- toolExitCode = result.exitCode;
588
- if (toolExitCode !== 0) {
589
- logDebug('SUBAGENT', `bash failed: exit ${toolExitCode} | ${toolOutput.slice(0, 200)}`);
590
- // Если bash падает из-за отсутствия shell —提示 модель использовать write_file
591
- if (/pwsh|powershell|not recognized|not found/i.test(toolOutput)) {
592
- toolOutput += '\n\n[HINT] Shell unavailable on this system. Use write_file tool to create files manually. Do NOT retry bash commands.';
593
- }
594
- }
595
- }
596
- messages.push({ role: 'tool', content: toolOutput.slice(0, 8000), tool_call_id: toolId });
597
- messages.push({ role: 'user', content: 'Continue.' });
160
+ const { output, exitCode } = await executeToolCall(call, toolExecutor, cwd, createdFiles, errors, step);
161
+ messages.push({
162
+ role: "tool",
163
+ content: output.slice(0, 8000),
164
+ tool_call_id: generateSubagentToolCallId(),
165
+ });
166
+ messages.push({ role: "user", content: "Continue." });
598
167
  }
599
168
  }
600
- const turns = messages.filter(m => m.role === 'assistant').length;
169
+ const turns = messages.filter((m) => m.role === "assistant").length;
601
170
  const elapsedMs = performance.now() - startTime;
602
171
  await ctx.updateTokens(messages);
603
172
  const finalTokens = ctx.totalTokens;
604
173
  const finalUsage = ctx.usagePercent;
605
- logInfo('SUBAGENT', `DONE | ${turns} turns | ${toolCallCount} tool calls | ${createdFiles.length} files created | ${errors.length} errors | ${finalTokens} tokens (${finalUsage.toFixed(1)}%) | ${(elapsedMs / 1000).toFixed(1)}s`);
174
+ logInfo("SUBAGENT", `DONE | ${turns} turns | ${toolCallCount} tool calls | ${createdFiles.length} files created | ${errors.length} errors | ${finalTokens} tokens (${finalUsage.toFixed(1)}%) | ${(elapsedMs / 1000).toFixed(1)}s`);
606
175
  // Собираем список использованных инструментов из истории
607
- const toolsUsed = [...new Set(messages
608
- .filter(m => m.role === 'assistant')
609
- .flatMap(m => {
176
+ const toolsUsed = [
177
+ ...new Set(messages
178
+ .filter((m) => m.role === "assistant")
179
+ .flatMap((m) => {
610
180
  const calls = m.content.matchAll(/<tool_call>\{"name":"([^"]+)"/g);
611
- return [...calls].map(c => c[1]);
612
- }))];
613
- const toolsStr = toolsUsed.length > 0 ? `\nTools used: ${toolsUsed.join(', ')}` : '';
181
+ return [...calls].map((c) => c[1]);
182
+ })),
183
+ ];
184
+ const toolsStr = toolsUsed.length > 0 ? `\nTools used: ${toolsUsed.join(", ")}` : "";
614
185
  const summary = createdFiles.length > 0
615
- ? `[Subagent done — ${createdFiles.length} file(s), ${turns} turns]\nFiles: ${createdFiles.join(', ')}${toolsStr}${errors.length > 0 ? `\nErrors: ${errors.join('; ')}` : ''}`
616
- : `[Subagent done — no files, ${turns} turns]${toolsStr}${errors.length > 0 ? `\nErrors: ${errors.join('; ')}` : ''}`;
186
+ ? `[Subagent done — ${createdFiles.length} file(s), ${turns} turns]\nFiles: ${createdFiles.join(", ")}${toolsStr}${errors.length > 0 ? `\nErrors: ${errors.join("; ")}` : ""}`
187
+ : `[Subagent done — no files, ${turns} turns]${toolsStr}${errors.length > 0 ? `\nErrors: ${errors.join("; ")}` : ""}`;
617
188
  return { createdFiles, errors, summary, turns };
618
189
  }
619
- export const subagentDefinition = {
620
- type: 'function',
621
- function: {
622
- name: 'subagent',
623
- description: 'Delegate a task to an isolated agent with a FRESH context (no history of prior errors). Use for: (1) each file when creating 3+ new files, (2) any task where you are stuck or making repeated errors, (3) simple single-step operations (bash, file read/write). The subagent has all tools and starts clean — ideal when accumulated context is causing confusion.',
624
- parameters: {
625
- type: 'object',
626
- properties: {
627
- task: { type: 'string', description: 'What the subagent should do — be specific and include file paths' },
628
- existingFiles: { type: 'string', description: 'Comma-separated files the subagent can read for context (optional)' },
629
- },
630
- required: ['task'],
631
- },
632
- },
633
- };
634
190
  //# sourceMappingURL=subagent.js.map