micro-models-agent 0.2.91 → 0.3.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 (86) hide show
  1. package/dist/agent/error-escalation.d.ts +18 -0
  2. package/dist/agent/error-escalation.d.ts.map +1 -0
  3. package/dist/agent/error-escalation.js +122 -0
  4. package/dist/agent/error-escalation.js.map +1 -0
  5. package/dist/agent/loop.d.ts +2 -1
  6. package/dist/agent/loop.d.ts.map +1 -1
  7. package/dist/agent/loop.js +22 -274
  8. package/dist/agent/loop.js.map +1 -1
  9. package/dist/agent/modes/applier.d.ts +2 -2
  10. package/dist/agent/modes/applier.d.ts.map +1 -1
  11. package/dist/agent/modes/registry.d.ts.map +1 -1
  12. package/dist/agent/modes/registry.js.map +1 -1
  13. package/dist/agent/modes/types.d.ts +12 -2
  14. package/dist/agent/modes/types.d.ts.map +1 -1
  15. package/dist/agent/react-loop.d.ts +4 -0
  16. package/dist/agent/react-loop.d.ts.map +1 -1
  17. package/dist/agent/react-loop.js +5 -23
  18. package/dist/agent/react-loop.js.map +1 -1
  19. package/dist/agent/system-prompt.d.ts +2 -1
  20. package/dist/agent/system-prompt.d.ts.map +1 -1
  21. package/dist/agent/system-prompt.js +23 -32
  22. package/dist/agent/system-prompt.js.map +1 -1
  23. package/dist/agent/tool-executor.d.ts +2 -0
  24. package/dist/agent/tool-executor.d.ts.map +1 -1
  25. package/dist/agent/tool-executor.js +1 -1
  26. package/dist/agent/tool-executor.js.map +1 -1
  27. package/dist/agent/write-enforcer.d.ts +11 -0
  28. package/dist/agent/write-enforcer.d.ts.map +1 -0
  29. package/dist/agent/write-enforcer.js +92 -0
  30. package/dist/agent/write-enforcer.js.map +1 -0
  31. package/dist/cli.d.ts.map +1 -1
  32. package/dist/cli.js +19 -16
  33. package/dist/cli.js.map +1 -1
  34. package/dist/config/config.d.ts.map +1 -1
  35. package/dist/config/config.js +4 -1
  36. package/dist/config/config.js.map +1 -1
  37. package/dist/config/types.d.ts +5 -0
  38. package/dist/config/types.d.ts.map +1 -1
  39. package/dist/logger.d.ts +1 -0
  40. package/dist/logger.d.ts.map +1 -1
  41. package/dist/logger.js +5 -0
  42. package/dist/logger.js.map +1 -1
  43. package/dist/repl/commands.js +0 -5
  44. package/dist/repl/commands.js.map +1 -1
  45. package/dist/repl/history.d.ts +2 -1
  46. package/dist/repl/history.d.ts.map +1 -1
  47. package/dist/repl/history.js.map +1 -1
  48. package/dist/repl/index.d.ts +2 -0
  49. package/dist/repl/index.d.ts.map +1 -1
  50. package/dist/repl/index.js +2 -2
  51. package/dist/repl/index.js.map +1 -1
  52. package/dist/skills/loader.d.ts +16 -0
  53. package/dist/skills/loader.d.ts.map +1 -1
  54. package/dist/skills/loader.js +110 -9
  55. package/dist/skills/loader.js.map +1 -1
  56. package/dist/tools/id-gen.d.ts +4 -0
  57. package/dist/tools/id-gen.d.ts.map +1 -0
  58. package/dist/tools/id-gen.js +12 -0
  59. package/dist/tools/id-gen.js.map +1 -0
  60. package/dist/tools/native/theme.d.ts.map +1 -1
  61. package/dist/tools/native/theme.js +0 -5
  62. package/dist/tools/native/theme.js.map +1 -1
  63. package/dist/tools/parse-tool-call.d.ts +11 -0
  64. package/dist/tools/parse-tool-call.d.ts.map +1 -0
  65. package/dist/tools/parse-tool-call.js +85 -0
  66. package/dist/tools/parse-tool-call.js.map +1 -0
  67. package/dist/tools/plan.d.ts +2 -0
  68. package/dist/tools/plan.d.ts.map +1 -1
  69. package/dist/tools/plan.js +8 -2
  70. package/dist/tools/plan.js.map +1 -1
  71. package/dist/tools/subagent.d.ts +0 -8
  72. package/dist/tools/subagent.d.ts.map +1 -1
  73. package/dist/tools/subagent.js +15 -94
  74. package/dist/tools/subagent.js.map +1 -1
  75. package/dist/tools/todo.d.ts +2 -0
  76. package/dist/tools/todo.d.ts.map +1 -1
  77. package/dist/tools/todo.js +8 -2
  78. package/dist/tools/todo.js.map +1 -1
  79. package/dist/ui/index.d.ts +1 -15
  80. package/dist/ui/index.d.ts.map +1 -1
  81. package/dist/ui/index.js +1 -12
  82. package/dist/ui/index.js.map +1 -1
  83. package/dist/ui/renderer.d.ts +2 -2
  84. package/dist/ui/renderer.d.ts.map +1 -1
  85. package/dist/ui/renderer.js +1 -1
  86. package/package.json +47 -51
@@ -0,0 +1,18 @@
1
+ import type { LLMProvider, ChatMessage } from '../llm/provider.js';
2
+ import type { ToolRegistry } from '../tools/registry.js';
3
+ import type { ToolExecutor } from './tool-executor.js';
4
+ import type { OutputRenderer } from '../ui/renderer.js';
5
+ /**
6
+ * Управляет эскалацией ошибок инструментов: отслеживание,
7
+ * вызов specialist'а, переключение стратегии и блокировка.
8
+ */
9
+ export declare class ErrorEscalator {
10
+ consecutiveToolErrors: Map<string, number>;
11
+ totalToolErrors: number;
12
+ specialistSpawnedFor: string | null;
13
+ errorStrategy: 'normal' | 'subagent-hint' | 'force-subagent';
14
+ resetForTool(toolName: string): void;
15
+ reset(): void;
16
+ handleToolError(toolName: string, toolOutput: string, toolArgs: Record<string, unknown>, toolId: string, messages: ChatMessage[], createdFiles: string[], enrichedPrompt: string, renderer: OutputRenderer | undefined, quiet: boolean, provider: LLMProvider, registry: ToolRegistry, toolExecutor: ToolExecutor, modeConfig: any, llmCfg: any): Promise<'continue' | 'block'>;
17
+ }
18
+ //# sourceMappingURL=error-escalation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-escalation.d.ts","sourceRoot":"","sources":["../../src/agent/error-escalation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAoDxD;;;GAGG;AACH,qBAAa,cAAc;IACzB,qBAAqB,sBAA6B;IAClD,eAAe,SAAK;IACpB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC3C,aAAa,EAAE,QAAQ,GAAG,eAAe,GAAG,gBAAgB,CAAY;IAExE,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAOpC,KAAK,IAAI,IAAI;IAOP,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,WAAW,EAAE,EACvB,YAAY,EAAE,MAAM,EAAE,EACtB,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,WAAW,EACrB,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,GAAG,EACf,MAAM,EAAE,GAAG,GACV,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC;CAyDjC"}
@@ -0,0 +1,122 @@
1
+ import { generateToolCallId } from '../tools/id-gen.js';
2
+ import { logInfo, logError } from '../logger.js';
3
+ import { loadConfig, getSpecialistConfig } from '../config/config.js';
4
+ import { buildSpecialistTask, buildSubagentContext, isTrivialError, runIsolatedLoop } from '../tools/subagent.js';
5
+ import { SubagentRenderer } from '../ui/subagent-renderer.js';
6
+ import { truncateOutput } from './tool-executor.js';
7
+ /**
8
+ * Запускает specialist'а для решения повторяющейся ошибки.
9
+ */
10
+ async function spawnSpecialist(p) {
11
+ if (!p.quiet) {
12
+ p.renderer?.writeBracketTag('specialist', `solving '${p.errorOutput.slice(0, 60).replace(/\n/g, ' ')}' error...`);
13
+ }
14
+ const task = buildSpecialistTask(p.toolName, p.toolArgs, p.errorOutput, p.enrichedPrompt, p.errorCount);
15
+ const ctx = buildSubagentContext(process.cwd());
16
+ const sub = new SubagentRenderer();
17
+ try {
18
+ logInfo('LOOP', `spawning specialist for ${p.toolName} (attempt #${p.errorCount})`);
19
+ const r = await runIsolatedLoop(p.provider, p.registry, task, ctx, process.cwd(), p.quiet, sub, p.toolExecutor, p.modeConfig.subagentMode, p.llmCfg.toolModel, p.specialistCfg.maxSteps);
20
+ for (let i = p.messages.length - 1; i >= 0; i--) {
21
+ if (p.messages[i].role === 'tool' && p.messages[i].tool_call_id === p.toolId) {
22
+ const text = r.errors.length > 0
23
+ ? `[Specialist attempted to fix "${p.toolName}" but failed:\n${r.errors.join('\n')}\n${r.summary}]\nTry a different approach.`
24
+ : `[Specialist resolved "${p.toolName}" error.\n${r.summary}]\nContinue with the task.`;
25
+ p.messages[i].content = truncateOutput(p.toolName, text, 0);
26
+ break;
27
+ }
28
+ }
29
+ for (const f of r.createdFiles) {
30
+ if (!p.createdFiles.includes(f))
31
+ p.createdFiles.push(f);
32
+ }
33
+ const buf = sub.getBuffer();
34
+ if (!p.quiet && p.renderer?.writeToolBuffer)
35
+ p.renderer.writeToolBuffer({ lines: buf.lines, toolCalls: buf.toolCalls });
36
+ logInfo('LOOP', `specialist done | files: ${r.createdFiles.length} | turns: ${r.turns} | errors: ${r.errors.length}`);
37
+ return true;
38
+ }
39
+ catch (e) {
40
+ logError('LOOP', `specialist error: ${e.message}`);
41
+ if (!p.quiet)
42
+ p.renderer?.writeError(` [specialist failed: ${e.message}]`);
43
+ return false;
44
+ }
45
+ }
46
+ /**
47
+ * Управляет эскалацией ошибок инструментов: отслеживание,
48
+ * вызов specialist'а, переключение стратегии и блокировка.
49
+ */
50
+ export class ErrorEscalator {
51
+ consecutiveToolErrors = new Map();
52
+ totalToolErrors = 0;
53
+ specialistSpawnedFor = null;
54
+ errorStrategy = 'normal';
55
+ resetForTool(toolName) {
56
+ for (const [key] of this.consecutiveToolErrors) {
57
+ if (key.startsWith(toolName + '::'))
58
+ this.consecutiveToolErrors.delete(key);
59
+ }
60
+ this.specialistSpawnedFor = null;
61
+ }
62
+ reset() {
63
+ this.consecutiveToolErrors.clear();
64
+ this.totalToolErrors = 0;
65
+ this.specialistSpawnedFor = null;
66
+ this.errorStrategy = 'normal';
67
+ }
68
+ async handleToolError(toolName, toolOutput, toolArgs, toolId, messages, createdFiles, enrichedPrompt, renderer, quiet, provider, registry, toolExecutor, modeConfig, llmCfg) {
69
+ this.totalToolErrors++;
70
+ const errKey = `${toolName}::${toolOutput.slice(0, 100).replace(/[A-Z]:\\[^\s"']+/gi, '<PATH>').replace(/\/[^\s"']+/g, '<PATH>')}`;
71
+ const prevCount = this.consecutiveToolErrors.get(errKey) ?? 0;
72
+ this.consecutiveToolErrors.set(errKey, prevCount + 1);
73
+ const errorCount = prevCount + 1;
74
+ const config = loadConfig();
75
+ const specialistCfg = getSpecialistConfig(config);
76
+ if (specialistCfg.enabled &&
77
+ errorCount === specialistCfg.triggerThreshold &&
78
+ this.specialistSpawnedFor !== errKey &&
79
+ !isTrivialError(toolName, toolOutput) &&
80
+ toolOutput.length >= specialistCfg.minErrorLength) {
81
+ this.specialistSpawnedFor = errKey;
82
+ const spawned = await spawnSpecialist({
83
+ provider, registry, toolExecutor, modeConfig, llmCfg, specialistCfg,
84
+ quiet, renderer, messages, toolId, createdFiles,
85
+ toolName, toolArgs,
86
+ errorOutput: toolOutput, errorCount, enrichedPrompt,
87
+ });
88
+ if (spawned)
89
+ return 'continue';
90
+ }
91
+ if (errorCount > specialistCfg.triggerThreshold) {
92
+ let webHint = '';
93
+ if (toolName === 'web_browse' && String(toolArgs?.action) === 'evaluate') {
94
+ webHint = ' For web scraping: use web_browse(action:"discover") to find real CSS selectors, or web_browse(action:"get_links") to see all links, or web_browse(action:"extract", format:"html") to inspect the page structure.';
95
+ }
96
+ else if (toolName === 'web_browse' && String(toolArgs?.action) === 'navigate' && toolOutput.includes('Status: 40')) {
97
+ webHint = ' Page returned HTTP error. Try navigating to the site homepage first, then use web_browse(action:"get_links") or web_browse(action:"discover") to explore available categories and sections instead of guessing URLs.';
98
+ }
99
+ const msg = errorCount === specialistCfg.triggerThreshold + 1
100
+ ? `[System: Tool "${toolName}" failed with the SAME error ${errorCount} times. Read the error message carefully. The input is likely wrong — try a completely different approach, or use subagent for a fresh context.]${webHint}`
101
+ : `[System: Tool "${toolName}" failed ${errorCount} times with the same error. STOP. Do NOT retry the same call. Use subagent(task="...") for a fresh context, or tell the user you're stuck.]${webHint}`;
102
+ messages.push({ role: 'user', content: msg });
103
+ if (!quiet)
104
+ renderer?.writeWarning(` [error escalation: ${toolName} failed ${errorCount} times]`);
105
+ }
106
+ if (this.totalToolErrors >= 5 && this.errorStrategy === 'normal') {
107
+ this.errorStrategy = 'subagent-hint';
108
+ messages.push({ role: 'user', content: `[System: ${this.totalToolErrors} tool errors. Use subagent(task="...") for a fresh context.]` });
109
+ if (!quiet)
110
+ renderer?.writeWarning(` [strategy: subagent-hint after ${this.totalToolErrors} errors]`);
111
+ }
112
+ if (errorCount >= 4) {
113
+ const blockId = generateToolCallId();
114
+ messages.push({ role: 'tool', content: `BLOCKED: Tool "${toolName}" failed ${errorCount} times. Temporarily blocked.`, tool_call_id: blockId });
115
+ if (!quiet)
116
+ renderer?.writeWarning(` [blocked ${toolName}: too many repeated errors]`);
117
+ return 'block';
118
+ }
119
+ return 'block';
120
+ }
121
+ }
122
+ //# sourceMappingURL=error-escalation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-escalation.js","sourceRoot":"","sources":["../../src/agent/error-escalation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAClH,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAmBpD;;GAEG;AACH,KAAK,UAAU,eAAe,CAAC,CAAmB;IAChD,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;QACb,CAAC,CAAC,QAAQ,EAAE,eAAe,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;IACpH,CAAC;IACD,MAAM,IAAI,GAAG,mBAAmB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IACxG,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,IAAI,gBAAgB,EAAE,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC,QAAQ,cAAc,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC;QACpF,MAAM,CAAC,GAAG,MAAM,eAAe,CAC7B,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAChD,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,UAAU,CAAC,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,aAAa,CAAC,QAAQ,CACtG,CAAC;QACF,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAChD,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;gBAC7E,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;oBAC9B,CAAC,CAAC,iCAAiC,CAAC,CAAC,QAAQ,kBAAkB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,8BAA8B;oBAC9H,CAAC,CAAC,yBAAyB,CAAC,CAAC,QAAQ,aAAa,CAAC,CAAC,OAAO,4BAA4B,CAAC;gBAC1F,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,cAAc,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;gBAC5D,MAAM;YACR,CAAC;QACH,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,CAAC;YAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAAE,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;QAC5F,MAAM,GAAG,GAAG,GAAG,CAAC,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,QAAQ,EAAE,eAAe;YAAE,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;QACxH,OAAO,CAAC,MAAM,EAAE,4BAA4B,CAAC,CAAC,YAAY,CAAC,MAAM,aAAa,CAAC,CAAC,KAAK,cAAc,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QACtH,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACnD,IAAI,CAAC,CAAC,CAAC,KAAK;YAAE,CAAC,CAAC,QAAQ,EAAE,UAAU,CAAC,yBAAyB,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC;QAC5E,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,cAAc;IACzB,qBAAqB,GAAG,IAAI,GAAG,EAAkB,CAAC;IAClD,eAAe,GAAG,CAAC,CAAC;IACpB,oBAAoB,GAAkB,IAAI,CAAC;IAC3C,aAAa,GAAkD,QAAQ,CAAC;IAExE,YAAY,CAAC,QAAgB;QAC3B,KAAK,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC/C,IAAI,GAAG,CAAC,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC;gBAAE,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9E,CAAC;QACD,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IACnC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,qBAAqB,CAAC,KAAK,EAAE,CAAC;QACnC,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,QAAgB,EAChB,UAAkB,EAClB,QAAiC,EACjC,MAAc,EACd,QAAuB,EACvB,YAAsB,EACtB,cAAsB,EACtB,QAAoC,EACpC,KAAc,EACd,QAAqB,EACrB,QAAsB,EACtB,YAA0B,EAC1B,UAAe,EACf,MAAW;QAEX,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,GAAG,QAAQ,KAAK,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,oBAAoB,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,QAAQ,CAAC,EAAE,CAAC;QACnI,MAAM,SAAS,GAAG,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC9D,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC;QACtD,MAAM,UAAU,GAAG,SAAS,GAAG,CAAC,CAAC;QAEjC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,aAAa,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;QAElD,IACE,aAAa,CAAC,OAAO;YACrB,UAAU,KAAK,aAAa,CAAC,gBAAgB;YAC7C,IAAI,CAAC,oBAAoB,KAAK,MAAM;YACpC,CAAC,cAAc,CAAC,QAAQ,EAAE,UAAU,CAAC;YACrC,UAAU,CAAC,MAAM,IAAI,aAAa,CAAC,cAAc,EACjD,CAAC;YACD,IAAI,CAAC,oBAAoB,GAAG,MAAM,CAAC;YACnC,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC;gBACpC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa;gBACnE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY;gBAC/C,QAAQ,EAAE,QAAQ;gBAClB,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc;aACpD,CAAC,CAAC;YACH,IAAI,OAAO;gBAAE,OAAO,UAAU,CAAC;QACjC,CAAC;QAED,IAAI,UAAU,GAAG,aAAa,CAAC,gBAAgB,EAAE,CAAC;YAChD,IAAI,OAAO,GAAG,EAAE,CAAC;YACjB,IAAI,QAAQ,KAAK,YAAY,IAAI,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC;gBACzE,OAAO,GAAG,oNAAoN,CAAC;YACjO,CAAC;iBAAM,IAAI,QAAQ,KAAK,YAAY,IAAI,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,UAAU,IAAI,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;gBACrH,OAAO,GAAG,uNAAuN,CAAC;YACpO,CAAC;YAED,MAAM,GAAG,GAAG,UAAU,KAAK,aAAa,CAAC,gBAAgB,GAAG,CAAC;gBAC3D,CAAC,CAAC,kBAAkB,QAAQ,gCAAgC,UAAU,mJAAmJ,OAAO,EAAE;gBAClO,CAAC,CAAC,kBAAkB,QAAQ,YAAY,UAAU,8IAA8I,OAAO,EAAE,CAAC;YAC5M,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;YAC9C,IAAI,CAAC,KAAK;gBAAE,QAAQ,EAAE,YAAY,CAAC,wBAAwB,QAAQ,WAAW,UAAU,SAAS,CAAC,CAAC;QACrG,CAAC;QAED,IAAI,IAAI,CAAC,eAAe,IAAI,CAAC,IAAI,IAAI,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;YACjE,IAAI,CAAC,aAAa,GAAG,eAAe,CAAC;YACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,IAAI,CAAC,eAAe,8DAA8D,EAAE,CAAC,CAAC;YACzI,IAAI,CAAC,KAAK;gBAAE,QAAQ,EAAE,YAAY,CAAC,oCAAoC,IAAI,CAAC,eAAe,UAAU,CAAC,CAAC;QACzG,CAAC;QAED,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;YACpB,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;YACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,QAAQ,YAAY,UAAU,8BAA8B,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;YAChJ,IAAI,CAAC,KAAK;gBAAE,QAAQ,EAAE,YAAY,CAAC,cAAc,QAAQ,6BAA6B,CAAC,CAAC;YACxF,OAAO,OAAO,CAAC;QACjB,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;CACF"}
@@ -2,7 +2,7 @@ import type { LLMProvider, ChatMessage } from '../llm/provider.js';
2
2
  import { ToolRegistry } from '../tools/registry.js';
3
3
  import { ContextManager } from './context.js';
4
4
  import type { OutputRenderer } from '../ui/renderer.js';
5
- import type { AgentMode } from '../config/types.js';
5
+ import type { AgentMode, RuntimeFlags } from '../config/types.js';
6
6
  /**
7
7
  * Результат выполнения цикла агента.
8
8
  * Содержит всю историю сообщений и опциональный pending-вопрос
@@ -41,5 +41,6 @@ export declare function runAgentLoop(provider: LLMProvider, registry: ToolRegist
41
41
  quiet?: boolean;
42
42
  showThinking?: boolean;
43
43
  mode?: AgentMode;
44
+ flags?: RuntimeFlags;
44
45
  }): Promise<AgentLoopResult>;
45
46
  //# sourceMappingURL=loop.d.ts.map
@@ -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;AAGpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAY9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAiHpD;;;;GAIG;AACH,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;AAmED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,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,CA+iC1B"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAIpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAa9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAgFlE;;;;GAIG;AACH,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;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,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,CAAC;IAAC,KAAK,CAAC,EAAE,YAAY,CAAA;CAAE,GAC5F,OAAO,CAAC,eAAe,CAAC,CA64B1B"}
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { jsonrepair } from 'jsonrepair';
2
+ import { join, isAbsolute, relative } from 'node:path';
3
+ import { parseToolCalls } from '../tools/parse-tool-call.js';
4
4
  import { parseFileRefs } from '../file-loader.js';
5
5
  import { ContextManager } from './context.js';
6
6
  import { autoLearnFromError } from './memory.js';
@@ -11,13 +11,16 @@ import { loadSkill, loadSkillsFromRefs, enforceSkillBudget, preloadSkillsByKeywo
11
11
  import { runPreToolUseHooks, runPostToolUseHooks } from '../tools/hooks.js';
12
12
  import { isMCPCommand, parseMCPCommand, callMCPTool } from '../tools/mcp.js';
13
13
  import { showPlan, getPlanPendingCount, clearPlan, getNextPendingStep, updatePlanStep, extractDirFromStepTitle } from '../tools/plan.js';
14
+ import { generateToolCallId } from '../tools/id-gen.js';
14
15
  import { checkFileSyntax } from '../tools/syntax-check.js';
15
- import { loadConfig, getLLMConfig, getSpecialistConfig, getGroundingConfig } from '../config/config.js';
16
+ import { loadConfig, getLLMConfig, getGroundingConfig } from '../config/config.js';
16
17
  import { buildSystemPrompt } from './system-prompt.js';
17
18
  import { logInfo, logWarn, logError, logDebug, logToolCall, setStatusWriter, writeStatus } from '../logger.js';
18
- import { buildSubagentContext, buildSpecialistTask, isTrivialError, runIsolatedLoop, extractAllToolCalls, validateToolCall, doChat } from '../tools/subagent.js';
19
+ import { buildSubagentContext, runIsolatedLoop, doChat } from '../tools/subagent.js';
19
20
  import { SubagentRenderer } from '../ui/subagent-renderer.js';
20
21
  import { ToolExecutor } from './tool-executor.js';
22
+ import { ErrorEscalator } from './error-escalation.js';
23
+ import { WriteEnforcer } from './write-enforcer.js';
21
24
  import { PluginManager } from '../plugins/manager.js';
22
25
  import { loadAllPluginsWithInfo } from '../plugins/loader.js';
23
26
  import { handleToolAfter as lintHandleToolAfter } from '../plugins/builtin/lint-on-write/index.js';
@@ -46,40 +49,7 @@ import { treeHandler } from '../tools/fs/tree.js';
46
49
  import { webSearchHandler } from '../tools/web-search.js';
47
50
  import { webFetchHandler } from '../tools/web-fetch.js';
48
51
  import { webBrowseHandler } from '../tools/web-browse.js';
49
- /**
50
- * Умная обрезка tool-результатов для экономии токенов в контексте.
51
- *
52
- * Полные результаты сохраняются в файл сессии, в контексте — только
53
- * достаточно информации для модели чтобы продолжить работу.
54
- *
55
- * Лимиты подобраны эмпирически: достаточно для маленьких моделей
56
- * понять что произошло, но не занимают весь контекст.
57
- */
58
- function truncateToolOutput(toolName, output, exitCode) {
59
- // Ошибки обрезаем мягче — модели нужен контекст для диагностики
60
- if (exitCode !== 0) {
61
- return output.length > 3000 ? output.slice(0, 3000) + '\n...[truncated]' : output;
62
- }
63
- // Лимиты для успешных операций (в символах)
64
- const LIMITS = {
65
- read_file: 2000, // модель может ссылаться на строки
66
- write_file: 200, // достаточно "Written X bytes"
67
- edit_file: 300, // нужно знать что изменилось
68
- bash: 1500, // build output, git status
69
- list_dir: 1500, // структура обычно компактна
70
- glob_search: 1000, // список файлов
71
- grep_search: 1500, // результаты поиска
72
- tree: 1500, // дерево файлов
73
- subagent: 500, // краткое резюме от субагента
74
- web_search: 1000, // результаты поиска
75
- web_fetch: 1500, // контент страницы
76
- web_browse: 2000, // browser interaction results
77
- };
78
- const limit = LIMITS[toolName] ?? 800;
79
- if (output.length <= limit)
80
- return output;
81
- return output.slice(0, limit) + '\n...[truncated]';
82
- }
52
+ import { truncateOutput } from './tool-executor.js';
83
53
  /**
84
54
  * Основной цикл ReAct-агента.
85
55
  *
@@ -121,58 +91,6 @@ function extractToolCallFallback(response) {
121
91
  }
122
92
  return null;
123
93
  }
124
- /**
125
- * Запускает specialist'а для решения повторяющейся ошибки.
126
- *
127
- * Specialist — это изолированный суб-агент со свежим контекстом,
128
- * который получает задачу "диагностируй и исправь ошибку инструмента X".
129
- *
130
- * Алгоритм:
131
- * 1. Формирует задачу через buildSpecialistTask()
132
- * 2. Собирает контекст проекта через buildSubagentContext()
133
- * 3. Запускает runIsolatedLoop() с ограничением maxSteps
134
- * 4. Заменяет результат ошибки в messages[] на результат specialist'а
135
- * 5. Трекает созданные specialist'ом файлы
136
- *
137
- * @returns true если specialist отработал (основной цикл должен continue),
138
- * false если specialist упал (ошибка остаётся в messages)
139
- */
140
- async function spawnSpecialist(p) {
141
- if (!p.quiet) {
142
- p.renderer?.writeBracketTag('specialist', `solving '${p.errorOutput.slice(0, 60).replace(/\n/g, ' ')}' error...`);
143
- }
144
- const task = buildSpecialistTask(p.toolName, p.toolArgs, p.errorOutput, p.enrichedPrompt, p.errorCount);
145
- const ctx = buildSubagentContext(process.cwd());
146
- const sub = new SubagentRenderer();
147
- try {
148
- logInfo('LOOP', `spawning specialist for ${p.toolName} (attempt #${p.errorCount})`);
149
- const r = await runIsolatedLoop(p.provider, p.registry, task, ctx, process.cwd(), p.quiet, sub, p.toolExecutor, p.modeConfig.subagentMode, p.llmCfg.toolModel, p.specialistCfg.maxSteps);
150
- for (let i = p.messages.length - 1; i >= 0; i--) {
151
- if (p.messages[i].role === 'tool' && p.messages[i].tool_call_id === p.toolId) {
152
- const text = r.errors.length > 0
153
- ? `[Specialist attempted to fix "${p.toolName}" but failed:\n${r.errors.join('\n')}\n${r.summary}]\nTry a different approach.`
154
- : `[Specialist resolved "${p.toolName}" error.\n${r.summary}]\nContinue with the task.`;
155
- p.messages[i].content = truncateToolOutput(p.toolName, text, 0);
156
- break;
157
- }
158
- }
159
- for (const f of r.createdFiles) {
160
- if (!p.createdFiles.includes(f))
161
- p.createdFiles.push(f);
162
- }
163
- const buf = sub.getBuffer();
164
- if (!p.quiet && p.renderer?.writeToolBuffer)
165
- p.renderer.writeToolBuffer({ lines: buf.lines, toolCalls: buf.toolCalls });
166
- logInfo('LOOP', `specialist done | files: ${r.createdFiles.length} | turns: ${r.turns} | errors: ${r.errors.length}`);
167
- return true;
168
- }
169
- catch (e) {
170
- logError('LOOP', `specialist error: ${e.message}`);
171
- if (!p.quiet)
172
- p.renderer?.writeError(` [specialist failed: ${e.message}]`);
173
- return false;
174
- }
175
- }
176
94
  /**
177
95
  * Запускает основной цикл ReAct-агента.
178
96
  *
@@ -315,7 +233,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
315
233
  const pluginSections = await pluginManager.getPromptSections();
316
234
  // Сборка system prompt (включает инструкции, информацию о проекте, скилы, память, плагины)
317
235
  const sysStart = performance.now();
318
- const systemPrompt = buildSystemPrompt(loadedSkills.length > 0 ? loadedSkills : undefined, options?.mode, pluginSections);
236
+ const systemPrompt = buildSystemPrompt(loadedSkills.length > 0 ? loadedSkills : undefined, options?.mode, pluginSections, options?.flags);
319
237
  const sysElapsed = ((performance.now() - sysStart) / 1000).toFixed(1);
320
238
  writeStatus(`Done (${sysElapsed}s)`);
321
239
  setStatusWriter(null);
@@ -332,15 +250,6 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
332
250
  let aborted = false;
333
251
  let continuationNudges = 0; // счётчик подсказок "продолжай работать"
334
252
  let consecutiveMissingGrounding = 0; // счётчик пропущенных grounding-маркеров подряд
335
- let directWriteStreak = 0; // количество прямых write_file без subagent
336
- /** Трекер повторяющихся tool errors: { normalizedError → count } */
337
- const consecutiveToolErrors = new Map();
338
- /** Общий счётчик ошибок за сессию */
339
- let totalToolErrors = 0;
340
- /** Стратегия обработки ошибок: 'normal' | 'subagent-hint' | 'force-subagent' */
341
- let errorStrategy = 'normal';
342
- /** Флаг: specialist уже был запущен для текущей ошибки (чтобы не дублировать) */
343
- let specialistSpawnedFor = null;
344
253
  /** Счётчик бесполезных действий (list_dir, glob_search, todo list без результата) */
345
254
  let uselessActionCount = 0;
346
255
  /** История последних чтений файлов для.detect повторных чтений */
@@ -360,6 +269,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
360
269
  fileInfoHandler, globSearchHandler, grepSearchHandler, treeHandler,
361
270
  webSearchHandler, webFetchHandler, webBrowseHandler,
362
271
  ]);
272
+ const errorEscalator = new ErrorEscalator();
273
+ const writeEnforcer = new WriteEnforcer();
363
274
  // Reset abort signal for this run
364
275
  if (renderer?.abortSignal === undefined) {
365
276
  // Renderer doesn't support abort — fine
@@ -460,7 +371,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
460
371
  }
461
372
  }
462
373
  renderer?.writeInfo(` Auto-loaded ${loadedSkills.filter(s => newSkillNames.includes(s.name)).length} skill(s): ${newSkillNames.join(', ')}`);
463
- messages[0] = { role: 'system', content: buildSystemPrompt(loadedSkills, options?.mode, pluginSections) };
374
+ messages[0] = { role: 'system', content: buildSystemPrompt(loadedSkills, options?.mode, pluginSections, options?.flags) };
464
375
  // Не удаляем ответ ассистента — skills загружаются для следующих шагов цикла.
465
376
  // Модель увидит обновлённый system prompt на следующей итерации и сможет
466
377
  // использовать загруженные скилы без retry (экономит ~один LLM-запрос).
@@ -509,42 +420,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
509
420
  }
510
421
  consecutiveEmpty = 0;
511
422
  messages.push({ role: 'assistant', content: response });
512
- // ─── Парсинг tool calls ─────────────────────────────
513
- // Извлекаем JSON из тегов <tool_call>...</tool_call> и валидируем
514
- const allJsonRaw = extractAllToolCalls(response);
515
- const validCalls = [];
516
- const invalidCalls = [];
517
- for (const jsonRaw of allJsonRaw) {
518
- let p = null;
519
- try {
520
- p = JSON.parse(jsonrepair(jsonRaw));
521
- }
522
- catch {
523
- try {
524
- p = JSON.parse(jsonRaw);
525
- }
526
- catch { /* continue */ }
527
- }
528
- if (p) {
529
- const v = validateToolCall(p);
530
- if (v.valid) {
531
- validCalls.push({ parsed: p, cmd: v.cmd });
532
- }
533
- else if (v.error) {
534
- invalidCalls.push(v.error);
535
- }
536
- }
537
- }
538
- // ─── Fallback: regex-извлечение при невалидном JSON ───
539
- if (validCalls.length === 0) {
540
- const fallback = extractToolCallFallback(response);
541
- if (fallback) {
542
- validCalls.push({ parsed: { name: 'bash', arguments: { cmd: fallback.cmd } }, cmd: fallback.cmd });
543
- if (!quiet) {
544
- renderer?.writeBracketTag('fallback', 'regex extracted command');
545
- }
546
- }
547
- }
423
+ const { validCalls, invalidCalls } = parseToolCalls(response);
548
424
  if (validCalls.length === 0) {
549
425
  if (invalidCalls.length > 0) {
550
426
  // If the response has substantial text outside tool_call tags,
@@ -764,81 +640,9 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
764
640
  // ─── Enforce: лимиты на запись файлов ────────────────
765
641
  // Отслеживание повторных чтений одного файла для консолидации
766
642
  let lastReadFile = null;
767
- // Блокировка прямых write_file когда есть план с 3+ шагами — force subagent
768
- const planSteps = showPlan();
769
- const hasMultiStepPlan = planEnforcement && !planSteps.startsWith('No active plan') && (planSteps.match(/\n/g) || []).length >= 3;
770
- if (hasMultiStepPlan) {
771
- const directWrites = validCalls.filter(c => c.parsed.name === 'write_file' || c.parsed.name === 'edit_file');
772
- const hasSubagent = validCalls.some(c => c.parsed.name === 'subagent');
773
- const hasBash = validCalls.some(c => c.parsed.name === 'bash');
774
- if (directWrites.length > 0 && !hasSubagent) {
775
- // Block direct writes — model must use subagent
776
- for (const wc of directWrites) {
777
- const idx = validCalls.indexOf(wc);
778
- if (idx !== -1)
779
- validCalls.splice(idx, 1);
780
- const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
781
- messages.push({
782
- role: 'tool',
783
- content: `BLOCKED: Multi-step plan active. Use subagent for each step. Example: subagent({task:"Create file X", existingFiles:"Y.ts"}). After subagent completes, mark step done: plan({action:"done", step:N})`,
784
- tool_call_id: blockId,
785
- });
786
- }
787
- if (!quiet) {
788
- renderer?.writeWarning(` [blocked ${directWrites.length} direct writes — use subagent for multi-step plans]`);
789
- }
790
- }
791
- // Allow bash for verification (tsc --noEmit, npm run build) — don't block these
792
- }
793
- // ESCALATION: block write_file when creating N+ files without subagent (no plan case)
794
- if (directWriteStreakMax < Infinity && !hasMultiStepPlan) {
795
- const hasWrite = validCalls.some(c => c.parsed.name === 'write_file');
796
- const hasSubagentNow = validCalls.some(c => c.parsed.name === 'subagent');
797
- if (hasSubagentNow) {
798
- directWriteStreak = 0;
799
- }
800
- else if (hasWrite) {
801
- const writeCount = validCalls.filter(c => c.parsed.name === 'write_file').length;
802
- directWriteStreak += writeCount;
803
- if (directWriteStreak >= directWriteStreakMax) {
804
- const directWrites = validCalls.filter(c => c.parsed.name === 'write_file');
805
- for (const wc of directWrites) {
806
- const idx = validCalls.indexOf(wc);
807
- if (idx !== -1)
808
- validCalls.splice(idx, 1);
809
- }
810
- if (directWrites.length > 0) {
811
- const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
812
- messages.push({
813
- role: 'tool',
814
- content: `BLOCKED: You're creating file #${directWriteStreak} without subagent. For 3+ new files, use subagent for each one. Example: subagent("Create file X. Context: Y already exists.")`,
815
- tool_call_id: blockId,
816
- });
817
- }
818
- if (!quiet) {
819
- renderer?.writeWarning(` [blocked ${directWrites.length} writes: ${directWriteStreak} files without subagent — use subagent for multi-file projects]`);
820
- }
821
- }
822
- }
823
- }
824
- // Блокировка нескольких записей файлов за один ответ — максимум одна
825
- const writeCalls = validCalls.filter(c => c.parsed.name === 'write_file' || c.parsed.name === 'edit_file');
826
- if (writeCalls.length > 1) {
827
- // Keep only the first write call, block the rest
828
- const blocked = writeCalls.slice(1);
829
- validCalls.splice(validCalls.indexOf(blocked[0]), blocked.length);
830
- for (const bc of blocked) {
831
- const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
832
- messages.push({
833
- role: 'tool',
834
- 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.`,
835
- tool_call_id: blockId,
836
- });
837
- }
838
- if (!quiet) {
839
- renderer?.writeWarning(` [blocked ${blocked.length} extra file writes — one at a time]`);
840
- }
841
- }
643
+ writeEnforcer.enforcePlan(validCalls, messages, planEnforcement, quiet, renderer);
644
+ writeEnforcer.enforceWriteStreak(validCalls, messages, directWriteStreakMax, quiet, renderer);
645
+ writeEnforcer.enforceSingleWrite(validCalls, messages, quiet, renderer);
842
646
  // ─── Исполнение tool calls ──────────────────────────
843
647
  for (const call of validCalls) {
844
648
  const { parsed, cmd } = call;
@@ -846,7 +650,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
846
650
  const preview = isBash
847
651
  ? (cmd.length > 200 ? cmd.slice(0, 200) + '...' : cmd)
848
652
  : JSON.stringify(parsed.arguments);
849
- const toolId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
653
+ const toolId = generateToolCallId();
850
654
  logToolCall(parsed.name, preview);
851
655
  if (!quiet) {
852
656
  renderer?.writeToolHeader(parsed.name);
@@ -1051,7 +855,6 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
1051
855
  ? cmd.match(/-Path\s+['"]?([^'"]+\.(?:ts|tsx|js|jsx|json))['"]?/i)?.[1]?.replace(/\\/g, '/')
1052
856
  : String(parsed.arguments.path ?? '');
1053
857
  if (rawPath && /\.(ts|tsx|js|jsx|json)$/.test(rawPath)) {
1054
- const { isAbsolute, relative } = await import('node:path');
1055
858
  const filePath = isAbsolute(rawPath) ? relative(process.cwd(), rawPath).replace(/\\/g, '/') : rawPath;
1056
859
  const syntaxResult = checkFileSyntax(filePath, process.cwd());
1057
860
  if (syntaxResult !== 'OK') {
@@ -1120,72 +923,17 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
1120
923
  }
1121
924
  messages.push({
1122
925
  role: 'tool',
1123
- content: truncateToolOutput(parsed.name, toolOutput, toolExitCode),
926
+ content: truncateOutput(parsed.name, toolOutput, toolExitCode),
1124
927
  tool_call_id: toolId,
1125
928
  });
1126
929
  // ─── Error escalation + specialist auto-delegation ──────────
1127
930
  if (toolExitCode !== 0 && toolOutput) {
1128
- totalToolErrors++;
1129
- const errKey = `${parsed.name}::${toolOutput.slice(0, 100).replace(/[A-Z]:\\[^\s"']+/gi, '<PATH>').replace(/\/[^\s"']+/g, '<PATH>')}`;
1130
- const prevCount = consecutiveToolErrors.get(errKey) ?? 0;
1131
- consecutiveToolErrors.set(errKey, prevCount + 1);
1132
- const errorCount = prevCount + 1;
1133
- // ── Specialist: авто-делегация при повторяющихся ошибках ──
1134
- const specialistCfg = getSpecialistConfig(config);
1135
- if (specialistCfg.enabled &&
1136
- errorCount === specialistCfg.triggerThreshold &&
1137
- specialistSpawnedFor !== errKey &&
1138
- !isTrivialError(parsed.name, toolOutput) &&
1139
- toolOutput.length >= specialistCfg.minErrorLength) {
1140
- specialistSpawnedFor = errKey;
1141
- const spawned = await spawnSpecialist({
1142
- provider, registry, toolExecutor, modeConfig, llmCfg, specialistCfg,
1143
- quiet, renderer, messages, toolId, createdFiles,
1144
- toolName: parsed.name, toolArgs: parsed.arguments,
1145
- errorOutput: toolOutput, errorCount, enrichedPrompt,
1146
- });
1147
- if (spawned)
1148
- continue;
1149
- }
1150
- // ── Escalation: hint / strategy switch / block ──
1151
- if (errorCount > specialistCfg.triggerThreshold) {
1152
- // Веб-специфичные подсказки
1153
- let webHint = '';
1154
- if (parsed.name === 'web_browse' && String(parsed.arguments?.action) === 'evaluate') {
1155
- webHint = ' For web scraping: use web_browse(action:"discover") to find real CSS selectors, or web_browse(action:"get_links") to see all links, or web_browse(action:"extract", format:"html") to inspect the page structure.';
1156
- }
1157
- else if (parsed.name === 'web_browse' && String(parsed.arguments?.action) === 'navigate' && toolOutput.includes('Status: 40')) {
1158
- webHint = ' Page returned HTTP error. Try navigating to the site homepage first, then use web_browse(action:"get_links") or web_browse(action:"discover") to explore available categories and sections instead of guessing URLs.';
1159
- }
1160
- const msg = errorCount === specialistCfg.triggerThreshold + 1
1161
- ? `[System: Tool "${parsed.name}" failed with the SAME error ${errorCount} times. Read the error message carefully. The input is likely wrong — try a completely different approach, or use subagent for a fresh context.]${webHint}`
1162
- : `[System: Tool "${parsed.name}" failed ${errorCount} times with the same error. STOP. Do NOT retry the same call. Use subagent(task="...") for a fresh context, or tell the user you're stuck.]${webHint}`;
1163
- messages.push({ role: 'user', content: msg });
1164
- if (!quiet)
1165
- renderer?.writeWarning(` [error escalation: ${parsed.name} failed ${errorCount} times]`);
1166
- }
1167
- if (totalToolErrors >= 5 && errorStrategy === 'normal') {
1168
- errorStrategy = 'subagent-hint';
1169
- messages.push({ role: 'user', content: `[System: ${totalToolErrors} tool errors. Use subagent(task="...") for a fresh context.]` });
1170
- if (!quiet)
1171
- renderer?.writeWarning(` [strategy: subagent-hint after ${totalToolErrors} errors]`);
1172
- }
1173
- if (errorCount >= 4) {
1174
- const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
1175
- messages.push({ role: 'tool', content: `BLOCKED: Tool "${parsed.name}" failed ${errorCount} times. Temporarily blocked.`, tool_call_id: blockId });
1176
- const idx = validCalls.indexOf(call);
1177
- if (idx !== -1)
1178
- validCalls.splice(idx, 1);
1179
- if (!quiet)
1180
- renderer?.writeWarning(` [blocked ${parsed.name}: too many repeated errors]`);
1181
- }
931
+ const action = await errorEscalator.handleToolError(parsed.name, toolOutput, parsed.arguments, toolId, messages, createdFiles, enrichedPrompt, renderer, quiet, provider, registry, toolExecutor, modeConfig, llmCfg);
932
+ if (action === 'continue')
933
+ continue;
1182
934
  }
1183
935
  else {
1184
- for (const [key] of consecutiveToolErrors) {
1185
- if (key.startsWith(parsed.name + '::'))
1186
- consecutiveToolErrors.delete(key);
1187
- }
1188
- specialistSpawnedFor = null;
936
+ errorEscalator.resetForTool(parsed.name);
1189
937
  }
1190
938
  // НЕ добавляем "Continue." после каждого tool-вызова — это нарушает
1191
939
  // структуру диалога. Bounded nudge (строка 418) уже покрывает