min-agent 0.5.1 → 0.6.0

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