micro-models-agent 0.63.0 → 1.1.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 (186) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +8 -4
  41. package/dist/i18n/ru.json +8 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1755 -841
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/bridge-server.mjs +37 -4
  59. package/dist/modules/browser/driver.js +46 -4
  60. package/dist/modules/certification/cli.js +85 -42
  61. package/dist/modules/certification/loader.js +15 -1
  62. package/dist/modules/certification/manifest.js +126 -15
  63. package/dist/modules/certification/runner.js +4 -26
  64. package/dist/modules/certification/scenarios.js +184 -5
  65. package/dist/modules/certification/syntax-scenarios.js +51 -0
  66. package/dist/modules/context/chunk-query.js +25 -5
  67. package/dist/modules/context/fact-extractor.js +6 -2
  68. package/dist/modules/context/manager.js +23 -7
  69. package/dist/modules/execution/audit-runners.js +7 -1
  70. package/dist/modules/execution/auditor.js +3 -3
  71. package/dist/modules/execution/execution-plugin.js +22 -15
  72. package/dist/modules/execution/input-from.js +46 -0
  73. package/dist/modules/execution/module.js +107 -18
  74. package/dist/modules/execution/moe-executor.js +166 -54
  75. package/dist/modules/execution/plan-actions.js +524 -0
  76. package/dist/modules/execution/plan-steps.js +23 -0
  77. package/dist/modules/execution/plan-store.js +15 -3
  78. package/dist/modules/execution/plan-tool.js +6 -488
  79. package/dist/modules/execution/plan-validator.js +24 -0
  80. package/dist/modules/execution/stuck-detector.js +3 -18
  81. package/dist/modules/execution/tracker.js +14 -5
  82. package/dist/modules/execution/transient-error.js +30 -0
  83. package/dist/modules/execution/verifier.js +94 -7
  84. package/dist/modules/execution/windows-commands.js +11 -0
  85. package/dist/modules/hallucination/confidence.js +36 -23
  86. package/dist/modules/hallucination/consistency.js +3 -0
  87. package/dist/modules/hallucination/detector.js +8 -3
  88. package/dist/modules/hallucination/factual.js +26 -7
  89. package/dist/modules/hallucination/llm-judge.js +12 -2
  90. package/dist/modules/indexer/map-command.js +35 -0
  91. package/dist/modules/indexer/map-select.js +87 -0
  92. package/dist/modules/indexer/module.js +34 -22
  93. package/dist/modules/indexer/symbols.js +189 -0
  94. package/dist/modules/indexer/walker.js +96 -42
  95. package/dist/modules/lsp/check-tool.js +2 -1
  96. package/dist/modules/lsp/client.js +49 -32
  97. package/dist/modules/lsp/config.js +55 -2
  98. package/dist/modules/lsp/module.js +38 -5
  99. package/dist/modules/lsp/probe.js +4 -3
  100. package/dist/modules/lsp/project-root.js +41 -1
  101. package/dist/modules/lsp/startup-check.js +12 -4
  102. package/dist/modules/mcp/client.js +153 -104
  103. package/dist/modules/mcp/module.js +165 -41
  104. package/dist/modules/memory/module.js +4 -3
  105. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  106. package/dist/modules/plugins/manager.js +47 -84
  107. package/dist/modules/pricing/index.js +17 -7
  108. package/dist/modules/pricing/prices.js +30 -12
  109. package/dist/modules/processes/index.js +1 -0
  110. package/dist/modules/processes/kill-tree.js +56 -0
  111. package/dist/modules/processes/registry.js +2 -54
  112. package/dist/modules/providers/cache.js +23 -0
  113. package/dist/modules/providers/factory.js +28 -0
  114. package/dist/modules/providers/fallback.js +7 -5
  115. package/dist/modules/providers/health.js +2 -1
  116. package/dist/modules/providers/index.js +1 -0
  117. package/dist/modules/providers/manager.js +17 -2
  118. package/dist/modules/providers/presets.js +79 -6
  119. package/dist/modules/reasoning/policy.js +40 -0
  120. package/dist/modules/reasoning/probe.js +111 -0
  121. package/dist/modules/security/audit-notifier.js +42 -27
  122. package/dist/modules/security/command-validator.js +25 -20
  123. package/dist/modules/security/encryption.js +6 -12
  124. package/dist/modules/security/network-validator.js +76 -5
  125. package/dist/modules/security/path-validator.js +77 -34
  126. package/dist/modules/security/rate-limiter.js +11 -0
  127. package/dist/modules/security/security-policies.js +1 -1
  128. package/dist/modules/security/session-encryption.js +13 -2
  129. package/dist/modules/security/session-isolation.js +2 -9
  130. package/dist/modules/session/manager.js +11 -0
  131. package/dist/modules/session/module.js +11 -3
  132. package/dist/modules/session/store.js +41 -5
  133. package/dist/modules/skills/loader.js +7 -1
  134. package/dist/modules/skills/module.js +2 -1
  135. package/dist/modules/updater/changelog-reader.js +94 -0
  136. package/dist/modules/updater/dev-detect.js +17 -0
  137. package/dist/modules/updater/index.js +1 -0
  138. package/dist/modules/updater/module.js +14 -3
  139. package/dist/output/bus.js +32 -0
  140. package/dist/output/channel.js +233 -0
  141. package/dist/output/format.js +14 -0
  142. package/dist/output/index.js +7 -0
  143. package/dist/output/json-sink.js +22 -0
  144. package/dist/output/machine.js +8 -0
  145. package/dist/output/session-sink.js +27 -0
  146. package/dist/output/types.js +1 -0
  147. package/dist/tools/approve.js +6 -2
  148. package/dist/tools/attach-image.js +11 -11
  149. package/dist/tools/auto-fixer.js +198 -0
  150. package/dist/tools/bash.js +142 -89
  151. package/dist/tools/chunk-query.js +10 -6
  152. package/dist/tools/download-file.js +1 -1
  153. package/dist/tools/edit-file.js +20 -2
  154. package/dist/tools/executor.js +54 -9
  155. package/dist/tools/glob-tool.js +7 -0
  156. package/dist/tools/grep-tool.js +15 -1
  157. package/dist/tools/index.js +3 -1
  158. package/dist/tools/list-dir.js +3 -1
  159. package/dist/tools/load-skill.js +2 -1
  160. package/dist/tools/mcp-call.js +1 -1
  161. package/dist/tools/move-file.js +5 -4
  162. package/dist/tools/path-utils.js +7 -0
  163. package/dist/tools/pipeline-run.js +1 -1
  164. package/dist/tools/prompt-io.js +28 -0
  165. package/dist/tools/question.js +12 -12
  166. package/dist/tools/scope-request.js +91 -0
  167. package/dist/tools/session-info.js +44 -0
  168. package/dist/tools/set-thinking.js +71 -0
  169. package/dist/tools/subagent.js +50 -9
  170. package/dist/tools/syntax-validator.js +177 -0
  171. package/dist/tools/user-input.js +16 -9
  172. package/dist/tools/write-file.js +17 -1
  173. package/dist/ui/diff.js +10 -0
  174. package/dist/ui/line-editor.js +179 -26
  175. package/dist/ui/line-math.js +20 -3
  176. package/dist/ui/md-formatter.js +100 -10
  177. package/dist/ui/output.js +5 -4
  178. package/dist/ui/plan-view.js +2 -7
  179. package/dist/ui/renderer.js +89 -85
  180. package/dist/ui/spinner.js +14 -4
  181. package/dist/utils/error.js +4 -0
  182. package/dist/utils/index.js +4 -0
  183. package/dist/utils/retry.js +17 -0
  184. package/dist/utils/sleep.js +23 -0
  185. package/dist/utils/truncate.js +9 -0
  186. package/package.json +1 -1
@@ -1,15 +1,8 @@
1
- import { createProvider } from "../modules/providers/create";
1
+ import { ProviderManager } from "../modules/providers/manager";
2
+ import { FallbackProvider } from "../modules/providers/fallback";
2
3
  import { jsonrepair } from "jsonrepair";
3
- const PLAN_SYSTEM_PROMPT = `You are a planning assistant for an agent system with multiple expert sub-agents.
4
- Break down the user's task into subtasks that can be executed by different expert agents.
5
-
6
- Available experts and their tool tags:
7
- - code: file operations, code editing, shell commands
8
- - research: web search, web fetch, web browse, history search
9
- - browser: browser automation, screenshots
10
- - vision: browser + file reading for visual tasks
11
-
12
- Rules:
4
+ import { parseChunks } from "./response";
5
+ const PLAN_SYSTEM_PROMPT_RULES = `Rules:
13
6
  1. Each subtask must have an expert_tag from the available experts.
14
7
  2. Use depends_on for ordering when subtask B reads what subtask A writes.
15
8
  3. Independent subtasks should NOT depend on each other (they run in parallel).
@@ -39,6 +32,40 @@ Respond with a JSON object only (no markdown fences):
39
32
  "refs": []
40
33
  }
41
34
  }`;
35
+ const PLAN_SYSTEM_PROMPT = `You are a planning assistant for an agent system with multiple expert sub-agents.
36
+ Break down the user's task into subtasks that can be executed by different expert agents.
37
+
38
+ Available experts and their tool tags:
39
+ - code: file operations, code editing, shell commands
40
+ - research: web search, web fetch, web browse, history search
41
+ - browser: browser automation, screenshots
42
+ - vision: browser + file reading for visual tasks
43
+
44
+ ${PLAN_SYSTEM_PROMPT_RULES}`;
45
+ /**
46
+ * Render the planner system prompt from the configured experts instead of the
47
+ * hardcoded code/research/browser/vision list, so user-defined experts
48
+ * (config.experts) are visible to the Router. Experts without a description
49
+ * fall back to their tool_tags. Empty/missing registry → legacy hardcoded
50
+ * prompt (back-compat for direct OrchestratorClient usage in tests).
51
+ */
52
+ export function buildPlanSystemPrompt(experts) {
53
+ if (!experts || Object.keys(experts).length === 0) {
54
+ return PLAN_SYSTEM_PROMPT;
55
+ }
56
+ const lines = Object.entries(experts).map(([tag, cfg]) => {
57
+ const desc = cfg.description ||
58
+ (cfg.tool_tags?.length ? `tools: ${cfg.tool_tags.join(", ")}` : "general purpose");
59
+ return `- ${tag}: ${desc}`;
60
+ });
61
+ return `You are a planning assistant for an agent system with multiple expert sub-agents.
62
+ Break down the user's task into subtasks that can be executed by different expert agents.
63
+
64
+ Available experts and their tool tags:
65
+ ${lines.join("\n")}
66
+
67
+ ${PLAN_SYSTEM_PROMPT_RULES}`;
68
+ }
42
69
  const VERIFY_SYSTEM_PROMPT = `You are a verification and merge assistant for an agent system with multiple expert sub-agents.
43
70
  You receive the original plan, the results from each subtask, and any verification errors.
44
71
  Your job is to determine if the overall task was completed successfully or if re-planning is needed.
@@ -48,52 +75,77 @@ Respond with JSON only:
48
75
  - If re-plan needed: {"type": "replan", "plan": {updated MoEPlan}, "explanation": "why re-plan is needed"}
49
76
 
50
77
  Max 3 re-plan cycles. After 3 cycles, return partial result.`;
78
+ const SCOPE_DECISION_SYSTEM_PROMPT = `You are the router of a multi-expert agent system. A sub-agent could not finish its task because it lacked access to files outside its assigned scope.
79
+ Decide what to do with its scope request:
80
+ - "approve" — grant access ONLY to files it justifies; keep write grants minimal.
81
+ - "reject" — when the request is unjustified, excessive, or another subtask owns those files.
82
+
83
+ Respond with JSON only (no markdown fences):
84
+ {"action": "approve", "write": ["file1"], "read": ["file2"]}
85
+ or
86
+ {"action": "reject", "explanation": "why"}`;
87
+ const DEFAULT_BASE_URL = "http://localhost:1234/v1";
88
+ /** Fallback context window when neither the provider entry nor the config sets one. */
89
+ const DEFAULT_ORCH_CONTEXT_WINDOW = 32768;
51
90
  export class OrchestratorClient {
52
91
  config;
53
92
  provider = null;
54
- replanCycle = 0;
55
- constructor(config, defaultProvider) {
93
+ getSessionId;
94
+ constructor(config, defaultProvider, opts) {
56
95
  this.config = config;
57
- if (config.model) {
58
- if (config.provider) {
59
- this.provider = createProvider(config.provider.type ?? "openai-compat", {
60
- model: config.model,
61
- baseUrl: config.provider.baseUrl || "http://localhost:1234/v1",
62
- apiKey: config.provider.apiKey,
63
- contextWindow: 32768,
64
- retry: config.retry,
65
- });
66
- }
67
- else if (defaultProvider) {
68
- this.provider = defaultProvider;
69
- }
70
- else {
71
- this.provider = createProvider("openai-compat", {
72
- model: config.model,
73
- baseUrl: "http://localhost:1234/v1",
74
- contextWindow: 32768,
75
- retry: config.retry,
76
- });
77
- }
96
+ this.getSessionId = opts?.getSessionId;
97
+ if (!config.model)
98
+ return;
99
+ if (config.provider) {
100
+ this.provider = this.buildManagedProvider(config);
101
+ }
102
+ else if (defaultProvider) {
103
+ this.provider = defaultProvider;
78
104
  }
105
+ else {
106
+ this.provider = this.buildManagedProvider({
107
+ ...config,
108
+ provider: { type: "openai-compat", baseUrl: DEFAULT_BASE_URL },
109
+ });
110
+ }
111
+ }
112
+ /**
113
+ * Build the orchestrator provider through ProviderManager so per-entry
114
+ * contextWindow/retry/rateLimits and failover entries are honored
115
+ * (legacy {type,baseUrl,apiKey} configs are normalized by the manager).
116
+ * Previously the provider was built directly with a hardcoded 32768 window
117
+ * and no retry/rate-limit/failover support at all.
118
+ */
119
+ buildManagedProvider(config) {
120
+ const providerCfg = config.provider;
121
+ const manager = new ProviderManager({ ...providerCfg, baseUrl: providerCfg.baseUrl || DEFAULT_BASE_URL }, {
122
+ contextWindow: config.contextWindow ?? DEFAULT_ORCH_CONTEXT_WINDOW,
123
+ retry: config.retry,
124
+ rateLimits: config.rateLimits,
125
+ getSessionId: this.getSessionId,
126
+ logger: config.logger,
127
+ });
128
+ manager.setModel(config.model);
129
+ let provider = manager.active;
130
+ if (providerCfg.fallback && manager.listNames().length > 1) {
131
+ provider = new FallbackProvider(manager, (from, to, error) => {
132
+ config.logger?.warn(`orchestrator provider failover: ${from} -> ${to} (${error.message.slice(0, 120)})`);
133
+ });
134
+ }
135
+ return provider;
79
136
  }
80
137
  isEnabled() {
81
138
  return this.provider !== null;
82
139
  }
83
- getReplanCycle() {
84
- return this.replanCycle;
85
- }
86
- async chat(messages) {
140
+ async chat(messages, signal) {
87
141
  if (!this.provider)
88
142
  throw new Error("Orchestrator not enabled");
89
143
  const chunks = [];
90
- for await (const chunk of this.provider.chat(messages)) {
144
+ for await (const chunk of this.provider.chat(messages, undefined, signal)) {
91
145
  chunks.push(chunk);
92
146
  }
93
- return chunks
94
- .filter((c) => c.type === "text")
95
- .map((c) => c.content)
96
- .join("");
147
+ const parsed = parseChunks(chunks);
148
+ return parsed.type === "text" ? parsed.content : "";
97
149
  }
98
150
  parseJSON(text) {
99
151
  try {
@@ -119,39 +171,29 @@ export class OrchestratorClient {
119
171
  }
120
172
  }
121
173
  }
122
- async plan(userPrompt, _context) {
174
+ async plan(userPrompt, _context, signal) {
123
175
  if (!this.provider)
124
176
  return { error: "Orchestrator not enabled — no orchestrator model configured" };
125
177
  const messages = [
126
- { role: "system", content: PLAN_SYSTEM_PROMPT },
178
+ { role: "system", content: buildPlanSystemPrompt(this.config.experts) },
127
179
  { role: "user", content: userPrompt },
128
180
  ];
129
- const text = await this.chat(messages);
181
+ const text = await this.chat(messages, signal);
130
182
  const parsed = this.parseJSON(text);
131
183
  if (parsed && parsed.subtasks && parsed.subtasks.length > 0) {
132
184
  return { plan: parsed, raw: text };
133
185
  }
134
186
  return { error: `Failed to parse plan from LLM output. Raw: ${text.slice(0, 500)}` };
135
187
  }
136
- async verifyAndMerge(input) {
188
+ async verifyAndMerge(input, signal) {
137
189
  if (!this.provider)
138
190
  return { type: "final", finalAnswer: input.results.map((r) => r.summary).join("\n") };
139
- this.replanCycle++;
140
- if (this.replanCycle > 3) {
141
- return {
142
- type: "final",
143
- finalAnswer: input.results
144
- .map((r) => `${r.subtaskId}: ${r.success ? "OK" : "FAIL"} — ${r.summary}`)
145
- .join("\n"),
146
- explanation: "Max re-plan cycles (3) reached. Returning partial results.",
147
- };
148
- }
149
191
  const context = JSON.stringify(input, null, 2);
150
192
  const messages = [
151
193
  { role: "system", content: VERIFY_SYSTEM_PROMPT },
152
194
  { role: "user", content: context },
153
195
  ];
154
- const text = await this.chat(messages);
196
+ const text = await this.chat(messages, signal);
155
197
  const parsed = this.parseJSON(text);
156
198
  if (parsed && parsed.type) {
157
199
  return parsed;
@@ -162,6 +204,38 @@ export class OrchestratorClient {
162
204
  explanation: "Failed to parse verifier output, returning collected results.",
163
205
  };
164
206
  }
207
+ /**
208
+ * Router decision on a sub-agent's scope request. Defaults to reject on any
209
+ * parse failure — scope must never expand silently.
210
+ */
211
+ async resolveScopeRequest(subtaskId, request, signal) {
212
+ if (!this.provider)
213
+ return { action: "reject", explanation: "orchestrator not enabled" };
214
+ const messages = [
215
+ { role: "system", content: SCOPE_DECISION_SYSTEM_PROMPT },
216
+ {
217
+ role: "user",
218
+ content: JSON.stringify({ subtaskId, request }, null, 2),
219
+ },
220
+ ];
221
+ const text = await this.chat(messages, signal);
222
+ const parsed = this.parseJSON(text);
223
+ if (parsed && parsed.action === "approve") {
224
+ const write = Array.isArray(parsed.write) ? parsed.write.map(String) : [];
225
+ const read = Array.isArray(parsed.read) ? parsed.read.map(String) : [];
226
+ // An approve without any concrete files is meaningless — treat as reject.
227
+ if (write.length === 0 && read.length === 0) {
228
+ return { action: "reject", explanation: "approve carried no files" };
229
+ }
230
+ return { action: "approve", write, read };
231
+ }
232
+ return {
233
+ action: "reject",
234
+ explanation: parsed?.action === "reject"
235
+ ? parsed.explanation
236
+ : `unparseable router decision: ${text.slice(0, 200)}`,
237
+ };
238
+ }
165
239
  async createPlan(task) {
166
240
  if (!this.provider)
167
241
  throw new Error("Orchestrator not enabled");
@@ -173,12 +247,10 @@ export class OrchestratorClient {
173
247
  { role: "user", content: task },
174
248
  ];
175
249
  const text = await this.chat(messages);
176
- try {
177
- return JSON.parse(text);
178
- }
179
- catch {
180
- return { steps: [task] };
181
- }
250
+ const parsed = this.parseJSON(text);
251
+ if (parsed)
252
+ return parsed;
253
+ return { steps: [task] };
182
254
  }
183
255
  async resolveConflict(context) {
184
256
  if (!this.provider)
@@ -190,11 +262,11 @@ export class OrchestratorClient {
190
262
  },
191
263
  { role: "user", content: context },
192
264
  ];
193
- let result = "";
265
+ const chunks = [];
194
266
  for await (const chunk of this.provider.chat(messages)) {
195
- if (chunk.type === "text" && chunk.content)
196
- result += chunk.content;
267
+ chunks.push(chunk);
197
268
  }
198
- return result;
269
+ const parsed = parseChunks(chunks);
270
+ return parsed.type === "text" ? parsed.content : "";
199
271
  }
200
272
  }
@@ -0,0 +1,68 @@
1
+ // src/llm/provider-budget
2
+ /**
3
+ * Запрос баланса/расхода у провайдера. Поддерживается только OpenRouter —
4
+ * единственный, кто отдаёт эти данные по API-ключу. Zen/Go баланс не
5
+ * возвращают (только веб-дашборд), поэтому для них честно `unsupported`.
6
+ */
7
+ const DEFAULT_TIMEOUT_MS = 5000;
8
+ /** Приводит значение к конечному числу; иначе undefined. */
9
+ function num(value) {
10
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
11
+ }
12
+ /** GET JSON с Bearer-авторизацией; не-2xx превращает в ошибку. */
13
+ async function getJson(url, apiKey, timeoutMs) {
14
+ const response = await fetch(url, {
15
+ headers: { Authorization: `Bearer ${apiKey}` },
16
+ signal: AbortSignal.timeout(timeoutMs),
17
+ });
18
+ if (!response.ok) {
19
+ throw new Error(`HTTP ${response.status}`);
20
+ }
21
+ return response.json();
22
+ }
23
+ /**
24
+ * Запрашивает бюджет у провайдера. Никогда не бросает: любые сбои возвращаются
25
+ * как `reason: "error"` (и логируются), чтобы вызывающая команда показала
26
+ * причину, а не падала.
27
+ */
28
+ export async function fetchProviderBudget(provider, baseUrl, apiKey, options) {
29
+ if (provider !== "openrouter") {
30
+ options.log("debug", `budget: provider "${provider}" does not expose an API balance`);
31
+ return { budget: null, reason: "unsupported" };
32
+ }
33
+ if (!apiKey) {
34
+ options.log("debug", "budget: no api key configured");
35
+ return { budget: null, reason: "no-key" };
36
+ }
37
+ const base = baseUrl.replace(/\/$/, "");
38
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
39
+ try {
40
+ const keyBody = (await getJson(`${base}/key`, apiKey, timeoutMs));
41
+ const data = keyBody?.data ?? {};
42
+ const budget = { provider, source: "openrouter" };
43
+ budget.keyLabel = typeof data.label === "string" ? data.label : undefined;
44
+ budget.keyUsageUsd = num(data.usage);
45
+ budget.keyLimitUsd = num(data.limit);
46
+ budget.keyRemainingUsd = num(data.limit_remaining);
47
+ budget.isFreeTier = typeof data.is_free_tier === "boolean" ? data.is_free_tier : undefined;
48
+ // /credits требует management key; при отказе продолжаем с данными ключа.
49
+ try {
50
+ const creditsBody = (await getJson(`${base}/credits`, apiKey, timeoutMs));
51
+ const c = creditsBody?.data ?? {};
52
+ budget.totalCreditsUsd = num(c.total_credits);
53
+ budget.totalUsageUsd = num(c.total_usage);
54
+ if (budget.totalCreditsUsd !== undefined && budget.totalUsageUsd !== undefined) {
55
+ budget.balanceUsd = budget.totalCreditsUsd - budget.totalUsageUsd;
56
+ }
57
+ }
58
+ catch (creditsErr) {
59
+ options.log("debug", `budget: /credits unavailable (${creditsErr instanceof Error ? creditsErr.message : String(creditsErr)})`);
60
+ }
61
+ return { budget, reason: "ok" };
62
+ }
63
+ catch (err) {
64
+ const message = err instanceof Error ? err.message : String(err);
65
+ options.log("warn", `budget: request failed (${message})`);
66
+ return { budget: null, reason: "error", error: message };
67
+ }
68
+ }
@@ -1,4 +1,3 @@
1
- // src/llm/provider.ts
2
1
  /** Helper: extract plain text from message content (for token counting, logging, etc.) */
3
2
  export function getMessageText(content) {
4
3
  if (typeof content === "string")
@@ -0,0 +1,26 @@
1
+ export class StreamState {
2
+ sawDone = false;
3
+ sawText = false;
4
+ /** tool_call начал накапливаться — сервер закоммитился на ответ. */
5
+ sawToolCallStart = false;
6
+ parsedChunks = 0;
7
+ /** Не-SSE строки (Ollama NDJSON, комментарии, keep-alive) — для диагностики. */
8
+ nonDataLines = 0;
9
+ nonDataSamples = [];
10
+ /** Провайдер прислал {"error": ...} уже после старта генерации. */
11
+ midStreamError = null;
12
+ usage;
13
+ lastFinishReason;
14
+ /** Снимок для debug-лога по завершении стрима. */
15
+ snapshot() {
16
+ return {
17
+ sawDone: this.sawDone,
18
+ lastFinishReason: this.lastFinishReason ?? null,
19
+ sawText: this.sawText,
20
+ sawToolCallStart: this.sawToolCallStart,
21
+ parsedChunks: this.parsedChunks,
22
+ nonDataLines: this.nonDataLines,
23
+ nonDataSamples: this.nonDataLines > 0 ? this.nonDataSamples : undefined,
24
+ };
25
+ }
26
+ }
@@ -1,6 +1,34 @@
1
1
  // src/llm/token-counter.ts
2
2
  import { encodingForModel, getEncoding } from "js-tiktoken";
3
3
  import { getMessageText } from "./provider";
4
+ /**
5
+ * Script-aware token ESTIMATE (cheap, no full tokenization). Cheap enough for
6
+ * every hand-built PromptBlock; used at block-build time where the exact
7
+ * TokenCounter may not be in scope. Measured on cl100k_base: Latin AND
8
+ * Cyrillic both encode at ~3.3-5 chars/token (the old flat len/4 was actually
9
+ * close for Cyrillic — the "cyr ≈ /2" assumption was wrong); CJK/Hangul are
10
+ * the real outliers at ~1-1.5 tokens/char, so they get /1.5. Within ±30% of
11
+ * tiktoken for Russian text.
12
+ */
13
+ export function estimateTokens(text) {
14
+ if (!text)
15
+ return 0;
16
+ let cjk = 0;
17
+ let rest = 0;
18
+ for (const ch of text) {
19
+ const code = ch.codePointAt(0);
20
+ if ((code >= 0x3000 && code <= 0x9fff) || // CJK unified + symbols
21
+ (code >= 0xac00 && code <= 0xd7af) || // Hangul syllables
22
+ (code >= 0xff00 && code <= 0xffef) // fullwidth forms
23
+ ) {
24
+ cjk++;
25
+ }
26
+ else {
27
+ rest++;
28
+ }
29
+ }
30
+ return Math.ceil(cjk / 1.5 + rest / 4);
31
+ }
4
32
  export class TokenCounter {
5
33
  encoder;
6
34
  constructor(model = "gpt-4o") {
@@ -2,26 +2,19 @@ import { appendFileSync, mkdirSync, existsSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { sanitizeLogMessage } from "../modules/security/data-sanitizer";
4
4
  import { FileLogWriter } from "./file-log";
5
- import pc from "picocolors";
5
+ import { defaultOutputBus } from "../output";
6
6
  const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
7
- const LEVEL_COLORS = {
8
- debug: (s) => pc.dim(s),
9
- info: (s) => s,
10
- warn: (s) => pc.yellow(s),
11
- error: (s) => pc.red(s),
12
- };
13
- function isColorEnabled() {
14
- return !process.env.NO_COLOR && !process.env.CI && process.stdout.isTTY === true;
15
- }
16
7
  export class Logger {
17
8
  level;
18
9
  prefix;
19
10
  logDir = null;
20
11
  sessionDir = null;
21
12
  fileLog;
22
- constructor(level = "info", prefix = "") {
13
+ sink;
14
+ constructor(level = "info", prefix = "", sink = defaultOutputBus) {
23
15
  this.level = level;
24
16
  this.prefix = prefix;
17
+ this.sink = sink;
25
18
  this.fileLog = new FileLogWriter();
26
19
  }
27
20
  setLevel(level) {
@@ -74,7 +67,7 @@ export class Logger {
74
67
  this.fileLog.logREPL(tag, sanitizeLogMessage(content));
75
68
  }
76
69
  child(prefix) {
77
- const childLogger = new Logger(this.level, this.prefix ? `${this.prefix}:${prefix}` : prefix);
70
+ const childLogger = new Logger(this.level, this.prefix ? `${this.prefix}:${prefix}` : prefix, this.sink);
78
71
  if (this.logDir)
79
72
  childLogger.setLogDir(this.logDir);
80
73
  if (this.sessionDir)
@@ -100,10 +93,14 @@ export class Logger {
100
93
  const sanitizedMsg = sanitizeLogMessage(msg);
101
94
  const sanitizedMeta = meta ? this.sanitizeMeta(meta) : undefined;
102
95
  const ts = new Date().toISOString();
103
- const prefix = this.prefix ? ` [${this.prefix}]` : "";
104
96
  const metaStr = sanitizedMeta ? ` ${JSON.stringify(sanitizedMeta)}` : "";
105
- const line = `[${level.toUpperCase()}]${prefix} ${ts} — ${sanitizedMsg}${metaStr}`;
106
- console.log(isColorEnabled() ? LEVEL_COLORS[level](line) : line);
97
+ const line = `${sanitizedMsg}${metaStr}`;
98
+ try {
99
+ this.sink.emit({ level, source: this.prefix || "mma", kind: "log", text: line });
100
+ }
101
+ catch {
102
+ /* terminal output is best-effort; file logging below must still run */
103
+ }
107
104
  // Legacy v1 behavior: also append to the tagged `.log` file (session or daily).
108
105
  this.fileLog.log(level.toUpperCase(), this.prefix || "MMA", `${ts} — ${sanitizedMsg}${metaStr}`);
109
106
  const logTarget = this.sessionDir ?? this.logDir;