micro-models-agent 0.63.3 → 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.
- package/CHANGELOG.md +148 -1
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +7 -4
- package/dist/i18n/ru.json +7 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1606 -800
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
package/dist/llm/orchestrator.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProviderManager } from "../modules/providers/manager";
|
|
2
|
+
import { FallbackProvider } from "../modules/providers/fallback";
|
|
2
3
|
import { jsonrepair } from "jsonrepair";
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
55
|
-
constructor(config, defaultProvider) {
|
|
93
|
+
getSessionId;
|
|
94
|
+
constructor(config, defaultProvider, opts) {
|
|
56
95
|
this.config = config;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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:
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
265
|
+
const chunks = [];
|
|
194
266
|
for await (const chunk of this.provider.chat(messages)) {
|
|
195
|
-
|
|
196
|
-
result += chunk.content;
|
|
267
|
+
chunks.push(chunk);
|
|
197
268
|
}
|
|
198
|
-
|
|
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
|
+
}
|
package/dist/llm/provider.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
106
|
-
|
|
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;
|