min-agent 0.2.1 → 0.4.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/README.md +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/agent.js
CHANGED
|
@@ -1,31 +1,93 @@
|
|
|
1
|
-
import { streamText, stepCountIs } from "ai";
|
|
2
|
-
import { readFileSync, existsSync } from "fs";
|
|
1
|
+
import { streamText, stepCountIs, } from "ai";
|
|
2
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { resolveModel } from "./provider.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { isProviderStallError, describeError } from "./fetch-timeout.js";
|
|
6
|
+
import { getEffectiveConfig, getActiveProvider } from "./config.js";
|
|
7
|
+
import { getModelPrice, estimateCost } from "./pricing.js";
|
|
8
|
+
import { createTools } from "./tools/index.js";
|
|
9
|
+
import { scanProject, buildCodeSystemPrompt } from "./code-mode.js";
|
|
10
|
+
import { initMcp, shutdownMcp, getMcpTools, getMcpCatalogTools, getMcpReadOnlyToolIds } from "./mcp.js";
|
|
11
|
+
import { discoverSkills, attachSkills, collectLoadedSkillNames } from "./skills.js";
|
|
8
12
|
import { loadInstructions } from "./instructions.js";
|
|
9
13
|
import { getMemorySystemPrompt, getMemoryTools } from "./memory.js";
|
|
10
|
-
import { needsCompaction, compactMessages, estimateTokens, TokenTracker } from "./compaction.js";
|
|
11
|
-
import { loadPluginTools } from "./plugins.js";
|
|
14
|
+
import { needsCompaction, compactMessages, estimateTokens, estimateStringTokens, estimateOverheadTokens, TokenTracker, COMPACTION_RATIO, PRUNE_PRESSURE_RATIO, PRUNE_TARGET_RATIO, applyToolPrune, pruneToolOutputs, } from "./compaction.js";
|
|
15
|
+
import { loadPluginTools, getPluginReadOnlyIds } from "./plugins.js";
|
|
12
16
|
import { MarkdownRenderer } from "./markdown.js";
|
|
13
|
-
import { DoomLoopDetector } from "./doom-loop.js";
|
|
17
|
+
import { DoomLoopDetector, STEER_PROMPT, DELIVER_PROMPT, LOOP_HALT_MESSAGE, RESEARCH_STUB_RESULT, WEB_RESEARCH_TOOLS, } from "./doom-loop.js";
|
|
14
18
|
import { ThinkingBodySplitter, stripThinkingFromAssistantText } from "./assistant-stream.js";
|
|
19
|
+
import { XmlSearchSplitter } from "./xml-search.js";
|
|
15
20
|
import { printHeader, printDivider, printToolCall, printToolResult, printDone } from "./output.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
import { createTodoTool, captureGoal, copyTaskState, emptyTaskState, formatTaskStatePrompt, } from "./tools/todo.js";
|
|
22
|
+
import { getContextWindowInfo } from "./context-window.js";
|
|
23
|
+
import { log, logToolCall, logToolResult, startRunLog, nextRunPass, endRunLog } from "./logger.js";
|
|
24
|
+
import { markSyntheticMessage, isSyntheticMessage } from "./synthetic.js";
|
|
25
|
+
import { runWithInstructionTracker, resetActiveInstructionTracker } from "./instructions.js";
|
|
26
|
+
const UNBOUNDED_STEP_CAP = 100_000;
|
|
27
|
+
const DEFAULT_MAX_CONTINUES = 40;
|
|
28
|
+
/** Attempts (including the first) allowed when the provider answers with nothing at all. */
|
|
29
|
+
const DEFAULT_MAX_EMPTY_ATTEMPTS = 4;
|
|
30
|
+
const EMPTY_RETRY_BASE_DELAY_MS = 1000;
|
|
31
|
+
const EMPTY_RETRY_MAX_DELAY_MS = 8000;
|
|
32
|
+
/** Wall-clock budget for one turn before the model is asked to wrap up. */
|
|
33
|
+
const DEFAULT_TURN_TIME_LIMIT_MS = 20 * 60 * 1000;
|
|
34
|
+
/** Tool steps in one turn before the model is asked to wrap up. */
|
|
35
|
+
const DEFAULT_SOFT_STEP_LIMIT = 120;
|
|
36
|
+
const KEEP_RECENT_STEPS_BEFORE_PRUNE = 4;
|
|
37
|
+
const CONTINUE_PROMPT = "Continue the task from where you left off. Do not wait for another user message. If the work is complete, give a brief summary and stop.";
|
|
38
|
+
const WRAP_UP_PROMPT = "You are out of time budget for this turn. Stop starting new work: finish or save what is already in progress, then reply with a short summary of what is done, what is not, and the exact next step. Do not call more tools than needed to leave things in a consistent state.";
|
|
39
|
+
/** Exported for tests / callers that want to recognise the injected wrap-up turn. */
|
|
40
|
+
export const WRAP_UP_PROMPT_TEXT = WRAP_UP_PROMPT;
|
|
41
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
42
|
+
const IMAGE_MIME_TYPES = {
|
|
43
|
+
".png": "image/png",
|
|
44
|
+
".jpg": "image/jpeg",
|
|
45
|
+
".jpeg": "image/jpeg",
|
|
46
|
+
".gif": "image/gif",
|
|
47
|
+
".webp": "image/webp",
|
|
48
|
+
};
|
|
49
|
+
const DIM_STYLE = "NO_COLOR" in process.env ? { dim: "", reset: "" } : { dim: "\x1b[90m", reset: "\x1b[0m" };
|
|
22
50
|
/** Shown immediately so the terminal does not look frozen while MCP / rules load. */
|
|
51
|
+
function resolveMaxSteps(raw) {
|
|
52
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1) {
|
|
53
|
+
return { cap: UNBOUNDED_STEP_CAP, bounded: false };
|
|
54
|
+
}
|
|
55
|
+
return { cap: Math.floor(raw), bounded: true };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Backoff before re-sending a request the provider answered with an empty
|
|
59
|
+
* stream. The first retry is immediate (most empty replies are one-off), later
|
|
60
|
+
* ones back off so a stalling gateway is not hammered.
|
|
61
|
+
*/
|
|
62
|
+
export function emptyRetryDelay(attempt, baseMs = EMPTY_RETRY_BASE_DELAY_MS) {
|
|
63
|
+
if (attempt <= 1 || baseMs <= 0)
|
|
64
|
+
return 0;
|
|
65
|
+
return Math.min(EMPTY_RETRY_MAX_DELAY_MS, baseMs * 2 ** (attempt - 2));
|
|
66
|
+
}
|
|
67
|
+
function sleep(ms) {
|
|
68
|
+
if (ms <= 0)
|
|
69
|
+
return Promise.resolve();
|
|
70
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
|
+
}
|
|
72
|
+
/** User-facing note when the provider kept answering with an empty stream. */
|
|
73
|
+
export function emptyResponseMessage(attempts) {
|
|
74
|
+
return `模型连续 ${attempts} 次返回空响应(provider 可能超时或被限流),本轮已停止。发送消息可重试`;
|
|
75
|
+
}
|
|
76
|
+
/** User-facing note when a turn hit its time / step budget. */
|
|
77
|
+
export const WRAP_UP_MESSAGE = "本轮已达时间或步数预算,已让模型收尾并停止。发送消息可继续";
|
|
78
|
+
function resolveLimit(...values) {
|
|
79
|
+
for (const value of values) {
|
|
80
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
23
85
|
function printInitLoading() {
|
|
24
|
-
const { dim, reset } =
|
|
86
|
+
const { dim, reset } = DIM_STYLE;
|
|
25
87
|
console.log(`${dim}⟳ 正在初始化(MCP、技能、规则)…${reset}`);
|
|
26
88
|
}
|
|
27
89
|
function printInitReady() {
|
|
28
|
-
const { dim, reset } =
|
|
90
|
+
const { dim, reset } = DIM_STYLE;
|
|
29
91
|
console.log(`${dim}✓ 就绪${reset}`);
|
|
30
92
|
}
|
|
31
93
|
/** Stream thinking to stderr (dim). Set MIN_AGENT_SHOW_THINKING=0 to hide. */
|
|
@@ -34,547 +96,1233 @@ function writeThinkingDelta(text) {
|
|
|
34
96
|
return;
|
|
35
97
|
if (process.env.MIN_AGENT_SHOW_THINKING === "0" || process.env.MIN_AGENT_SHOW_THINKING === "false")
|
|
36
98
|
return;
|
|
37
|
-
|
|
38
|
-
const dim = noColor ? "" : "\x1b[2m";
|
|
39
|
-
const reset = noColor ? "" : "\x1b[0m";
|
|
40
|
-
process.stderr.write(`${dim}${text}${reset}`);
|
|
41
|
-
}
|
|
42
|
-
function buildSystemPrompt(instructions) {
|
|
43
|
-
const parts = [
|
|
44
|
-
`You are a helpful coding agent. You can read files, write files, run shell commands, search the web, and search the codebase to help the user with software engineering tasks.`,
|
|
45
|
-
"",
|
|
46
|
-
"Be concise and direct. When you run a command, briefly explain why.",
|
|
47
|
-
"Use the available tools to complete tasks. When multiple independent operations are needed, call tools in parallel.",
|
|
48
|
-
"When the user asks about current events, news, or anything requiring up-to-date information, use the web_search tool.",
|
|
49
|
-
"",
|
|
50
|
-
`Working directory: ${process.cwd()}`,
|
|
51
|
-
`Platform: ${process.platform}`,
|
|
52
|
-
`Date: ${new Date().toDateString()}`,
|
|
53
|
-
"",
|
|
54
|
-
`When the user asks to configure, install, or manage MCP servers, skills, rules, memory, or other min-agent features, use the read tool on the file at ${path.resolve(path.dirname(new URL(import.meta.url).pathname), "../README.md")} first, then follow what it says.`,
|
|
55
|
-
];
|
|
56
|
-
const skillsPrompt = getSkillsSystemPrompt();
|
|
57
|
-
if (skillsPrompt) {
|
|
58
|
-
parts.push("", skillsPrompt);
|
|
59
|
-
}
|
|
60
|
-
const memoryPrompt = getMemorySystemPrompt();
|
|
61
|
-
if (memoryPrompt) {
|
|
62
|
-
parts.push("", memoryPrompt);
|
|
63
|
-
}
|
|
64
|
-
if (instructions.length > 0) {
|
|
65
|
-
parts.push("", "# User Instructions", "");
|
|
66
|
-
parts.push(...instructions);
|
|
67
|
-
}
|
|
68
|
-
return parts.join("\n");
|
|
99
|
+
process.stderr.write(`${DIM_STYLE.dim}${text}${DIM_STYLE.reset}`);
|
|
69
100
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!imagePaths || imagePaths.length === 0)
|
|
73
|
-
return message;
|
|
74
|
-
const parts = [{ type: "text", text: message }];
|
|
101
|
+
export function loadImageParts(imagePaths, notify) {
|
|
102
|
+
const parts = [];
|
|
75
103
|
for (const imgPath of imagePaths) {
|
|
76
104
|
const resolved = path.resolve(process.cwd(), imgPath);
|
|
77
105
|
if (!existsSync(resolved)) {
|
|
78
|
-
|
|
106
|
+
notify?.("not_found", imgPath);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const size = statSync(resolved).size;
|
|
110
|
+
if (size > MAX_IMAGE_BYTES) {
|
|
111
|
+
notify?.("too_large", imgPath, size);
|
|
79
112
|
continue;
|
|
80
113
|
}
|
|
81
114
|
const data = readFileSync(resolved);
|
|
82
115
|
const ext = path.extname(resolved).toLowerCase();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
".jpg": "image/jpeg",
|
|
86
|
-
".jpeg": "image/jpeg",
|
|
87
|
-
".gif": "image/gif",
|
|
88
|
-
".webp": "image/webp",
|
|
89
|
-
};
|
|
90
|
-
const mimeType = mimeMap[ext] ?? "image/png";
|
|
91
|
-
parts.push({
|
|
92
|
-
type: "image",
|
|
93
|
-
image: data,
|
|
94
|
-
mimeType,
|
|
95
|
-
});
|
|
96
|
-
console.log(`\x1b[90m 📎 ${imgPath}\x1b[0m`);
|
|
116
|
+
parts.push({ type: "image", image: data, mimeType: IMAGE_MIME_TYPES[ext] ?? "image/png" });
|
|
117
|
+
notify?.("attached", imgPath);
|
|
97
118
|
}
|
|
98
119
|
return parts;
|
|
99
120
|
}
|
|
121
|
+
/** Build user message content, optionally with images */
|
|
122
|
+
export async function buildUserContent(message, imagePaths) {
|
|
123
|
+
if (!imagePaths || imagePaths.length === 0)
|
|
124
|
+
return message;
|
|
125
|
+
const images = loadImageParts(imagePaths, (kind, imgPath, sizeBytes) => {
|
|
126
|
+
if (kind === "not_found") {
|
|
127
|
+
console.error(`\x1b[33m Warning: Image not found: ${imgPath}\x1b[0m`);
|
|
128
|
+
}
|
|
129
|
+
else if (kind === "too_large") {
|
|
130
|
+
console.error(`\x1b[33m Warning: Image skipped (${((sizeBytes ?? 0) / 1024 / 1024).toFixed(1)} MB exceeds ${MAX_IMAGE_BYTES / 1024 / 1024} MB limit): ${imgPath}\x1b[0m`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(`\x1b[90m 📎 ${imgPath}\x1b[0m`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return [{ type: "text", text: message }, ...images];
|
|
137
|
+
}
|
|
100
138
|
/** Single-shot: send one message, get response, exit */
|
|
101
|
-
export async function runAgent(message, modelId, imagePaths) {
|
|
139
|
+
export async function runAgent(message, modelId, imagePaths, providerName, resumeSessionId) {
|
|
102
140
|
printHeader(modelId);
|
|
103
141
|
printDivider();
|
|
104
142
|
printInitLoading();
|
|
105
143
|
await initMcp();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
await shutdownMcp();
|
|
115
|
-
}
|
|
116
|
-
/** runOnce variant that accepts a pre-built system prompt (for code mode) */
|
|
117
|
-
export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSignal, callbacks, tracker) {
|
|
118
|
-
const model = resolveModel(modelId);
|
|
119
|
-
const api = !!callbacks;
|
|
120
|
-
if (needsCompaction(messages, tracker)) {
|
|
121
|
-
console.log("\x1b[90m⟳ Compacting context...\x1b[0m");
|
|
122
|
-
const result = await compactMessages(messages, model);
|
|
123
|
-
if (result.compacted) {
|
|
124
|
-
messages.length = 0;
|
|
125
|
-
messages.push(...result.messages);
|
|
126
|
-
if (result.shouldContinue) {
|
|
127
|
-
const continueText = result.replayText || "Continue with your task.";
|
|
128
|
-
messages.push({ role: "user", content: continueText });
|
|
129
|
-
}
|
|
130
|
-
if (tracker)
|
|
131
|
-
tracker.resetContext();
|
|
132
|
-
console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
|
|
144
|
+
try {
|
|
145
|
+
discoverSkills();
|
|
146
|
+
const instructions = await loadInstructions();
|
|
147
|
+
const { loadExecHistory, saveSession } = await import("./sessions.js");
|
|
148
|
+
const history = loadExecHistory(resumeSessionId);
|
|
149
|
+
if (!history.ok) {
|
|
150
|
+
console.error(history.error);
|
|
151
|
+
return true;
|
|
133
152
|
}
|
|
153
|
+
printInitReady();
|
|
154
|
+
console.log(`\x1b[36m> ${message}\x1b[0m\n`);
|
|
155
|
+
const content = await buildUserContent(message, imagePaths);
|
|
156
|
+
const messages = [...history.messages, { role: "user", content }];
|
|
157
|
+
const tracker = new TokenTracker();
|
|
158
|
+
const taskState = emptyTaskState();
|
|
159
|
+
if (history.taskState)
|
|
160
|
+
copyTaskState(history.taskState, taskState);
|
|
161
|
+
// Persist as the run progresses so a crash keeps the work done so far.
|
|
162
|
+
const { createSaveThrottle } = await import("./save-throttle.js");
|
|
163
|
+
let sessionId = history.sessionId;
|
|
164
|
+
const persist = () => {
|
|
165
|
+
try {
|
|
166
|
+
sessionId = saveSession(messages, sessionId, undefined, undefined, taskState);
|
|
167
|
+
}
|
|
168
|
+
catch { }
|
|
169
|
+
};
|
|
170
|
+
const saveThrottle = createSaveThrottle(persist);
|
|
171
|
+
const { hasError } = await runOnce(messages, instructions, modelId, undefined, undefined, tracker, {
|
|
172
|
+
providerName,
|
|
173
|
+
taskState,
|
|
174
|
+
onStepPersist: () => saveThrottle.request(),
|
|
175
|
+
});
|
|
176
|
+
saveThrottle.cancel();
|
|
177
|
+
persist();
|
|
178
|
+
if (sessionId)
|
|
179
|
+
console.log(`\nResume with: min-agent exec --resume ${sessionId}`);
|
|
180
|
+
return hasError;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await shutdownMcp();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Push assistant text + tool call/result history into `messages` so the model
|
|
188
|
+
* remembers its tool activity across turns and resumed sessions.
|
|
189
|
+
*/
|
|
190
|
+
export function pushTurn(messages, assistantText, toolCalls, toolResults) {
|
|
191
|
+
const cleaned = stripThinkingFromAssistantText(assistantText);
|
|
192
|
+
const resultIds = new Set(toolResults.map((r) => r.toolCallId));
|
|
193
|
+
const parts = [];
|
|
194
|
+
if (cleaned.trim())
|
|
195
|
+
parts.push({ type: "text", text: cleaned });
|
|
196
|
+
for (const c of toolCalls) {
|
|
197
|
+
if (!resultIds.has(c.toolCallId))
|
|
198
|
+
continue;
|
|
199
|
+
parts.push({ type: "tool-call", toolCallId: c.toolCallId, toolName: c.toolName, input: c.input });
|
|
200
|
+
}
|
|
201
|
+
if (parts.length > 0) {
|
|
202
|
+
messages.push({ role: "assistant", content: parts });
|
|
203
|
+
}
|
|
204
|
+
for (const r of toolResults) {
|
|
205
|
+
const text = safeText(r.output);
|
|
206
|
+
messages.push({
|
|
207
|
+
role: "tool",
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "tool-result",
|
|
211
|
+
toolCallId: r.toolCallId,
|
|
212
|
+
toolName: r.toolName,
|
|
213
|
+
output: { type: "text", value: text },
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
});
|
|
134
217
|
}
|
|
135
|
-
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Tools that stay constant for a whole run: everything except the skill tool,
|
|
221
|
+
* which is re-bound per iteration (see buildIterationPrompts). Building these
|
|
222
|
+
* once per run avoids re-reading plugin manifests and re-wrapping MCP tools on
|
|
223
|
+
* every continue.
|
|
224
|
+
*/
|
|
225
|
+
async function buildRunTools(modelId, abortSignal, planMode, tracker, taskState, subAgent) {
|
|
226
|
+
const builtinTools = createTools();
|
|
227
|
+
builtinTools.todo = createTodoTool({ store: taskState });
|
|
136
228
|
const mcpTools = getMcpTools();
|
|
229
|
+
const catalogTools = getMcpCatalogTools();
|
|
137
230
|
const memoryTools = getMemoryTools();
|
|
138
231
|
const pluginTools = await loadPluginTools();
|
|
139
|
-
const
|
|
232
|
+
const allTools = {
|
|
233
|
+
...builtinTools,
|
|
234
|
+
...memoryTools,
|
|
235
|
+
...pluginTools,
|
|
236
|
+
...mcpTools,
|
|
237
|
+
...catalogTools,
|
|
238
|
+
};
|
|
239
|
+
const onUsage = tracker ? (usage) => tracker.add(usage) : undefined;
|
|
140
240
|
const { createExploreTool } = await import("./tools/explore.js");
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
allTools["explore"] = createExploreTool(modelId);
|
|
149
|
-
let stepCount = 0;
|
|
150
|
-
let hasError = false;
|
|
151
|
-
const doomLoop = new DoomLoopDetector();
|
|
152
|
-
const result = streamText({
|
|
153
|
-
model,
|
|
154
|
-
system: systemPrompt,
|
|
155
|
-
messages,
|
|
156
|
-
tools: allTools,
|
|
157
|
-
stopWhen: stepCountIs(MAX_STEPS),
|
|
158
|
-
maxRetries: 3,
|
|
159
|
-
abortSignal,
|
|
160
|
-
onStepFinish() { stepCount++; },
|
|
161
|
-
onError() { },
|
|
241
|
+
allTools.explore = createExploreTool(modelId, abortSignal, onUsage, { shouldStop: subAgent.shouldStop });
|
|
242
|
+
const { createTaskTool } = await import("./tools/task.js");
|
|
243
|
+
// Sub-agents share the parent's loop guard and budget so they cannot restart
|
|
244
|
+
// the research allowance or run past the cost ceiling.
|
|
245
|
+
allTools.task = createTaskTool(modelId, abortSignal, onUsage, {
|
|
246
|
+
loopGuard: subAgent.loopGuard,
|
|
247
|
+
shouldStop: subAgent.shouldStop,
|
|
162
248
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
249
|
+
if (planMode) {
|
|
250
|
+
for (const name of ["bash", "write", "edit", "apply_patch"])
|
|
251
|
+
delete allTools[name];
|
|
252
|
+
const mcpReadOnly = getMcpReadOnlyToolIds();
|
|
253
|
+
for (const id of Object.keys(mcpTools)) {
|
|
254
|
+
if (!mcpReadOnly.has(id))
|
|
255
|
+
delete allTools[id];
|
|
256
|
+
}
|
|
257
|
+
const pluginReadOnly = getPluginReadOnlyIds();
|
|
258
|
+
for (const id of Object.keys(pluginTools)) {
|
|
259
|
+
if (!pluginReadOnly.has(id))
|
|
260
|
+
delete allTools[id];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return allTools;
|
|
264
|
+
}
|
|
265
|
+
function buildIterationPrompts(allTools, messages, taskState, loopGuard) {
|
|
266
|
+
const skillsPrompt = attachSkills(allTools, collectLoadedSkillNames(messages));
|
|
267
|
+
return {
|
|
268
|
+
stable: [skillsPrompt].filter((s) => s.length > 0),
|
|
269
|
+
volatile: [getMemorySystemPrompt(), formatTaskStatePrompt(taskState), loopGuard.promptHint()].filter((s) => s.length > 0),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/** Ephemeral per-request message carrying the volatile prompt sections. */
|
|
273
|
+
export const SESSION_STATE_HEADER = "## Session state (current, not part of the conversation)";
|
|
274
|
+
function sessionStateMessage(volatile) {
|
|
275
|
+
if (volatile.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
return { role: "system", content: [SESSION_STATE_HEADER, ...volatile].join("\n\n") };
|
|
278
|
+
}
|
|
279
|
+
function formatErrorMessage(msg) {
|
|
280
|
+
if (msg.includes("API key") || msg.includes("Unauthorized") || msg.includes("Forbidden")) {
|
|
281
|
+
return "Authentication error: Check your API key.";
|
|
282
|
+
}
|
|
283
|
+
if (msg.includes("429") || msg.includes("rate limit") || msg.includes("Rate limit")) {
|
|
284
|
+
return "Rate limited after retries. Please wait and try again.";
|
|
285
|
+
}
|
|
286
|
+
if (msg.includes("timeout") || msg.includes("ETIMEDOUT") || msg.includes("ECONNRESET")) {
|
|
287
|
+
return `Network error (retries exhausted): ${msg}`;
|
|
288
|
+
}
|
|
289
|
+
return msg;
|
|
290
|
+
}
|
|
291
|
+
function safeText(value) {
|
|
292
|
+
if (typeof value === "string")
|
|
293
|
+
return value;
|
|
168
294
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
295
|
+
return JSON.stringify(value) ?? String(value);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return String(value);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function isContextOverflowError(msg) {
|
|
302
|
+
return /context[\s_-]*(length|window|size)|context_length_exceeded|maximum context|too many tokens|prompt (is )?too long|reduce the (prompt )?length|token limit exceeded/i.test(msg);
|
|
303
|
+
}
|
|
304
|
+
function invokeCallback(fn, ...args) {
|
|
305
|
+
if (!fn)
|
|
306
|
+
return;
|
|
307
|
+
try {
|
|
308
|
+
fn(...args);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
log("error", `callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const INTERRUPTED_TOOL_RESULT = "Error: tool call was interrupted before a result was returned.";
|
|
315
|
+
function pairToolTurn(calls, results) {
|
|
316
|
+
const resultIds = new Set(results.map((r) => r.toolCallId));
|
|
317
|
+
const callIds = new Set(calls.map((c) => c.toolCallId));
|
|
318
|
+
const keptResults = results.filter((r) => callIds.has(r.toolCallId));
|
|
319
|
+
const synthetics = calls
|
|
320
|
+
.filter((c) => !resultIds.has(c.toolCallId))
|
|
321
|
+
.map((c) => ({
|
|
322
|
+
toolCallId: c.toolCallId,
|
|
323
|
+
toolName: c.toolName,
|
|
324
|
+
output: INTERRUPTED_TOOL_RESULT,
|
|
325
|
+
}));
|
|
326
|
+
return { calls, results: [...keptResults, ...synthetics] };
|
|
327
|
+
}
|
|
328
|
+
async function safeUsage(result) {
|
|
329
|
+
try {
|
|
330
|
+
return await result.usage;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function usageNonZero(usage) {
|
|
337
|
+
return (usage?.inputTokens ?? 0) > 0 || (usage?.outputTokens ?? 0) > 0;
|
|
338
|
+
}
|
|
339
|
+
function mergeUsage(a, b) {
|
|
340
|
+
if (!usageNonZero(b))
|
|
341
|
+
return a ?? b;
|
|
342
|
+
if (!usageNonZero(a))
|
|
343
|
+
return b;
|
|
344
|
+
const inputTokens = (a.inputTokens ?? 0) + (b.inputTokens ?? 0);
|
|
345
|
+
const outputTokens = (a.outputTokens ?? 0) + (b.outputTokens ?? 0);
|
|
346
|
+
return {
|
|
347
|
+
...a,
|
|
348
|
+
...b,
|
|
349
|
+
inputTokens,
|
|
350
|
+
outputTokens,
|
|
351
|
+
totalTokens: (a.totalTokens ?? 0) + (b.totalTokens ?? 0) || inputTokens + outputTokens,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function pickUsage(...candidates) {
|
|
355
|
+
const present = candidates.filter((u) => u != null);
|
|
356
|
+
if (present.length === 0)
|
|
357
|
+
return undefined;
|
|
358
|
+
const scored = present.filter(usageNonZero);
|
|
359
|
+
const pool = scored.length > 0 ? scored : present;
|
|
360
|
+
return pool.reduce((best, u) => {
|
|
361
|
+
const outDiff = (u.outputTokens ?? 0) - (best.outputTokens ?? 0);
|
|
362
|
+
if (outDiff !== 0)
|
|
363
|
+
return outDiff > 0 ? u : best;
|
|
364
|
+
return (u.inputTokens ?? 0) > (best.inputTokens ?? 0) ? u : best;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function injectUser(messages, text) {
|
|
368
|
+
const last = messages[messages.length - 1];
|
|
369
|
+
if (last && isSyntheticMessage(last) && last.role === "user") {
|
|
370
|
+
last.content = text;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const continueMsg = { role: "user", content: text };
|
|
374
|
+
messages.push(continueMsg);
|
|
375
|
+
markSyntheticMessage(continueMsg);
|
|
376
|
+
}
|
|
377
|
+
function continuePromptFor(guard) {
|
|
378
|
+
if (guard.researchCapped)
|
|
379
|
+
return DELIVER_PROMPT;
|
|
380
|
+
if (guard.webResearchCount >= 6)
|
|
381
|
+
return STEER_PROMPT;
|
|
382
|
+
return CONTINUE_PROMPT;
|
|
383
|
+
}
|
|
384
|
+
function stubWebResearchTools(tools) {
|
|
385
|
+
for (const name of WEB_RESEARCH_TOOLS) {
|
|
386
|
+
const current = tools[name];
|
|
387
|
+
if (!current)
|
|
388
|
+
continue;
|
|
389
|
+
tools[name] = {
|
|
390
|
+
...current,
|
|
391
|
+
execute: async () => RESEARCH_STUB_RESULT,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Drop the research tools for the rest of the turn. Removing them from the
|
|
397
|
+
* schema (instead of leaving a stub the model keeps calling) is what actually
|
|
398
|
+
* stops the model from burning steps on searches that cannot return anything.
|
|
399
|
+
*/
|
|
400
|
+
function removeWebResearchTools(tools) {
|
|
401
|
+
const removed = [];
|
|
402
|
+
for (const name of WEB_RESEARCH_TOOLS) {
|
|
403
|
+
if (!tools[name])
|
|
404
|
+
continue;
|
|
405
|
+
delete tools[name];
|
|
406
|
+
removed.push(name);
|
|
407
|
+
}
|
|
408
|
+
return removed;
|
|
409
|
+
}
|
|
410
|
+
async function applyCompaction(messages, model, tracker, cbs, callbacks, compactCfg, pruneOptions = {}) {
|
|
411
|
+
applyToolPrune(messages, pruneOptions);
|
|
412
|
+
try {
|
|
413
|
+
if (!(await needsCompaction(messages, tracker, compactCfg)))
|
|
414
|
+
return false;
|
|
415
|
+
if (cbs.onCompaction)
|
|
416
|
+
invokeCallback(cbs.onCompaction, "compacting_start");
|
|
417
|
+
else if (!callbacks)
|
|
418
|
+
console.log("\x1b[90m⟳ Compacting context...\x1b[0m");
|
|
419
|
+
const result = await compactMessages(messages, model, compactCfg);
|
|
420
|
+
if (!result.compacted)
|
|
421
|
+
return false;
|
|
422
|
+
if (result.usage && tracker)
|
|
423
|
+
tracker.add(result.usage);
|
|
424
|
+
messages.length = 0;
|
|
425
|
+
messages.push(...result.messages);
|
|
426
|
+
if (tracker)
|
|
427
|
+
tracker.resetContext();
|
|
428
|
+
resetActiveInstructionTracker();
|
|
429
|
+
if (cbs.onCompaction)
|
|
430
|
+
invokeCallback(cbs.onCompaction, `compacted_ok estimated_tokens=${estimateTokens(messages)}`);
|
|
431
|
+
else if (!callbacks)
|
|
432
|
+
console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
log("warn", `compaction failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
437
|
+
invokeCallback(cbs.onCompaction, "compacted_failed");
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function runOnceCore(messages, systemPrompt, modelId, abortSignal, callbacks, tracker, options) {
|
|
442
|
+
return runWithInstructionTracker(() => runOnceCoreLoop(messages, systemPrompt, modelId, abortSignal, callbacks, tracker, options));
|
|
443
|
+
}
|
|
444
|
+
async function runOnceCoreLoop(messages, systemPrompt, modelId, abortSignal, callbacks, tracker, options) {
|
|
445
|
+
const model = resolveModel(modelId, options?.providerName);
|
|
446
|
+
startRunLog();
|
|
447
|
+
log("info", `run start messages=${messages.length} model=${typeof model === "string" ? model : model.modelId}`);
|
|
448
|
+
const cbs = callbacks ?? {};
|
|
449
|
+
const cfg = getEffectiveConfig();
|
|
450
|
+
const sampling = cfg.sampling ?? {};
|
|
451
|
+
const temperature = options?.temperature ?? sampling.temperature;
|
|
452
|
+
const maxTokens = options?.maxTokens ?? sampling.maxTokens;
|
|
453
|
+
const topP = options?.topP ?? sampling.topP;
|
|
454
|
+
const taskState = options?.taskState ?? emptyTaskState();
|
|
455
|
+
captureGoal(taskState, messages);
|
|
456
|
+
const { cap: maxSteps, bounded: stepBounded } = resolveMaxSteps(options?.maxSteps ?? cfg.agent?.maxSteps);
|
|
457
|
+
const maxContinues = options?.maxContinues ?? cfg.agent?.maxContinues ?? DEFAULT_MAX_CONTINUES;
|
|
458
|
+
const autoContinue = options?.autoContinue ?? cfg.agent?.autoContinue ?? cfg.compaction?.autoContinue ?? true;
|
|
459
|
+
const maxEmptyAttempts = Math.max(1, options?.maxEmptyAttempts ?? cfg.agent?.maxEmptyAttempts ?? DEFAULT_MAX_EMPTY_ATTEMPTS);
|
|
460
|
+
const emptyRetryDelayMs = options?.emptyRetryDelayMs ?? cfg.agent?.emptyRetryDelayMs ?? EMPTY_RETRY_BASE_DELAY_MS;
|
|
461
|
+
const turnTimeLimitMs = resolveLimit(options?.turnTimeLimitMs, cfg.agent?.turnTimeLimitMs) ?? DEFAULT_TURN_TIME_LIMIT_MS;
|
|
462
|
+
const softStepLimit = resolveLimit(options?.softStepLimit, cfg.agent?.softStepLimit) ?? DEFAULT_SOFT_STEP_LIMIT;
|
|
463
|
+
const turnStartedAt = Date.now();
|
|
464
|
+
/** Time / step budget for the whole turn (0 disables either half). */
|
|
465
|
+
const overTurnBudget = (steps) => (turnTimeLimitMs > 0 && Date.now() - turnStartedAt >= turnTimeLimitMs) ||
|
|
466
|
+
(softStepLimit > 0 && steps >= softStepLimit);
|
|
467
|
+
const loopGuard = new DoomLoopDetector({
|
|
468
|
+
steerAfter: options?.researchSteerAfter ?? cfg.agent?.researchSteerAfter,
|
|
469
|
+
stopAfter: options?.researchStopAfter ?? cfg.agent?.researchStopAfter,
|
|
470
|
+
totalCap: options?.researchTotalCap ?? cfg.agent?.researchTotalCap,
|
|
471
|
+
});
|
|
472
|
+
/** Tokens spent on the system prompt + tool schemas of the current pass. */
|
|
473
|
+
const overhead = { tokens: 0 };
|
|
474
|
+
const compactCfg = {
|
|
475
|
+
abortSignal,
|
|
476
|
+
modelId,
|
|
477
|
+
taskGoal: taskState.goal || undefined,
|
|
478
|
+
get overheadTokens() {
|
|
479
|
+
return overhead.tokens;
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
const windowInfo = await getContextWindowInfo(modelId);
|
|
483
|
+
const ctxWindow = windowInfo.tokens;
|
|
484
|
+
const compactThreshold = ctxWindow * COMPACTION_RATIO;
|
|
485
|
+
/** Old tool payloads are only dropped once the context is actually filling up. */
|
|
486
|
+
const pruneBudget = () => ({
|
|
487
|
+
pressureTokens: ctxWindow * PRUNE_PRESSURE_RATIO,
|
|
488
|
+
targetTokens: ctxWindow * PRUNE_TARGET_RATIO,
|
|
489
|
+
overheadTokens: overhead.tokens,
|
|
490
|
+
});
|
|
491
|
+
const budgetLimit = cfg.budget?.maxCostUSD;
|
|
492
|
+
let budgetPrice = null;
|
|
493
|
+
if (budgetLimit != null && budgetLimit > 0) {
|
|
494
|
+
const provider = options?.providerName
|
|
495
|
+
? cfg.providers?.find((p) => p.name === options.providerName)
|
|
496
|
+
: getActiveProvider(cfg);
|
|
497
|
+
budgetPrice = await getModelPrice(modelId ?? provider?.defaultModel ?? "");
|
|
498
|
+
}
|
|
499
|
+
let budgetExceeded = false;
|
|
500
|
+
const checkBudget = () => {
|
|
501
|
+
if (!tracker || budgetLimit == null || budgetLimit <= 0)
|
|
502
|
+
return false;
|
|
503
|
+
const cost = estimateCost({ inputTokens: tracker.totalInputTokens, outputTokens: tracker.totalOutputTokens }, budgetPrice);
|
|
504
|
+
if (cost != null && cost > budgetLimit) {
|
|
505
|
+
budgetExceeded = true;
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
return false;
|
|
509
|
+
};
|
|
510
|
+
await applyCompaction(messages, model, tracker, cbs, callbacks, compactCfg, pruneBudget());
|
|
511
|
+
let totalSteps = 0;
|
|
512
|
+
let continues = 0;
|
|
513
|
+
let emptyAttempts = 0;
|
|
514
|
+
let wrapUpAsked = false;
|
|
515
|
+
let hasError = false;
|
|
516
|
+
let lastUsage;
|
|
517
|
+
let allowContextAbort = true;
|
|
518
|
+
const allTools = await buildRunTools(modelId, abortSignal, options?.planMode ?? false, tracker, taskState, {
|
|
519
|
+
loopGuard,
|
|
520
|
+
shouldStop: () => checkBudget() || overTurnBudget(totalSteps),
|
|
521
|
+
});
|
|
522
|
+
const finish = async (opts) => {
|
|
523
|
+
log("info", `run end steps=${totalSteps} continues=${continues} tokens_in=${lastUsage?.inputTokens ?? 0} tokens_out=${lastUsage?.outputTokens ?? 0}`);
|
|
524
|
+
endRunLog();
|
|
525
|
+
if (cbs.onRunFinish) {
|
|
526
|
+
try {
|
|
527
|
+
await cbs.onRunFinish({
|
|
528
|
+
stepCount: totalSteps,
|
|
529
|
+
usage: lastUsage,
|
|
530
|
+
contextTokens: tracker?.lastInputTokens ?? 0,
|
|
531
|
+
hasError,
|
|
532
|
+
aborted: opts.aborted,
|
|
533
|
+
maxStepsReached: opts.maxStepsReached,
|
|
534
|
+
budgetExceeded,
|
|
535
|
+
continues,
|
|
536
|
+
incomplete: opts.incomplete ?? false,
|
|
537
|
+
stopped: opts.stopped ?? false,
|
|
538
|
+
emptyResponse: opts.emptyResponse ?? false,
|
|
539
|
+
emptyAttempts,
|
|
540
|
+
wrapUp: opts.wrapUp ?? false,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
log("error", `onRunFinish failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
249
545
|
}
|
|
546
|
+
return { hasError };
|
|
250
547
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
callbacks.onThinkingDelta(end.thinking);
|
|
255
|
-
else
|
|
256
|
-
writeThinkingDelta(end.thinking);
|
|
548
|
+
if (hasError) {
|
|
549
|
+
printDivider();
|
|
550
|
+
return { hasError };
|
|
257
551
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
552
|
+
printDivider();
|
|
553
|
+
printDone(totalSteps, lastUsage, ctxWindow, tracker?.lastInputTokens);
|
|
554
|
+
if (budgetExceeded)
|
|
555
|
+
console.log(`\x1b[33m⚠ 已达到预算上限 ($${budgetLimit}),运行已中断\x1b[0m`);
|
|
556
|
+
if (opts.stopped)
|
|
557
|
+
console.log(`\x1b[33m⚠ ${LOOP_HALT_MESSAGE}\x1b[0m`);
|
|
558
|
+
if (opts.emptyResponse) {
|
|
559
|
+
console.log(`\x1b[33m⚠ ${emptyResponseMessage(emptyAttempts)}\x1b[0m`);
|
|
267
560
|
}
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
if (remaining)
|
|
271
|
-
process.stdout.write(remaining);
|
|
272
|
-
if (rawText.trim())
|
|
273
|
-
console.log();
|
|
561
|
+
else if (opts.wrapUp) {
|
|
562
|
+
console.log(`\x1b[33m⚠ ${WRAP_UP_MESSAGE}\x1b[0m`);
|
|
274
563
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
messages.push({ role: "assistant", content: cleaned });
|
|
278
|
-
let usage;
|
|
279
|
-
try {
|
|
280
|
-
usage = await result.usage;
|
|
564
|
+
else if (opts.maxStepsReached) {
|
|
565
|
+
console.log(`\x1b[33m⚠ 已达到自动续跑上限,发送消息继续\x1b[0m`);
|
|
281
566
|
}
|
|
282
|
-
|
|
283
|
-
|
|
567
|
+
else if (opts.incomplete) {
|
|
568
|
+
console.log(`\x1b[33m⚠ 还没有完整回复,发送消息可继续\x1b[0m`);
|
|
284
569
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
570
|
+
return { hasError };
|
|
571
|
+
};
|
|
572
|
+
while (true) {
|
|
573
|
+
if (abortSignal?.aborted)
|
|
574
|
+
return finish({ aborted: true, maxStepsReached: false });
|
|
575
|
+
if (checkBudget())
|
|
576
|
+
return finish({ aborted: true, maxStepsReached: false });
|
|
577
|
+
let inner;
|
|
578
|
+
nextRunPass();
|
|
579
|
+
try {
|
|
580
|
+
inner = await runInnerStream({
|
|
581
|
+
messages,
|
|
582
|
+
systemPrompt,
|
|
583
|
+
model,
|
|
584
|
+
abortSignal,
|
|
585
|
+
callbacks,
|
|
586
|
+
cbs,
|
|
587
|
+
tracker,
|
|
588
|
+
taskState,
|
|
589
|
+
allTools,
|
|
590
|
+
maxSteps,
|
|
591
|
+
stepBounded,
|
|
592
|
+
temperature,
|
|
593
|
+
maxTokens,
|
|
594
|
+
topP,
|
|
595
|
+
checkBudget,
|
|
596
|
+
overTurnBudget: (stepsInPass) => overTurnBudget(totalSteps + stepsInPass),
|
|
597
|
+
pruneBudget,
|
|
598
|
+
overhead,
|
|
599
|
+
compactThreshold,
|
|
600
|
+
allowContextAbort,
|
|
601
|
+
loopGuard,
|
|
602
|
+
onStepPersist: options?.onStepPersist,
|
|
603
|
+
});
|
|
289
604
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
607
|
+
log("error", msg);
|
|
608
|
+
hasError = true;
|
|
609
|
+
if (cbs.onStreamError)
|
|
610
|
+
invokeCallback(cbs.onStreamError, msg);
|
|
611
|
+
else
|
|
612
|
+
console.error(`\x1b[31m${formatErrorMessage(msg)}\x1b[0m`);
|
|
613
|
+
return finish({ aborted: Boolean(abortSignal?.aborted), maxStepsReached: false });
|
|
300
614
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
615
|
+
totalSteps += inner.stepCount;
|
|
616
|
+
lastUsage = mergeUsage(lastUsage, inner.usage);
|
|
617
|
+
if (inner.hasError)
|
|
618
|
+
hasError = true;
|
|
619
|
+
if (inner.budgetExceeded)
|
|
620
|
+
budgetExceeded = true;
|
|
621
|
+
if (inner.userAborted)
|
|
622
|
+
return finish({ aborted: true, maxStepsReached: false });
|
|
623
|
+
if (inner.doomLoop)
|
|
624
|
+
return finish({ aborted: false, maxStepsReached: false, stopped: true });
|
|
625
|
+
if (inner.hasError && !inner.contextPressure && !inner.toolPressure) {
|
|
626
|
+
return finish({ aborted: false, maxStepsReached: false });
|
|
310
627
|
}
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
628
|
+
if (budgetExceeded)
|
|
629
|
+
return finish({ aborted: true, maxStepsReached: false });
|
|
630
|
+
if (inner.toolPressure) {
|
|
631
|
+
applyToolPrune(messages, pruneBudget());
|
|
632
|
+
continue;
|
|
316
633
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
634
|
+
// The provider produced nothing at all (no text, no reasoning-backed reply,
|
|
635
|
+
// no tool call): a transport / gateway hiccup rather than model intent.
|
|
636
|
+
// Re-send the same request with backoff instead of nudging the model.
|
|
637
|
+
if (inner.emptyCompletion) {
|
|
638
|
+
emptyAttempts++;
|
|
639
|
+
if (emptyAttempts >= maxEmptyAttempts) {
|
|
640
|
+
log("warn", `empty response from provider ${emptyAttempts}x, ending run`);
|
|
641
|
+
return finish({ aborted: false, maxStepsReached: false, incomplete: true, emptyResponse: true });
|
|
642
|
+
}
|
|
643
|
+
continues++;
|
|
644
|
+
if (continues > maxContinues) {
|
|
645
|
+
return finish({ aborted: false, maxStepsReached: true, incomplete: true });
|
|
646
|
+
}
|
|
647
|
+
const delay = emptyRetryDelay(emptyAttempts, emptyRetryDelayMs);
|
|
648
|
+
log("warn", `empty response from provider, retry ${emptyAttempts}/${maxEmptyAttempts - 1} after ${delay}ms`);
|
|
649
|
+
if (cbs.onRetryNotice)
|
|
650
|
+
invokeCallback(cbs.onRetryNotice, {
|
|
651
|
+
kind: "empty_response",
|
|
652
|
+
attempt: emptyAttempts,
|
|
653
|
+
maxAttempts: maxEmptyAttempts - 1,
|
|
654
|
+
delayMs: delay,
|
|
655
|
+
});
|
|
656
|
+
else if (!callbacks)
|
|
657
|
+
console.log(`\x1b[90m⟳ 模型返回空响应,重试 ${emptyAttempts}/${maxEmptyAttempts - 1}…\x1b[0m`);
|
|
658
|
+
await sleep(delay);
|
|
659
|
+
continue;
|
|
326
660
|
}
|
|
327
|
-
|
|
328
|
-
|
|
661
|
+
const hitResearchLimit = inner.researchCap || inner.researchSteer;
|
|
662
|
+
const forceDeliver = !loopGuard.producedArtifact &&
|
|
663
|
+
(hitResearchLimit ||
|
|
664
|
+
(inner.lastStepHadTools && (loopGuard.researchCapped || (!autoContinue && loopGuard.webResearchCount > 0))));
|
|
665
|
+
const stalled = inner.lastStepHadTools || inner.xmlToolFollowUp || inner.providerStall;
|
|
666
|
+
const shouldKeepGoing = inner.xmlToolFollowUp ||
|
|
667
|
+
(autoContinue && (inner.contextPressure || inner.lastStepHadTools || inner.providerStall));
|
|
668
|
+
// A finished answer always wins: never override it with a budget notice.
|
|
669
|
+
if (!forceDeliver && !shouldKeepGoing) {
|
|
670
|
+
const capped = !autoContinue && inner.maxStepsReached && inner.lastStepHadTools;
|
|
671
|
+
if (capped && !callbacks) {
|
|
672
|
+
console.log(`\x1b[33m⚠ 本轮已达到步数上限(${maxSteps}),发送消息继续\x1b[0m`);
|
|
673
|
+
}
|
|
674
|
+
return finish({ aborted: false, maxStepsReached: capped, incomplete: stalled && !capped });
|
|
329
675
|
}
|
|
330
|
-
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (result.shouldContinue) {
|
|
336
|
-
const continueText = result.replayText ||
|
|
337
|
-
"Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.";
|
|
338
|
-
messages.push({ role: "user", content: continueText });
|
|
676
|
+
// Out of time / steps for this turn: ask for a wrap-up once, then stop.
|
|
677
|
+
if (inner.wrapUp || overTurnBudget(totalSteps)) {
|
|
678
|
+
if (wrapUpAsked) {
|
|
679
|
+
log("warn", `turn budget spent after wrap-up (steps=${totalSteps})`);
|
|
680
|
+
return finish({ aborted: false, maxStepsReached: false, incomplete: true, wrapUp: true });
|
|
339
681
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
682
|
+
wrapUpAsked = true;
|
|
683
|
+
continues++;
|
|
684
|
+
if (continues > maxContinues) {
|
|
685
|
+
return finish({ aborted: false, maxStepsReached: true, incomplete: true });
|
|
344
686
|
}
|
|
345
|
-
|
|
346
|
-
|
|
687
|
+
log("info", `auto-continue reason=wrap_up steps=${totalSteps} elapsed=${Date.now() - turnStartedAt}ms`);
|
|
688
|
+
if (cbs.onRetryNotice)
|
|
689
|
+
invokeCallback(cbs.onRetryNotice, {
|
|
690
|
+
kind: "wrap_up",
|
|
691
|
+
attempt: 1,
|
|
692
|
+
maxAttempts: 1,
|
|
693
|
+
delayMs: 0,
|
|
694
|
+
});
|
|
695
|
+
else if (!callbacks)
|
|
696
|
+
console.log(`\x1b[90m⟳ 本轮预算用尽,正在收尾…\x1b[0m`);
|
|
697
|
+
await applyCompaction(messages, model, tracker, cbs, callbacks, compactCfg, pruneBudget());
|
|
698
|
+
injectUser(messages, WRAP_UP_PROMPT);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (forceDeliver) {
|
|
702
|
+
continues++;
|
|
703
|
+
if (continues > maxContinues) {
|
|
704
|
+
return finish({ aborted: false, maxStepsReached: true, incomplete: true });
|
|
347
705
|
}
|
|
706
|
+
removeWebResearchTools(allTools);
|
|
707
|
+
loopGuard.markCapped();
|
|
708
|
+
log("info", `auto-continue reason=${hitResearchLimit ? (inner.researchCap ? "research_cap" : "research_steer") : "awaiting_deliverable"}`);
|
|
709
|
+
await applyCompaction(messages, model, tracker, cbs, callbacks, compactCfg, pruneBudget());
|
|
710
|
+
injectUser(messages, DELIVER_PROMPT);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
continues++;
|
|
714
|
+
if (continues > maxContinues) {
|
|
715
|
+
return finish({ aborted: false, maxStepsReached: true, incomplete: stalled });
|
|
348
716
|
}
|
|
717
|
+
log("info", `auto-continue reason=${inner.xmlToolFollowUp ? "xml_search" : inner.contextPressure ? "context" : inner.providerStall ? "provider_stall" : "tools_without_reply"}`);
|
|
718
|
+
const compacted = await applyCompaction(messages, model, tracker, cbs, callbacks, { ...compactCfg, force: inner.contextPressure }, pruneBudget());
|
|
719
|
+
if (inner.contextPressure && !compacted)
|
|
720
|
+
allowContextAbort = false;
|
|
721
|
+
injectUser(messages, continuePromptFor(loopGuard));
|
|
349
722
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Accumulates the current assistant turn (text + tool call/result pairs) and
|
|
726
|
+
* flushes it into the shared message history at step boundaries.
|
|
727
|
+
*/
|
|
728
|
+
class TurnAssembler {
|
|
729
|
+
messages;
|
|
730
|
+
onFlush;
|
|
731
|
+
stepText = "";
|
|
732
|
+
toolCalls = [];
|
|
733
|
+
toolResults = [];
|
|
734
|
+
innerSteps = 0;
|
|
735
|
+
lastStepHadTools = false;
|
|
736
|
+
hadTools = false;
|
|
737
|
+
hadAssistantText = false;
|
|
738
|
+
/** Estimated model output of this pass (assistant text + tool-call arguments). */
|
|
739
|
+
producedTokens = 0;
|
|
740
|
+
constructor(messages, onFlush) {
|
|
741
|
+
this.messages = messages;
|
|
742
|
+
this.onFlush = onFlush;
|
|
359
743
|
}
|
|
360
|
-
|
|
361
|
-
|
|
744
|
+
get pendingResults() {
|
|
745
|
+
return this.toolResults.length;
|
|
362
746
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
747
|
+
get hasUnpairedCalls() {
|
|
748
|
+
const resultIds = new Set(this.toolResults.map((r) => r.toolCallId));
|
|
749
|
+
return this.toolCalls.some((c) => !resultIds.has(c.toolCallId));
|
|
750
|
+
}
|
|
751
|
+
appendText(display) {
|
|
752
|
+
this.stepText += display;
|
|
753
|
+
}
|
|
754
|
+
recordCall(call) {
|
|
755
|
+
this.toolCalls.push(call);
|
|
756
|
+
}
|
|
757
|
+
recordResult(result) {
|
|
758
|
+
this.toolResults.push(result);
|
|
759
|
+
}
|
|
760
|
+
/** Placeholder id for tool-call events missing one (kept stable per step). */
|
|
761
|
+
nextMissingCallId() {
|
|
762
|
+
return `missing-${this.innerSteps}-${this.toolCalls.length}`;
|
|
763
|
+
}
|
|
764
|
+
flushStep() {
|
|
765
|
+
const hasTools = this.toolCalls.length > 0 || this.toolResults.length > 0;
|
|
766
|
+
const hasText = Boolean(this.stepText.trim());
|
|
767
|
+
if (!hasText && !hasTools)
|
|
768
|
+
return;
|
|
769
|
+
this.lastStepHadTools = hasTools;
|
|
770
|
+
if (hasTools)
|
|
771
|
+
this.hadTools = true;
|
|
772
|
+
if (hasText)
|
|
773
|
+
this.hadAssistantText = true;
|
|
774
|
+
this.producedTokens += estimateStringTokens(this.stepText);
|
|
775
|
+
for (const call of this.toolCalls) {
|
|
776
|
+
this.producedTokens += estimateStringTokens(safeText(call.input));
|
|
777
|
+
}
|
|
778
|
+
const paired = pairToolTurn(this.toolCalls, this.toolResults);
|
|
779
|
+
pushTurn(this.messages, this.stepText, paired.calls, paired.results);
|
|
780
|
+
this.innerSteps++;
|
|
781
|
+
this.stepText = "";
|
|
782
|
+
this.toolCalls = [];
|
|
783
|
+
this.toolResults = [];
|
|
784
|
+
this.onFlush?.({ steps: this.innerSteps });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Splits thinking and inline <web_search> dumps from displayable text, then
|
|
789
|
+
* fans both out to the callbacks (HTTP/TUI) or the TTY (single-shot).
|
|
790
|
+
*/
|
|
791
|
+
class StreamRenderer {
|
|
792
|
+
cbs;
|
|
793
|
+
interactive;
|
|
794
|
+
md = new MarkdownRenderer();
|
|
795
|
+
thinkingSplit = new ThinkingBodySplitter();
|
|
796
|
+
xmlSplit = new XmlSearchSplitter();
|
|
797
|
+
rawText = "";
|
|
798
|
+
constructor(cbs, interactive) {
|
|
799
|
+
this.cbs = cbs;
|
|
800
|
+
this.interactive = interactive;
|
|
801
|
+
}
|
|
802
|
+
emitThinking(t) {
|
|
385
803
|
if (!t)
|
|
386
804
|
return;
|
|
387
|
-
if (
|
|
388
|
-
|
|
805
|
+
if (this.cbs.onThinkingDelta)
|
|
806
|
+
invokeCallback(this.cbs.onThinkingDelta, t);
|
|
389
807
|
else
|
|
390
808
|
writeThinkingDelta(t);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
emitThinking(thinking);
|
|
398
|
-
if (display) {
|
|
399
|
-
rawText += display;
|
|
400
|
-
assistantText += display;
|
|
401
|
-
if (callbacks?.onAssistantDisplayDelta)
|
|
402
|
-
callbacks.onAssistantDisplayDelta(display);
|
|
403
|
-
else {
|
|
404
|
-
const formatted = md.write(display);
|
|
405
|
-
if (formatted)
|
|
406
|
-
process.stdout.write(formatted);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
case "tool-call": {
|
|
412
|
-
// Doom loop detection
|
|
413
|
-
if (doomLoop.record(event.toolName, event.input)) {
|
|
414
|
-
const warning = `\x1b[33m⚠ Doom loop detected: "${event.toolName}" called ${3} times with same args. Breaking loop.\x1b[0m`;
|
|
415
|
-
if (callbacks?.onStreamError)
|
|
416
|
-
callbacks.onStreamError(warning);
|
|
417
|
-
else
|
|
418
|
-
console.log(`\n${warning}`);
|
|
419
|
-
hasError = true;
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
const splitFlush = thinkingSplit.flush();
|
|
423
|
-
emitThinking(splitFlush.thinking);
|
|
424
|
-
if (splitFlush.display) {
|
|
425
|
-
rawText += splitFlush.display;
|
|
426
|
-
assistantText += splitFlush.display;
|
|
427
|
-
if (callbacks?.onAssistantDisplayDelta)
|
|
428
|
-
callbacks.onAssistantDisplayDelta(splitFlush.display);
|
|
429
|
-
else {
|
|
430
|
-
const extra = md.write(splitFlush.display);
|
|
431
|
-
if (extra)
|
|
432
|
-
process.stdout.write(extra);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
if (!callbacks) {
|
|
436
|
-
const flushed = md.flush();
|
|
437
|
-
if (flushed)
|
|
438
|
-
process.stdout.write(flushed);
|
|
439
|
-
if (rawText.trim())
|
|
440
|
-
console.log();
|
|
441
|
-
}
|
|
442
|
-
rawText = "";
|
|
443
|
-
if (callbacks?.onToolCall)
|
|
444
|
-
callbacks.onToolCall(event.toolName, event.input);
|
|
445
|
-
else
|
|
446
|
-
printToolCall(event.toolName, event.input);
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
case "tool-result":
|
|
450
|
-
if (callbacks?.onToolResult)
|
|
451
|
-
callbacks.onToolResult(event.toolName, event.output);
|
|
452
|
-
else
|
|
453
|
-
printToolResult(event.toolName, event.output);
|
|
454
|
-
break;
|
|
455
|
-
case "error":
|
|
456
|
-
hasError = true;
|
|
457
|
-
const errorMsg = String(event.error);
|
|
458
|
-
if (callbacks?.onStreamError) {
|
|
459
|
-
callbacks.onStreamError(errorMsg);
|
|
460
|
-
}
|
|
461
|
-
else if (errorMsg.includes("Forbidden") || errorMsg.includes("Unauthorized") || errorMsg.includes("API key")) {
|
|
462
|
-
console.error(`\x1b[31mAuthentication error: Check your API key.\x1b[0m`);
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
console.error(`\x1b[31mError: ${errorMsg}\x1b[0m`);
|
|
466
|
-
}
|
|
467
|
-
break;
|
|
468
|
-
case "finish":
|
|
469
|
-
break;
|
|
809
|
+
}
|
|
810
|
+
emitDisplay(delta) {
|
|
811
|
+
try {
|
|
812
|
+
if (this.cbs.onAssistantDisplayDelta) {
|
|
813
|
+
this.cbs.onAssistantDisplayDelta(delta);
|
|
814
|
+
return;
|
|
470
815
|
}
|
|
816
|
+
const formatted = this.md.write(delta);
|
|
817
|
+
if (formatted)
|
|
818
|
+
process.stdout.write(formatted);
|
|
471
819
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
820
|
+
catch (err) {
|
|
821
|
+
log("error", `display callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
feedReasoning(text) {
|
|
825
|
+
this.emitThinking(text);
|
|
826
|
+
}
|
|
827
|
+
mirror(display) {
|
|
828
|
+
if (!display)
|
|
829
|
+
return;
|
|
830
|
+
this.rawText += display;
|
|
831
|
+
this.emitDisplay(display);
|
|
832
|
+
}
|
|
833
|
+
pipeXml(display, flush) {
|
|
834
|
+
const slices = display ? this.xmlSplit.feed(display) : [];
|
|
835
|
+
if (flush)
|
|
836
|
+
slices.push(...this.xmlSplit.flush());
|
|
837
|
+
const out = [];
|
|
838
|
+
for (const sl of slices) {
|
|
839
|
+
if (sl.type === "display") {
|
|
840
|
+
if (!sl.text)
|
|
841
|
+
continue;
|
|
842
|
+
this.mirror(sl.text);
|
|
843
|
+
out.push(sl);
|
|
844
|
+
}
|
|
479
845
|
else {
|
|
480
|
-
|
|
481
|
-
if (tail)
|
|
482
|
-
process.stdout.write(tail);
|
|
846
|
+
out.push(sl);
|
|
483
847
|
}
|
|
484
848
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
849
|
+
return out;
|
|
850
|
+
}
|
|
851
|
+
feedText(text) {
|
|
852
|
+
const { display, thinking } = this.thinkingSplit.feed(text);
|
|
853
|
+
this.emitThinking(thinking);
|
|
854
|
+
return this.pipeXml(display, false);
|
|
855
|
+
}
|
|
856
|
+
flushSplitText() {
|
|
857
|
+
const out = this.thinkingSplit.flush();
|
|
858
|
+
this.emitThinking(out.thinking);
|
|
859
|
+
return this.pipeXml(out.display, true);
|
|
860
|
+
}
|
|
861
|
+
flushMarkdown() {
|
|
862
|
+
if (this.interactive) {
|
|
863
|
+
try {
|
|
864
|
+
const flushed = this.md.flush();
|
|
865
|
+
if (flushed)
|
|
866
|
+
process.stdout.write(flushed);
|
|
867
|
+
}
|
|
868
|
+
catch { }
|
|
869
|
+
if (this.rawText.trim())
|
|
490
870
|
console.log();
|
|
491
871
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
872
|
+
this.rawText = "";
|
|
873
|
+
}
|
|
874
|
+
flushOutput() {
|
|
875
|
+
const slices = this.flushSplitText();
|
|
876
|
+
this.flushMarkdown();
|
|
877
|
+
return slices;
|
|
878
|
+
}
|
|
879
|
+
flushOnError() {
|
|
880
|
+
if (this.interactive && this.rawText.trim())
|
|
881
|
+
console.log();
|
|
882
|
+
const slices = this.flushSplitText();
|
|
883
|
+
if (this.interactive) {
|
|
884
|
+
try {
|
|
885
|
+
process.stdout.write(this.md.flush());
|
|
886
|
+
}
|
|
887
|
+
catch { }
|
|
495
888
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
889
|
+
this.rawText = "";
|
|
890
|
+
return slices;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Explicit state machine for one streaming pass: owns the abort controller,
|
|
895
|
+
* stop flags, doom-loop detector, turn assembly and rendering, and reacts to
|
|
896
|
+
* each fullStream event via handleEvent (returns true to stop the loop).
|
|
897
|
+
*/
|
|
898
|
+
class InnerStreamMachine {
|
|
899
|
+
params;
|
|
900
|
+
stop = {
|
|
901
|
+
budget: false,
|
|
902
|
+
doom: false,
|
|
903
|
+
steer: false,
|
|
904
|
+
cap: false,
|
|
905
|
+
context: false,
|
|
906
|
+
tools: false,
|
|
907
|
+
stall: false,
|
|
908
|
+
wrapUp: false,
|
|
909
|
+
};
|
|
910
|
+
hasError = false;
|
|
911
|
+
assembler;
|
|
912
|
+
renderer;
|
|
913
|
+
usageSteps = 0;
|
|
914
|
+
innerController = new AbortController();
|
|
915
|
+
forwardAbort = () => this.innerController.abort();
|
|
916
|
+
xmlSearchRecovered = false;
|
|
917
|
+
answerAfterXml = false;
|
|
918
|
+
pendingLoop = null;
|
|
919
|
+
stepFinishSeen = false;
|
|
920
|
+
stepUsage;
|
|
921
|
+
totalUsage;
|
|
922
|
+
constructor(params) {
|
|
923
|
+
this.params = params;
|
|
924
|
+
this.assembler = new TurnAssembler(params.messages, (info) => {
|
|
925
|
+
if (params.onStepPersist)
|
|
926
|
+
invokeCallback(params.onStepPersist, info);
|
|
927
|
+
});
|
|
928
|
+
this.renderer = new StreamRenderer(params.cbs, !params.callbacks);
|
|
929
|
+
}
|
|
930
|
+
get signal() {
|
|
931
|
+
return this.innerController.signal;
|
|
932
|
+
}
|
|
933
|
+
attachAbort() {
|
|
934
|
+
this.params.abortSignal?.addEventListener("abort", this.forwardAbort, { once: true });
|
|
935
|
+
}
|
|
936
|
+
detachAbort() {
|
|
937
|
+
this.params.abortSignal?.removeEventListener("abort", this.forwardAbort);
|
|
938
|
+
}
|
|
939
|
+
/** streamText onStepFinish: budget / context-pressure / tool-output pruning checks. */
|
|
940
|
+
onStepFinish(usage) {
|
|
941
|
+
const { tracker, checkBudget, allowContextAbort, compactThreshold, messages, overTurnBudget } = this.params;
|
|
942
|
+
if (usage && tracker)
|
|
943
|
+
tracker.update(usage);
|
|
944
|
+
this.usageSteps++;
|
|
945
|
+
if (checkBudget()) {
|
|
946
|
+
this.stop.budget = true;
|
|
947
|
+
this.innerController.abort();
|
|
499
948
|
}
|
|
500
|
-
|
|
501
|
-
|
|
949
|
+
else if (overTurnBudget(this.assembler.innerSteps)) {
|
|
950
|
+
// Do not let one pass run past the turn budget: stop here so the outer
|
|
951
|
+
// loop can ask for a wrap-up.
|
|
952
|
+
this.stop.wrapUp = true;
|
|
953
|
+
this.innerController.abort();
|
|
502
954
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
955
|
+
else if (allowContextAbort && this.usageSteps >= 2 && tracker && tracker.lastInputTokens > compactThreshold) {
|
|
956
|
+
this.stop.context = true;
|
|
957
|
+
this.innerController.abort();
|
|
506
958
|
}
|
|
507
|
-
if (
|
|
508
|
-
|
|
959
|
+
else if (this.usageSteps >= KEEP_RECENT_STEPS_BEFORE_PRUNE &&
|
|
960
|
+
pruneToolOutputs(messages, this.params.pruneBudget()) !== messages) {
|
|
961
|
+
this.stop.tools = true;
|
|
962
|
+
this.innerController.abort();
|
|
509
963
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
964
|
+
}
|
|
965
|
+
noteStepUsage(usage) {
|
|
966
|
+
this.stepUsage = mergeUsage(this.stepUsage, usage);
|
|
967
|
+
}
|
|
968
|
+
resolvedUsage(fromResult) {
|
|
969
|
+
const reported = pickUsage(fromResult, this.totalUsage, this.stepUsage);
|
|
970
|
+
if (usageNonZero(reported))
|
|
971
|
+
return reported;
|
|
972
|
+
// We cut the stream ourselves (research steer, context pressure, stall…),
|
|
973
|
+
// so the provider never sent its usage chunk. Estimate instead of dropping
|
|
974
|
+
// the pass from cost / context accounting entirely.
|
|
975
|
+
const estimated = this.estimateUsage();
|
|
976
|
+
if (!estimated)
|
|
977
|
+
return reported;
|
|
978
|
+
if (this.params.tracker)
|
|
979
|
+
this.params.tracker.update(estimated);
|
|
980
|
+
log("info", `usage estimated for aborted pass in=${estimated.inputTokens} out=${estimated.outputTokens}`);
|
|
981
|
+
return estimated;
|
|
982
|
+
}
|
|
983
|
+
/** Rough usage for a pass whose provider usage never arrived. */
|
|
984
|
+
estimateUsage() {
|
|
985
|
+
const outputTokens = this.assembler.producedTokens;
|
|
986
|
+
if (outputTokens === 0)
|
|
987
|
+
return undefined;
|
|
988
|
+
const inputTokens = estimateTokens(this.params.messages) + this.params.overhead.tokens;
|
|
989
|
+
return {
|
|
990
|
+
inputTokens,
|
|
991
|
+
outputTokens,
|
|
992
|
+
totalTokens: inputTokens + outputTokens,
|
|
993
|
+
inputTokenDetails: { noCacheTokens: inputTokens, cacheReadTokens: undefined, cacheWriteTokens: undefined },
|
|
994
|
+
outputTokenDetails: { textTokens: outputTokens, reasoningTokens: undefined },
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
/** streamText onError callback (step failed, will retry). */
|
|
998
|
+
onStepError(error) {
|
|
999
|
+
const msg = String(error);
|
|
1000
|
+
if (isContextOverflowError(msg))
|
|
1001
|
+
this.stop.context = true;
|
|
1002
|
+
log("warn", `step failed (will retry): ${msg}`);
|
|
1003
|
+
}
|
|
1004
|
+
async applySlices(slices) {
|
|
1005
|
+
for (const sl of slices) {
|
|
1006
|
+
if (sl.type === "display") {
|
|
1007
|
+
if (!sl.text)
|
|
1008
|
+
continue;
|
|
1009
|
+
if (this.assembler.pendingResults > 0)
|
|
1010
|
+
this.assembler.flushStep();
|
|
1011
|
+
this.assembler.appendText(sl.text);
|
|
1012
|
+
if (this.xmlSearchRecovered && sl.text.trim())
|
|
1013
|
+
this.answerAfterXml = true;
|
|
1014
|
+
}
|
|
1015
|
+
else if (await this.materializeXmlSearch(sl.block)) {
|
|
1016
|
+
return true;
|
|
514
1017
|
}
|
|
515
|
-
printDivider();
|
|
516
|
-
const { getContextWindow } = await import("./context-window.js");
|
|
517
|
-
const ctxWindow = await getContextWindow(modelId);
|
|
518
|
-
printDone(stepCount, usage, ctxWindow);
|
|
519
1018
|
}
|
|
1019
|
+
return false;
|
|
520
1020
|
}
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1021
|
+
applyLoopAction(action) {
|
|
1022
|
+
if (action === "ok")
|
|
1023
|
+
return false;
|
|
1024
|
+
if (action === "halt") {
|
|
1025
|
+
this.stop.doom = true;
|
|
1026
|
+
this.innerController.abort();
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
this.pendingLoop = action;
|
|
1030
|
+
stubWebResearchTools(this.params.allTools);
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
/** End the inner stream after research steer/cap, but only once every tool call has a result. */
|
|
1034
|
+
settlePendingLoop() {
|
|
1035
|
+
if (!this.pendingLoop)
|
|
1036
|
+
return false;
|
|
1037
|
+
if (this.assembler.hasUnpairedCalls)
|
|
1038
|
+
return false;
|
|
1039
|
+
this.stop.steer = this.pendingLoop === "steer";
|
|
1040
|
+
this.stop.cap = this.pendingLoop === "cap";
|
|
1041
|
+
this.pendingLoop = null;
|
|
1042
|
+
this.innerController.abort();
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
async flushAndSettleLoop() {
|
|
1046
|
+
if (this.assembler.hasUnpairedCalls)
|
|
1047
|
+
return false;
|
|
1048
|
+
if (await this.flushOutputAndStep())
|
|
1049
|
+
return true;
|
|
1050
|
+
return this.settlePendingLoop();
|
|
1051
|
+
}
|
|
1052
|
+
async materializeXmlSearch(block) {
|
|
1053
|
+
const { cbs, loopGuard } = this.params;
|
|
1054
|
+
const toolName = "search_web";
|
|
1055
|
+
const query = block.query.trim();
|
|
1056
|
+
const input = { query };
|
|
1057
|
+
const toolCallId = this.assembler.nextMissingCallId();
|
|
1058
|
+
this.renderer.flushMarkdown();
|
|
1059
|
+
this.assembler.recordCall({ toolCallId, toolName, input });
|
|
1060
|
+
logToolCall(toolName, input);
|
|
1061
|
+
if (cbs.onToolCall)
|
|
1062
|
+
invokeCallback(cbs.onToolCall, toolName, input, toolCallId);
|
|
1063
|
+
else
|
|
1064
|
+
printToolCall(toolName, input);
|
|
1065
|
+
if (loopGuard.researchCapped) {
|
|
1066
|
+
const output = RESEARCH_STUB_RESULT;
|
|
1067
|
+
this.assembler.recordResult({ toolCallId, toolName, output });
|
|
1068
|
+
logToolResult(toolName, output);
|
|
1069
|
+
if (cbs.onToolResult)
|
|
1070
|
+
invokeCallback(cbs.onToolResult, toolName, output, { toolCallId, isError: false });
|
|
1071
|
+
else
|
|
1072
|
+
printToolResult(toolName, output, false);
|
|
1073
|
+
this.xmlSearchRecovered = true;
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
if (this.applyLoopAction(loopGuard.observe(toolName, input))) {
|
|
1077
|
+
const output = LOOP_HALT_MESSAGE;
|
|
1078
|
+
this.assembler.recordResult({ toolCallId, toolName, output });
|
|
1079
|
+
logToolResult(toolName, output);
|
|
1080
|
+
if (cbs.onToolResult)
|
|
1081
|
+
invokeCallback(cbs.onToolResult, toolName, output, { toolCallId, isError: false });
|
|
1082
|
+
else
|
|
1083
|
+
printToolResult(toolName, output, false);
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
const output = block.kind === "results" ? block.body : await this.executeSearchWeb(query, toolCallId);
|
|
1087
|
+
const isError = typeof output === "string" && /^(error|search error)\b/i.test(output);
|
|
1088
|
+
this.assembler.recordResult({ toolCallId, toolName, output });
|
|
1089
|
+
logToolResult(toolName, output);
|
|
1090
|
+
if (cbs.onToolResult)
|
|
1091
|
+
invokeCallback(cbs.onToolResult, toolName, output, { toolCallId, isError });
|
|
1092
|
+
else
|
|
1093
|
+
printToolResult(toolName, output, isError);
|
|
1094
|
+
this.xmlSearchRecovered = true;
|
|
1095
|
+
this.applyLoopAction(loopGuard.observeResult(toolName, isError ? "error" : output));
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
async executeSearchWeb(query, toolCallId) {
|
|
1099
|
+
if (!query)
|
|
1100
|
+
return 'Search error: "query" must be a non-empty string.';
|
|
1101
|
+
const search = this.params.allTools.search_web;
|
|
1102
|
+
if (typeof search?.execute !== "function")
|
|
1103
|
+
return "Search error: search_web is unavailable.";
|
|
1104
|
+
try {
|
|
1105
|
+
const out = await search.execute({ query }, { toolCallId, messages: this.params.messages, abortSignal: this.signal });
|
|
1106
|
+
return typeof out === "string" ? out : safeText(out);
|
|
1107
|
+
}
|
|
1108
|
+
catch (err) {
|
|
1109
|
+
return `Search error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
async flushOutputAndStep() {
|
|
1113
|
+
const stop = await this.applySlices(this.renderer.flushOutput());
|
|
1114
|
+
this.assembler.flushStep();
|
|
1115
|
+
return stop;
|
|
1116
|
+
}
|
|
1117
|
+
/** Handle one fullStream event; returns true to break out of the stream loop. */
|
|
1118
|
+
async handleEvent(event) {
|
|
1119
|
+
const { cbs, loopGuard } = this.params;
|
|
1120
|
+
switch (event.type) {
|
|
1121
|
+
case "start-step":
|
|
1122
|
+
this.stepFinishSeen = false;
|
|
1123
|
+
if (!this.assembler.hasUnpairedCalls)
|
|
1124
|
+
this.assembler.flushStep();
|
|
1125
|
+
return false;
|
|
1126
|
+
case "finish-step":
|
|
1127
|
+
this.stepFinishSeen = true;
|
|
1128
|
+
this.noteStepUsage(event.usage);
|
|
1129
|
+
return await this.flushAndSettleLoop();
|
|
1130
|
+
case "finish":
|
|
1131
|
+
if (usageNonZero(event.totalUsage))
|
|
1132
|
+
this.totalUsage = event.totalUsage;
|
|
1133
|
+
return false;
|
|
1134
|
+
case "reasoning-delta": {
|
|
1135
|
+
if (event.text)
|
|
1136
|
+
this.renderer.feedReasoning(event.text);
|
|
1137
|
+
return false;
|
|
536
1138
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
messages.push({ role: "assistant", content: cleaned });
|
|
1139
|
+
case "text-delta": {
|
|
1140
|
+
if (this.assembler.pendingResults > 0 && (await this.flushOutputAndStep()))
|
|
1141
|
+
return true;
|
|
1142
|
+
return await this.applySlices(this.renderer.feedText(event.text));
|
|
542
1143
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
1144
|
+
case "tool-call": {
|
|
1145
|
+
if (this.assembler.pendingResults > 0 && (await this.flushOutputAndStep()))
|
|
1146
|
+
return true;
|
|
1147
|
+
if (await this.applySlices(this.renderer.flushOutput()))
|
|
1148
|
+
return true;
|
|
1149
|
+
const toolName = event.toolName || "unknown";
|
|
1150
|
+
const toolCallId = event.toolCallId || this.assembler.nextMissingCallId();
|
|
1151
|
+
this.assembler.recordCall({ toolCallId, toolName, input: event.input });
|
|
1152
|
+
logToolCall(toolName, event.input);
|
|
1153
|
+
if (cbs.onToolCall)
|
|
1154
|
+
invokeCallback(cbs.onToolCall, toolName, event.input, toolCallId);
|
|
1155
|
+
else
|
|
1156
|
+
printToolCall(toolName, event.input);
|
|
1157
|
+
if (this.applyLoopAction(loopGuard.observe(toolName, event.input))) {
|
|
1158
|
+
await this.flushOutputAndStep();
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
return false;
|
|
546
1162
|
}
|
|
547
|
-
|
|
548
|
-
|
|
1163
|
+
case "tool-result":
|
|
1164
|
+
case "tool-error": {
|
|
1165
|
+
const isError = event.type === "tool-error";
|
|
1166
|
+
const output = isError ? event.error : event.output;
|
|
1167
|
+
const displayed = isError ? `Error: ${safeText(event.error)}` : event.output;
|
|
1168
|
+
this.assembler.recordResult({
|
|
1169
|
+
toolCallId: event.toolCallId,
|
|
1170
|
+
toolName: event.toolName || "unknown",
|
|
1171
|
+
output,
|
|
1172
|
+
});
|
|
1173
|
+
logToolResult(event.toolName, displayed);
|
|
1174
|
+
if (cbs.onToolResult)
|
|
1175
|
+
invokeCallback(cbs.onToolResult, event.toolName, displayed, { toolCallId: event.toolCallId, isError });
|
|
1176
|
+
else
|
|
1177
|
+
printToolResult(event.toolName, displayed, isError);
|
|
1178
|
+
// Research productivity is judged on the result, not the call.
|
|
1179
|
+
this.applyLoopAction(loopGuard.observeResult(event.toolName || "unknown", isError ? "error" : output));
|
|
1180
|
+
if (this.stepFinishSeen || this.pendingLoop)
|
|
1181
|
+
return await this.flushAndSettleLoop();
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
case "error": {
|
|
1185
|
+
const errorMsg = String(event.error);
|
|
1186
|
+
if (isProviderStallError(event.error)) {
|
|
1187
|
+
this.stop.stall = true;
|
|
1188
|
+
log("warn", `provider stall: ${describeError(event.error)}`);
|
|
1189
|
+
await this.flushOutputAndStep();
|
|
1190
|
+
this.innerController.abort();
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
if (isContextOverflowError(errorMsg)) {
|
|
1194
|
+
this.stop.context = true;
|
|
1195
|
+
log("warn", `context overflow: ${errorMsg}`);
|
|
1196
|
+
await this.flushOutputAndStep();
|
|
1197
|
+
this.innerController.abort();
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
this.hasError = true;
|
|
1201
|
+
log("error", `stream error: ${errorMsg}`);
|
|
1202
|
+
if (cbs.onStreamError)
|
|
1203
|
+
invokeCallback(cbs.onStreamError, errorMsg);
|
|
1204
|
+
else
|
|
1205
|
+
console.error(`\x1b[31m${formatErrorMessage(errorMsg)}\x1b[0m`);
|
|
1206
|
+
return false;
|
|
549
1207
|
}
|
|
550
|
-
callbacks?.onRunFinish?.({ stepCount, usage, hasError: false, aborted: true });
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (!callbacks)
|
|
554
|
-
printDivider();
|
|
555
|
-
const msg = err.message ?? String(err);
|
|
556
|
-
if (callbacks?.onStreamError) {
|
|
557
|
-
callbacks.onStreamError(msg);
|
|
558
1208
|
}
|
|
559
|
-
|
|
560
|
-
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
snapshot(usage, extra = {}) {
|
|
1212
|
+
return {
|
|
1213
|
+
hasError: this.hasError,
|
|
1214
|
+
userAborted: Boolean(this.params.abortSignal?.aborted),
|
|
1215
|
+
budgetExceeded: this.stop.budget,
|
|
1216
|
+
doomLoop: this.stop.doom,
|
|
1217
|
+
researchSteer: this.stop.steer,
|
|
1218
|
+
researchCap: this.stop.cap,
|
|
1219
|
+
contextPressure: this.stop.context,
|
|
1220
|
+
toolPressure: this.stop.tools,
|
|
1221
|
+
maxStepsReached: this.params.stepBounded && this.assembler.innerSteps >= this.params.maxSteps && this.assembler.lastStepHadTools,
|
|
1222
|
+
wrapUp: this.stop.wrapUp,
|
|
1223
|
+
lastStepHadTools: this.assembler.lastStepHadTools,
|
|
1224
|
+
xmlToolFollowUp: this.xmlSearchRecovered && !this.answerAfterXml && !this.stop.doom,
|
|
1225
|
+
emptyCompletion: !this.assembler.hadAssistantText && !this.assembler.hadTools && !this.xmlSearchRecovered,
|
|
1226
|
+
providerStall: this.stop.stall,
|
|
1227
|
+
stepCount: this.assembler.innerSteps,
|
|
1228
|
+
usage,
|
|
1229
|
+
...extra,
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
/** Normal end-of-stream: flush everything and snapshot usage. */
|
|
1233
|
+
async finalize(result) {
|
|
1234
|
+
await this.flushOutputAndStep();
|
|
1235
|
+
this.settlePendingLoop();
|
|
1236
|
+
return this.snapshot(this.resolvedUsage(result ? await safeUsage(result) : undefined));
|
|
1237
|
+
}
|
|
1238
|
+
/** Exception path: flush, classify the error, snapshot. */
|
|
1239
|
+
async handleException(err, result) {
|
|
1240
|
+
await this.applySlices(this.renderer.flushOnError());
|
|
1241
|
+
this.assembler.flushStep();
|
|
1242
|
+
this.settlePendingLoop();
|
|
1243
|
+
const usage = this.resolvedUsage(result ? await safeUsage(result) : undefined);
|
|
1244
|
+
if (this.params.abortSignal?.aborted)
|
|
1245
|
+
return this.snapshot(usage, { userAborted: true });
|
|
1246
|
+
if (isProviderStallError(err)) {
|
|
1247
|
+
// The provider went quiet and we cut the request. Nothing was produced →
|
|
1248
|
+
// let the empty-reply retry path re-send it; otherwise keep the partial
|
|
1249
|
+
// turn and continue like any other tool-only step.
|
|
1250
|
+
const produced = this.assembler.hadAssistantText || this.assembler.hadTools;
|
|
1251
|
+
log("warn", `provider stall: ${describeError(err)}`);
|
|
1252
|
+
this.stop.stall = true;
|
|
1253
|
+
return this.snapshot(usage, { hasError: false, userAborted: false, emptyCompletion: !produced });
|
|
561
1254
|
}
|
|
562
|
-
|
|
563
|
-
|
|
1255
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1256
|
+
return this.snapshot(usage, { userAborted: false });
|
|
564
1257
|
}
|
|
565
|
-
|
|
566
|
-
|
|
1258
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1259
|
+
log("error", msg);
|
|
1260
|
+
if (isContextOverflowError(msg)) {
|
|
1261
|
+
this.stop.context = true;
|
|
1262
|
+
return this.snapshot(usage, { hasError: false, contextPressure: true, userAborted: false });
|
|
567
1263
|
}
|
|
568
|
-
|
|
1264
|
+
this.hasError = true;
|
|
1265
|
+
const display = formatErrorMessage(msg);
|
|
1266
|
+
if (this.params.cbs.onStreamError)
|
|
1267
|
+
invokeCallback(this.params.cbs.onStreamError, msg);
|
|
1268
|
+
else if (display !== msg)
|
|
1269
|
+
console.error(`\x1b[31m${display}\x1b[0m`);
|
|
1270
|
+
else
|
|
569
1271
|
console.error(`\x1b[31mError: ${msg}\x1b[0m`);
|
|
1272
|
+
return this.snapshot(usage, { hasError: true, userAborted: false, contextPressure: false });
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
async function runInnerStream(params) {
|
|
1276
|
+
const machine = new InnerStreamMachine(params);
|
|
1277
|
+
let result;
|
|
1278
|
+
try {
|
|
1279
|
+
machine.attachAbort();
|
|
1280
|
+
const prompts = buildIterationPrompts(params.allTools, params.messages, params.taskState, params.loopGuard);
|
|
1281
|
+
const system = [params.systemPrompt, ...prompts.stable].join("\n\n");
|
|
1282
|
+
applyToolPrune(params.messages, params.pruneBudget());
|
|
1283
|
+
// Volatile state rides along as a trailing message so the cached prefix
|
|
1284
|
+
// (system + history) stays stable across passes.
|
|
1285
|
+
const stateMessage = sessionStateMessage(prompts.volatile);
|
|
1286
|
+
const requestMessages = stateMessage ? [...params.messages, stateMessage] : params.messages;
|
|
1287
|
+
params.overhead.tokens =
|
|
1288
|
+
estimateOverheadTokens(system, params.allTools) +
|
|
1289
|
+
(stateMessage ? estimateOverheadTokens(String(stateMessage.content)) : 0);
|
|
1290
|
+
result = streamText({
|
|
1291
|
+
model: params.model,
|
|
1292
|
+
system,
|
|
1293
|
+
messages: requestMessages,
|
|
1294
|
+
tools: params.allTools,
|
|
1295
|
+
stopWhen: stepCountIs(params.maxSteps),
|
|
1296
|
+
maxRetries: 3,
|
|
1297
|
+
abortSignal: machine.signal,
|
|
1298
|
+
...(params.temperature != null ? { temperature: params.temperature } : {}),
|
|
1299
|
+
...(params.maxTokens != null ? { maxOutputTokens: params.maxTokens } : {}),
|
|
1300
|
+
...(params.topP != null ? { topP: params.topP } : {}),
|
|
1301
|
+
onStepFinish({ usage }) {
|
|
1302
|
+
machine.onStepFinish(usage);
|
|
1303
|
+
},
|
|
1304
|
+
onError({ error }) {
|
|
1305
|
+
machine.onStepError(error);
|
|
1306
|
+
},
|
|
1307
|
+
});
|
|
1308
|
+
for await (const event of result.fullStream) {
|
|
1309
|
+
if (await machine.handleEvent(event))
|
|
1310
|
+
break;
|
|
570
1311
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
callbacks?.onRunFinish?.({ stepCount, usage, hasError: true, aborted: false });
|
|
1312
|
+
return await machine.finalize(result);
|
|
1313
|
+
}
|
|
1314
|
+
catch (err) {
|
|
1315
|
+
return machine.handleException(err, result);
|
|
1316
|
+
}
|
|
1317
|
+
finally {
|
|
1318
|
+
machine.detachAbort();
|
|
579
1319
|
}
|
|
580
1320
|
}
|
|
1321
|
+
export async function runOnce(messages, instructions, modelId, abortSignal, callbacks, tracker, options) {
|
|
1322
|
+
const project = scanProject();
|
|
1323
|
+
const systemPrompt = buildCodeSystemPrompt(project, instructions);
|
|
1324
|
+
return runOnceCore(messages, systemPrompt, modelId, abortSignal, callbacks, tracker, options);
|
|
1325
|
+
}
|
|
1326
|
+
export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSignal, callbacks, tracker, options) {
|
|
1327
|
+
return runOnceCore(messages, systemPrompt, modelId, abortSignal, callbacks, tracker, options);
|
|
1328
|
+
}
|