micro-models-agent 0.4.0 → 0.5.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.
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +2 -1
- package/dist/agent/loop.js.map +1 -1
- package/dist/logger.d.ts +10 -3
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +89 -38
- package/dist/logger.js.map +1 -1
- package/dist/subagent/chat.d.ts +7 -0
- package/dist/subagent/chat.d.ts.map +1 -0
- package/dist/subagent/chat.js +257 -0
- package/dist/subagent/chat.js.map +1 -0
- package/dist/subagent/context.d.ts +2 -0
- package/dist/subagent/context.d.ts.map +1 -0
- package/dist/subagent/context.js +64 -0
- package/dist/subagent/context.js.map +1 -0
- package/dist/subagent/definition.d.ts +3 -0
- package/dist/subagent/definition.d.ts.map +1 -0
- package/dist/subagent/definition.js +22 -0
- package/dist/subagent/definition.js.map +1 -0
- package/dist/subagent/errors.d.ts +7 -0
- package/dist/subagent/errors.d.ts.map +1 -0
- package/dist/subagent/errors.js +64 -0
- package/dist/subagent/errors.js.map +1 -0
- package/dist/subagent/validation.d.ts +11 -0
- package/dist/subagent/validation.d.ts.map +1 -0
- package/dist/subagent/validation.js +77 -0
- package/dist/subagent/validation.js.map +1 -0
- package/dist/tools/subagent.d.ts +14 -57
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +112 -559
- package/dist/tools/subagent.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/subagent.js
CHANGED
|
@@ -1,466 +1,85 @@
|
|
|
1
|
-
import { generateSubagentToolCallId } from
|
|
2
|
-
import { parseToolCalls } from
|
|
3
|
-
import { LMStudioNative } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
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";
|
|
9
|
+
import { validateToolCall } from "../subagent/validation.js";
|
|
10
|
+
export { validateToolCall };
|
|
12
11
|
// ─── Context builder ──────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
}
|
|
12
|
+
import { buildSubagentContext } from "../subagent/context.js";
|
|
13
|
+
export { buildSubagentContext };
|
|
14
|
+
import { buildSpecialistTask, isTrivialError } from "../subagent/errors.js";
|
|
15
|
+
export { buildSpecialistTask, isTrivialError };
|
|
154
16
|
// ─── Tool call parsing ───────────────────────────────────
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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'],
|
|
17
|
+
// validateToolCall moved to src/subagent/validation.ts
|
|
18
|
+
import { doChat } from "../subagent/chat.js";
|
|
19
|
+
export { doChat };
|
|
20
|
+
// ─── Tool execution helper ────────────────────────────────
|
|
21
|
+
async function executeToolCall(call, toolExecutor, cwd, createdFiles, errors, step) {
|
|
22
|
+
const { parsed, cmd } = call;
|
|
23
|
+
const isBash = parsed.name === "bash";
|
|
24
|
+
const toolId = generateSubagentToolCallId();
|
|
25
|
+
if (parsed.name === "subagent") {
|
|
26
|
+
logWarn("SUBAGENT", `nested subagent blocked at step ${step}`);
|
|
27
|
+
return {
|
|
28
|
+
output: "Subagents cannot spawn subagents (max depth 1)",
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const preview = isBash
|
|
33
|
+
? String(cmd).slice(0, 80)
|
|
34
|
+
: parsed.name === "write_file" || parsed.name === "edit_file"
|
|
35
|
+
? String(parsed.arguments.path ?? "").slice(-60)
|
|
36
|
+
: JSON.stringify(parsed.arguments).slice(0, 80);
|
|
37
|
+
logToolCall(`sub:${parsed.name}`, preview);
|
|
38
|
+
const hookCmd = isBash
|
|
39
|
+
? String(cmd)
|
|
40
|
+
: `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
|
|
41
|
+
const execResult = toolExecutor
|
|
42
|
+
? await toolExecutor.execute({ id: toolId, name: parsed.name, arguments: parsed.arguments }, { cwd, quiet: false }, hookCmd)
|
|
43
|
+
: null;
|
|
44
|
+
const result = execResult?.result ?? {
|
|
45
|
+
stdout: "",
|
|
46
|
+
stderr: "ToolExecutor not available",
|
|
47
|
+
exitCode: 1,
|
|
48
|
+
truncated: false,
|
|
196
49
|
};
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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);
|
|
50
|
+
const output = result.stdout || result.stderr || "(no output)";
|
|
51
|
+
const exitCode = result.exitCode;
|
|
52
|
+
logToolOutput(`sub:${parsed.name}`, output, exitCode);
|
|
53
|
+
if (exitCode !== 0 &&
|
|
54
|
+
isBash &&
|
|
55
|
+
/pwsh|powershell|not recognized|not found/i.test(output)) {
|
|
56
|
+
return {
|
|
57
|
+
output: output +
|
|
58
|
+
"\n\n[HINT] Shell unavailable on this system. Use write_file tool to create files manually. Do NOT retry bash commands.",
|
|
59
|
+
exitCode,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (exitCode === 0 &&
|
|
63
|
+
(parsed.name === "write_file" || parsed.name === "edit_file")) {
|
|
64
|
+
const filePath = String(parsed.arguments.path ?? "");
|
|
65
|
+
if (filePath && !createdFiles.includes(filePath)) {
|
|
66
|
+
createdFiles.push(filePath);
|
|
67
|
+
logInfo("SUBAGENT", `created: ${filePath}`);
|
|
68
|
+
}
|
|
69
|
+
if (filePath && /\.(ts|tsx|js|jsx|json)$/.test(filePath)) {
|
|
70
|
+
const { isAbsolute, relative } = await import("node:path");
|
|
71
|
+
const relPath = isAbsolute(filePath)
|
|
72
|
+
? relative(cwd, filePath).replace(/\\/g, "/")
|
|
73
|
+
: filePath;
|
|
74
|
+
const syntaxResult = checkFileSyntax(relPath, cwd);
|
|
75
|
+
if (syntaxResult !== "OK") {
|
|
76
|
+
errors.push(`${filePath}: ${syntaxResult.split("\n")[0]}`);
|
|
77
|
+
logWarn("SUBAGENT", `syntax error in ${filePath}: ${syntaxResult.split("\n")[0]}`);
|
|
78
|
+
return { output: output + `\nSYNTAX ERROR: ${syntaxResult}`, exitCode };
|
|
417
79
|
}
|
|
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
80
|
}
|
|
456
|
-
renderer.writeError(`\n[LLM] ${e.message}`);
|
|
457
|
-
return { response: '', genTimeMs: 0 };
|
|
458
81
|
}
|
|
459
|
-
|
|
460
|
-
const response = fullText.join('');
|
|
461
|
-
renderer.commitMarkdown();
|
|
462
|
-
logLLMResponse(provider.model, response.length, genTimeMs, undefined, caller);
|
|
463
|
-
return { response, genTimeMs };
|
|
82
|
+
return { output, exitCode };
|
|
464
83
|
}
|
|
465
84
|
// ─── Isolated agent loop ──────────────────────────────────
|
|
466
85
|
/**
|
|
@@ -483,24 +102,24 @@ export async function runIsolatedLoop(provider, registry, task, existingFiles, c
|
|
|
483
102
|
let effectiveProvider = provider;
|
|
484
103
|
if (toolModel && provider instanceof LMStudioNative) {
|
|
485
104
|
effectiveProvider = new LMStudioNative(provider.baseUrl, toolModel, provider.contextWindow);
|
|
486
|
-
logInfo(
|
|
105
|
+
logInfo("SUBAGENT", `using tool model: ${toolModel} (main: ${provider.model})`);
|
|
487
106
|
}
|
|
488
|
-
logInfo(
|
|
107
|
+
logInfo("SUBAGENT", `START | task: "${task.slice(0, 200)}" | context window: ${effectiveProvider.contextWindow}`);
|
|
489
108
|
// Always build auto-context from project, even if model didn't pass existingFiles
|
|
490
109
|
const autoContext = buildSubagentContext(cwd);
|
|
491
110
|
const mergedContext = existingFiles
|
|
492
|
-
? existingFiles +
|
|
111
|
+
? existingFiles + "\n" + autoContext
|
|
493
112
|
: autoContext;
|
|
494
113
|
if (mergedContext) {
|
|
495
|
-
logDebug(
|
|
114
|
+
logDebug("SUBAGENT", `context (${mergedContext.split("\n").length} lines):\n${mergedContext}`);
|
|
496
115
|
}
|
|
497
116
|
const systemPrompt = buildSystemPrompt(undefined, (mode || "base"));
|
|
498
117
|
const contextInfo = mergedContext
|
|
499
118
|
? `\n\nКонтекст проекта:\n${mergedContext}\nПосмотри package.json для списка пакетов. Используй read_file для чтения существующих файлов.`
|
|
500
|
-
:
|
|
119
|
+
: "";
|
|
501
120
|
const messages = [
|
|
502
|
-
{ role:
|
|
503
|
-
{ role:
|
|
121
|
+
{ role: "system", content: systemPrompt },
|
|
122
|
+
{ role: "user", content: task + contextInfo },
|
|
504
123
|
];
|
|
505
124
|
const ctx = new ContextManager(effectiveProvider.contextWindow);
|
|
506
125
|
const maxSteps = maxStepsParam ?? 20;
|
|
@@ -508,127 +127,61 @@ export async function runIsolatedLoop(provider, registry, task, existingFiles, c
|
|
|
508
127
|
for (let step = 0; step < maxSteps; step++) {
|
|
509
128
|
await ctx.updateTokens(messages);
|
|
510
129
|
if (step === 0 || step % 5 === 0) {
|
|
511
|
-
logDebug(
|
|
130
|
+
logDebug("SUBAGENT", `step ${step} | ${messages.length} msgs | ${ctx.totalTokens} tokens (${ctx.usagePercent.toFixed(1)}%)`);
|
|
512
131
|
}
|
|
513
132
|
if (ctx.needsCompact && messages.length > 6) {
|
|
514
|
-
logInfo(
|
|
133
|
+
logInfo("SUBAGENT", `COMPACT at step ${step} | ${ctx.totalTokens} tokens (${ctx.usagePercent.toFixed(1)}%)`);
|
|
515
134
|
messages.splice(0, messages.length, ...ctx.compact(messages));
|
|
516
135
|
await ctx.updateTokens(messages);
|
|
517
|
-
logInfo(
|
|
136
|
+
logInfo("SUBAGENT", `after compact | ${messages.length} msgs | ${ctx.totalTokens} tokens`);
|
|
518
137
|
}
|
|
519
138
|
// После tool call увеличиваем max_tokens чтобы модель успела сгенерировать контент после reasoning
|
|
520
|
-
const hasToolResult = messages.some(m => m.role ===
|
|
139
|
+
const hasToolResult = messages.some((m) => m.role === "tool");
|
|
521
140
|
const llmCfg = getLLMConfig();
|
|
522
141
|
const maxTokens = hasToolResult ? llmCfg.maxTokensAfterTool : undefined;
|
|
523
|
-
const { response } = await doChat(effectiveProvider, messages, renderer, renderer?.abortSignal, 1, false,
|
|
142
|
+
const { response } = await doChat(effectiveProvider, messages, renderer, renderer?.abortSignal, 1, false, "subagent", maxTokens);
|
|
524
143
|
if (!response.trim()) {
|
|
525
|
-
logDebug(
|
|
144
|
+
logDebug("SUBAGENT", `empty response at step ${step}, stopping`);
|
|
526
145
|
break;
|
|
527
146
|
}
|
|
528
|
-
messages.push({ role:
|
|
147
|
+
messages.push({ role: "assistant", content: response });
|
|
529
148
|
const { validCalls } = parseToolCalls(response);
|
|
530
149
|
if (validCalls.length === 0) {
|
|
531
|
-
logDebug(
|
|
150
|
+
logDebug("SUBAGENT", `no valid tool calls at step ${step}, stopping`);
|
|
532
151
|
break;
|
|
533
152
|
}
|
|
534
153
|
for (const call of validCalls) {
|
|
535
|
-
const { parsed, cmd } = call;
|
|
536
|
-
const isBash = parsed.name === 'bash';
|
|
537
|
-
const toolId = generateSubagentToolCallId();
|
|
538
154
|
toolCallCount++;
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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.' });
|
|
155
|
+
const { output, exitCode } = await executeToolCall(call, toolExecutor, cwd, createdFiles, errors, step);
|
|
156
|
+
messages.push({
|
|
157
|
+
role: "tool",
|
|
158
|
+
content: output.slice(0, 8000),
|
|
159
|
+
tool_call_id: generateSubagentToolCallId(),
|
|
160
|
+
});
|
|
161
|
+
messages.push({ role: "user", content: "Continue." });
|
|
598
162
|
}
|
|
599
163
|
}
|
|
600
|
-
const turns = messages.filter(m => m.role ===
|
|
164
|
+
const turns = messages.filter((m) => m.role === "assistant").length;
|
|
601
165
|
const elapsedMs = performance.now() - startTime;
|
|
602
166
|
await ctx.updateTokens(messages);
|
|
603
167
|
const finalTokens = ctx.totalTokens;
|
|
604
168
|
const finalUsage = ctx.usagePercent;
|
|
605
|
-
logInfo(
|
|
169
|
+
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
170
|
// Собираем список использованных инструментов из истории
|
|
607
|
-
const toolsUsed = [
|
|
608
|
-
|
|
609
|
-
.
|
|
171
|
+
const toolsUsed = [
|
|
172
|
+
...new Set(messages
|
|
173
|
+
.filter((m) => m.role === "assistant")
|
|
174
|
+
.flatMap((m) => {
|
|
610
175
|
const calls = m.content.matchAll(/<tool_call>\{"name":"([^"]+)"/g);
|
|
611
|
-
return [...calls].map(c => c[1]);
|
|
612
|
-
}))
|
|
613
|
-
|
|
176
|
+
return [...calls].map((c) => c[1]);
|
|
177
|
+
})),
|
|
178
|
+
];
|
|
179
|
+
const toolsStr = toolsUsed.length > 0 ? `\nTools used: ${toolsUsed.join(", ")}` : "";
|
|
614
180
|
const summary = createdFiles.length > 0
|
|
615
|
-
? `[Subagent done — ${createdFiles.length} file(s), ${turns} turns]\nFiles: ${createdFiles.join(
|
|
616
|
-
: `[Subagent done — no files, ${turns} turns]${toolsStr}${errors.length > 0 ? `\nErrors: ${errors.join(
|
|
181
|
+
? `[Subagent done — ${createdFiles.length} file(s), ${turns} turns]\nFiles: ${createdFiles.join(", ")}${toolsStr}${errors.length > 0 ? `\nErrors: ${errors.join("; ")}` : ""}`
|
|
182
|
+
: `[Subagent done — no files, ${turns} turns]${toolsStr}${errors.length > 0 ? `\nErrors: ${errors.join("; ")}` : ""}`;
|
|
617
183
|
return { createdFiles, errors, summary, turns };
|
|
618
184
|
}
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
};
|
|
185
|
+
import { subagentDefinition } from "../subagent/definition.js";
|
|
186
|
+
export { subagentDefinition };
|
|
634
187
|
//# sourceMappingURL=subagent.js.map
|