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
@@ -1,189 +0,0 @@
1
- import { loadConfig } from "../config.js";
2
- import { truncateToolOutput } from "../tool-output.js";
3
- import { LANGUAGE_RE, MAX_BODY_BYTES, VALID_TIME_RANGES, clamp, collapse, dedupeByUrl, emptyResponse, fetchJsonOutcome, formatResults, hostOf, isString, resolveMaxResults, } from "./web_search.js";
4
- const SERPER_KEY_ENV = "MIN_AGENT_SERPER_API_KEY";
5
- const SERPER_API = "https://google.serper.dev/search";
6
- const SERPER_TIMEOUT_MS = 35000;
7
- export const DEFAULT_SERPER_API_KEY = "7913c19ae7a80320771b843ddffa152c9162c9f2";
8
- /** Google's tbs time filter for each supported time_range value. */
9
- const TBS_BY_RANGE = {
10
- day: "qdr:d",
11
- week: "qdr:w",
12
- month: "qdr:m",
13
- year: "qdr:y",
14
- };
15
- function serperKey() {
16
- const env = process.env[SERPER_KEY_ENV];
17
- if (env?.trim())
18
- return env.trim();
19
- const configured = loadConfig().serperApiKey;
20
- if (configured?.trim())
21
- return configured.trim();
22
- return DEFAULT_SERPER_API_KEY.trim();
23
- }
24
- function serperHint() {
25
- return `Set your Serper API key via "serperApiKey" in ~/.min-agent/config.json or the ${SERPER_KEY_ENV} env var, or configure a SearXNG instance via "webSearchURL" / MIN_AGENT_SEARXNG_URL.`;
26
- }
27
- function asSerperResult(r) {
28
- if (typeof r !== "object" || r === null)
29
- return null;
30
- const o = r;
31
- if (!isString(o.link) || o.link.trim() === "")
32
- return null;
33
- return {
34
- title: isString(o.title) ? o.title : "",
35
- url: o.link,
36
- content: isString(o.snippet) ? o.snippet : "",
37
- engine: hostOf(o.link),
38
- score: 0,
39
- publishedDate: isString(o.date) ? o.date : "",
40
- imgSrc: "",
41
- };
42
- }
43
- function emptySerper() {
44
- return { ...emptyResponse(), questions: [] };
45
- }
46
- function stringList(v, key) {
47
- return (Array.isArray(v) ? v : [])
48
- .map((item) => {
49
- if (typeof item === "object" && item !== null) {
50
- const rec = item;
51
- if (isString(rec[key]))
52
- return rec[key];
53
- }
54
- return "";
55
- })
56
- .filter((s) => s.trim() !== "");
57
- }
58
- function parseSerper(data) {
59
- if (typeof data !== "object" || data === null)
60
- return emptySerper();
61
- const o = data;
62
- const results = (Array.isArray(o.organic) ? o.organic : [])
63
- .map(asSerperResult)
64
- .filter((r) => r !== null);
65
- const answers = [];
66
- const ab = o.answerBox;
67
- if (typeof ab === "object" && ab !== null) {
68
- const rec = ab;
69
- for (const key of ["answer", "snippet", "title"]) {
70
- if (isString(rec[key]) && rec[key].trim()) {
71
- answers.push(collapse(rec[key]));
72
- break;
73
- }
74
- }
75
- }
76
- const suggestions = stringList(o.relatedSearches, "query");
77
- const questions = stringList(o.peopleAlsoAsk, "question");
78
- const infoboxes = [];
79
- const kg = o.knowledgeGraph;
80
- if (typeof kg === "object" && kg !== null) {
81
- const rec = kg;
82
- const title = isString(rec.title) ? rec.title : "";
83
- const description = isString(rec.description) ? rec.description : "";
84
- if (title || description)
85
- infoboxes.push({ infobox: title, content: description });
86
- }
87
- return { results, answers, suggestions, corrections: [], infoboxes, unresponsive_engines: [], questions };
88
- }
89
- async function serperSearch(payload, apiKey) {
90
- return fetchJsonOutcome(SERPER_API, {
91
- method: "POST",
92
- headers: {
93
- "X-API-KEY": apiKey,
94
- "Content-Type": "application/json",
95
- Accept: "application/json",
96
- },
97
- body: JSON.stringify(payload),
98
- signal: AbortSignal.timeout(SERPER_TIMEOUT_MS),
99
- }, parseSerper);
100
- }
101
- function renderSerperFailure(o) {
102
- const hint = serperHint();
103
- switch (o.kind) {
104
- case "network":
105
- return `Search error: could not reach the Serper search API (${o.message}). This is not a problem with the query.`;
106
- case "invalid-json":
107
- if (o.bodyTooLarge) {
108
- return `Search error: the search API response exceeded ${MAX_BODY_BYTES / 1024 / 1024} MB and could not be parsed. Narrow the query or request fewer results.`;
109
- }
110
- return `Search error: the search API returned a non-JSON response${o.sample ? ` (starts with: ${o.sample})` : ""}.`;
111
- case "http": {
112
- const detail = o.backendMessage ? ` Backend message: ${o.backendMessage}.` : "";
113
- if (o.status === 429) {
114
- return `Search error: the search API is rate-limiting (HTTP 429).${detail} Wait a few seconds before retrying.`;
115
- }
116
- if (o.status === 401 || o.status === 403) {
117
- return `Search error: the search API rejected the API key (HTTP ${o.status}).${detail} ${hint}`;
118
- }
119
- if (o.status === 400 || o.status === 422) {
120
- return `Search error: the search API rejected these parameters (HTTP ${o.status}).${detail} Fix the arguments and retry; retrying with the same values will fail again.`;
121
- }
122
- if (o.status >= 500) {
123
- return `Search error: the search API failed with HTTP ${o.status}.${detail} It may be temporarily overloaded — retry once.`;
124
- }
125
- return `Search error: HTTP ${o.status} from the search API.${detail}`;
126
- }
127
- }
128
- }
129
- function buildSerperPayload(input, notes, maxResults) {
130
- const payload = { q: input.query, num: maxResults };
131
- const lang = (input.language ?? "").trim().toLowerCase();
132
- if (lang) {
133
- if (LANGUAGE_RE.test(lang) && lang !== "all") {
134
- payload.hl = lang.replace("_", "-");
135
- }
136
- else {
137
- notes.push(`ignored invalid language "${input.language}"; use a code like "zh", "en" or "zh-CN"`);
138
- }
139
- }
140
- const range = (input.time_range ?? "").trim().toLowerCase();
141
- if (range) {
142
- if (VALID_TIME_RANGES.has(range))
143
- payload.tbs = TBS_BY_RANGE[range];
144
- else
145
- notes.push(`ignored invalid time_range "${range}"; valid values: day, week, month, year`);
146
- }
147
- return payload;
148
- }
149
- export async function runSerper(input, query, notes) {
150
- const apiKey = serperKey();
151
- if (!apiKey)
152
- return `Search error: no Serper API key configured. ${serperHint()}`;
153
- const maxResults = resolveMaxResults(input.max_results, notes);
154
- const payload = buildSerperPayload(input, notes, maxResults);
155
- const outcome = await serperSearch(payload, apiKey);
156
- if (outcome.kind !== "ok")
157
- return renderSerperFailure(outcome);
158
- const data = outcome.data;
159
- const results = dedupeByUrl(data.results).slice(0, maxResults);
160
- const noteBlock = notes.map((n) => `Note: ${n}`).join("\n");
161
- if (results.length === 0) {
162
- const parts = [`No search results found for "${query}".`];
163
- if (noteBlock)
164
- parts.push(noteBlock);
165
- if (data.questions.length > 0)
166
- parts.push(`Related questions: ${data.questions.slice(0, 5).join(" · ")}`);
167
- if (data.suggestions.length > 0)
168
- parts.push(`Related searches: ${data.suggestions.slice(0, 8).join(" · ")}`);
169
- if (data.questions.length === 0 && data.suggestions.length === 0) {
170
- parts.push("Try a different query or a broader wording.");
171
- }
172
- return parts.join("\n\n");
173
- }
174
- const sections = [];
175
- if (noteBlock)
176
- sections.push(noteBlock);
177
- for (const a of data.answers)
178
- sections.push(`Answer: ${collapse(a)}`);
179
- for (const b of data.infoboxes) {
180
- sections.push(`Infobox${b.infobox ? ` (${collapse(b.infobox)})` : ""}: ${clamp(collapse(b.content), 600)}`);
181
- }
182
- sections.push(`Found ${results.length} result${results.length === 1 ? "" : "s"} for "${query}":`);
183
- sections.push(formatResults(results));
184
- if (data.questions.length > 0)
185
- sections.push(`Related questions: ${data.questions.slice(0, 5).join(" · ")}`);
186
- if (data.suggestions.length > 0)
187
- sections.push(`Related searches: ${data.suggestions.slice(0, 8).join(" · ")}`);
188
- return truncateToolOutput(sections.join("\n\n"), { direction: "head" }).content;
189
- }
@@ -1,165 +0,0 @@
1
- import { tool, jsonSchema, streamText, stepCountIs } from "ai";
2
- import { resolveModel } from "../provider.js";
3
- import { createTools } from "./index.js";
4
- import { getMcpTools, getMcpCatalogTools } from "../mcp.js";
5
- import { attachSkills } from "../skills.js";
6
- import { loadPluginTools } from "../plugins.js";
7
- import { DoomLoopDetector } from "../doom-loop.js";
8
- import { log } from "../logger.js";
9
- import { getEffectiveConfig } from "../config.js";
10
- import { stripThinkingFromAssistantText } from "../assistant-stream.js";
11
- import { unwrapXmlSearchTags } from "../xml-search.js";
12
- import { truncateToolOutput } from "../tool-output.js";
13
- import { createTodoTool } from "./todo.js";
14
- const SUB_AGENT_MAX_STEPS = 15;
15
- function resolveSubAgentSteps(explicit) {
16
- const configured = getEffectiveConfig().agent?.subAgentMaxSteps;
17
- const value = explicit ?? configured ?? SUB_AGENT_MAX_STEPS;
18
- return Number.isFinite(value) && value >= 1 ? Math.floor(value) : SUB_AGENT_MAX_STEPS;
19
- }
20
- function subAgentSystem() {
21
- return `You are a focused sub-agent executing a specific task. Complete the task thoroughly and return a clear result.
22
-
23
- Rules:
24
- - Focus only on the assigned task
25
- - Be thorough but concise
26
- - Use tools as needed to complete the task
27
- - Return a clear summary of what you did and the result
28
-
29
- Working directory: ${process.cwd()}
30
- Platform: ${process.platform}
31
- Date: ${new Date().toDateString()}`;
32
- }
33
- let listener = null;
34
- export function setSubAgentListener(fn) {
35
- listener = fn;
36
- }
37
- function logSub(line) {
38
- if (!listener)
39
- console.error(line);
40
- }
41
- export function createTaskTool(modelId, abortSignal, onUsage, context = {}) {
42
- return tool({
43
- description: `Launch a sub-agent to execute a task independently. The sub-agent has its own context and tools. Use this for:
44
- - Parallel execution: call multiple tasks at once for independent work
45
- - Context isolation: keep the main conversation clean while the sub-agent explores
46
- - Delegation: hand off well-defined subtasks (search, analysis, file operations)
47
-
48
- The sub-agent can read/write files, run commands, search, and use all available tools.
49
- Call multiple tasks in parallel when the work is independent.`,
50
- inputSchema: jsonSchema({
51
- type: "object",
52
- properties: {
53
- description: { type: "string", description: "Short description of the task (shown to user)" },
54
- prompt: { type: "string", description: "Detailed instructions for the sub-agent" },
55
- },
56
- required: ["description", "prompt"],
57
- }),
58
- execute: async ({ description, prompt }, options) => {
59
- const parentToolCallId = options?.toolCallId;
60
- logSub(`\x1b[90m ┌─ Sub-agent: ${description}\x1b[0m`);
61
- try {
62
- const result = await runSubAgent(prompt, modelId, abortSignal, parentToolCallId, context);
63
- if (result.usage)
64
- onUsage?.(result.usage);
65
- logSub(`\x1b[90m └─ ✓ Done\x1b[0m`);
66
- return truncateToolOutput(result.text, { direction: "tail" }).content;
67
- }
68
- catch (err) {
69
- if (abortSignal?.aborted)
70
- return "Sub-agent cancelled by user.";
71
- const message = err instanceof Error ? err.message : String(err);
72
- log("error", `sub-agent failed: ${message}`);
73
- logSub(`\x1b[90m └─ ✗ Failed: ${message}\x1b[0m`);
74
- return `Sub-agent error: ${message}`;
75
- }
76
- },
77
- });
78
- }
79
- async function runSubAgent(prompt, modelId, abortSignal, parentToolCallId, context = {}) {
80
- const model = resolveModel(modelId);
81
- const builtinTools = createTools();
82
- builtinTools.todo = createTodoTool({ silent: true });
83
- const mcpTools = getMcpTools();
84
- const catalogTools = getMcpCatalogTools();
85
- const pluginTools = await loadPluginTools();
86
- const allTools = { ...builtinTools, ...pluginTools, ...mcpTools, ...catalogTools };
87
- const skillsPrompt = attachSkills(allTools);
88
- // Remove task/explore tools from sub-agent to prevent infinite recursion
89
- delete allTools.task;
90
- delete allTools.explore;
91
- const messages = [{ role: "user", content: prompt }];
92
- // Sharing the parent guard means the child cannot restart the research budget.
93
- const doomLoop = context.loopGuard ?? new DoomLoopDetector();
94
- const maxSteps = resolveSubAgentSteps(context.maxSteps);
95
- const controller = new AbortController();
96
- const signal = abortSignal ? AbortSignal.any([abortSignal, controller.signal]) : controller.signal;
97
- const result = streamText({
98
- model,
99
- system: skillsPrompt ? `${subAgentSystem()}\n\n${skillsPrompt}` : subAgentSystem(),
100
- messages,
101
- tools: allTools,
102
- stopWhen: stepCountIs(maxSteps),
103
- maxRetries: 2,
104
- abortSignal: signal,
105
- onError({ error }) {
106
- log("warn", `sub-agent step failed (will retry): ${String(error)}`);
107
- },
108
- });
109
- let assistantText = "";
110
- for await (const event of result.fullStream) {
111
- switch (event.type) {
112
- case "text-delta":
113
- assistantText += event.text;
114
- break;
115
- case "tool-call": {
116
- const action = doomLoop.observe(event.toolName, event.input);
117
- if (action === "halt" || action === "cap") {
118
- controller.abort();
119
- const note = action === "halt"
120
- ? "[Sub-agent stopped: repeated the same action too many times]"
121
- : "[Sub-agent stopped: too many web searches. Summarize what was found.]";
122
- return { text: unwrapXmlSearchTags(assistantText) + "\n\n" + note };
123
- }
124
- listener?.({ parentToolCallId, name: event.toolName, input: event.input, phase: "call" });
125
- logSub(`\x1b[90m │ ⚡ ${event.toolName}\x1b[0m`);
126
- break;
127
- }
128
- case "tool-result": {
129
- listener?.({
130
- parentToolCallId,
131
- name: event.toolName,
132
- output: event.output,
133
- phase: "result",
134
- });
135
- const action = doomLoop.observeResult(event.toolName, event.output);
136
- if (action === "cap") {
137
- controller.abort();
138
- return {
139
- text: unwrapXmlSearchTags(assistantText) +
140
- "\n\n[Sub-agent stopped: research budget spent. Summarize what was found.]",
141
- };
142
- }
143
- if (context.shouldStop?.()) {
144
- log("warn", "sub-agent stopped: parent budget reached");
145
- controller.abort();
146
- return {
147
- text: unwrapXmlSearchTags(assistantText) + "\n\n[Sub-agent stopped: run budget reached]",
148
- };
149
- }
150
- break;
151
- }
152
- case "error":
153
- log("error", `sub-agent stream error: ${String(event.error)}`);
154
- controller.abort();
155
- return { text: unwrapXmlSearchTags(assistantText) + `\n\n[Sub-agent error: ${event.error}]` };
156
- }
157
- }
158
- const cleaned = unwrapXmlSearchTags(stripThinkingFromAssistantText(assistantText));
159
- let usage;
160
- try {
161
- usage = await result.usage;
162
- }
163
- catch { }
164
- return { text: cleaned || "(sub-agent produced no output)", usage };
165
- }
@@ -1,213 +0,0 @@
1
- import { tool, jsonSchema } from "ai";
2
- import { isSyntheticMessage } from "../synthetic.js";
3
- export const TODO_STATUSES = ["pending", "in_progress", "done", "cancelled"];
4
- const GOAL_MAX_CHARS = 4000;
5
- export function emptyTaskState() {
6
- return { goal: "", todos: [], nextId: 1 };
7
- }
8
- export function resetTaskState(state) {
9
- state.goal = "";
10
- state.todos.length = 0;
11
- state.nextId = 1;
12
- }
13
- export function copyTaskState(from, into) {
14
- if (!from) {
15
- resetTaskState(into);
16
- return;
17
- }
18
- into.goal = from.goal;
19
- into.todos = from.todos.map((t) => ({ ...t }));
20
- into.nextId = from.nextId;
21
- }
22
- function isTodoStatus(value) {
23
- return typeof value === "string" && TODO_STATUSES.includes(value);
24
- }
25
- export function parseTaskState(raw) {
26
- if (!raw || typeof raw !== "object")
27
- return undefined;
28
- const rec = raw;
29
- const todos = [];
30
- if (Array.isArray(rec.todos)) {
31
- for (const item of rec.todos) {
32
- if (!item || typeof item !== "object")
33
- continue;
34
- const t = item;
35
- if (typeof t.id !== "number" || typeof t.text !== "string")
36
- continue;
37
- todos.push({
38
- id: t.id,
39
- text: t.text,
40
- status: isTodoStatus(t.status) ? t.status : "pending",
41
- });
42
- }
43
- }
44
- const maxId = todos.reduce((m, t) => Math.max(m, t.id), 0);
45
- const nextId = typeof rec.nextId === "number" && Number.isFinite(rec.nextId) && rec.nextId > maxId
46
- ? Math.floor(rec.nextId)
47
- : maxId + 1;
48
- return {
49
- goal: typeof rec.goal === "string" ? rec.goal : "",
50
- todos,
51
- nextId,
52
- };
53
- }
54
- function messageText(msg) {
55
- if (typeof msg.content === "string")
56
- return msg.content;
57
- if (!Array.isArray(msg.content))
58
- return "";
59
- return msg.content
60
- .filter((p) => p.type === "text" && typeof p.text === "string")
61
- .map((p) => p.text)
62
- .join("\n");
63
- }
64
- const CHITCHAT_RE = /^(你好|您好|嗨|哈喽|哈罗|哈囉|在吗|在嗎|早上好|晚上好|早安|午安|hello|hi|hey|yo|sup|thanks|thankyou|thx|ty|谢谢|谢谢你|多谢|感谢|ok|okay|好的|收到)$/i;
65
- /** Greetings / acknowledgements are not a durable task. "你好,帮我修 bug" is. */
66
- export function isChitchatUserText(text) {
67
- const stripped = text.trim().replace(/[\s\p{P}\p{S}\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, "");
68
- return stripped.length === 0 || CHITCHAT_RE.test(stripped);
69
- }
70
- function durableGoal(text) {
71
- const trimmed = text.trim();
72
- if (!trimmed || isChitchatUserText(trimmed))
73
- return undefined;
74
- return trimmed.slice(0, GOAL_MAX_CHARS);
75
- }
76
- /** Keep the first real user request so compaction cannot drop the original goal. */
77
- export function captureGoal(state, messages) {
78
- if (state.goal.trim() && !isChitchatUserText(state.goal))
79
- return;
80
- for (const msg of messages) {
81
- if (msg.role !== "user")
82
- continue;
83
- if (isSyntheticMessage(msg))
84
- continue;
85
- const goal = durableGoal(messageText(msg));
86
- if (!goal)
87
- continue;
88
- state.goal = goal;
89
- return;
90
- }
91
- }
92
- export function formatTaskStatePrompt(state) {
93
- const goal = state.goal.trim() && !isChitchatUserText(state.goal) ? state.goal : "";
94
- if (!goal && state.todos.length === 0)
95
- return "";
96
- const icons = {
97
- pending: "pending",
98
- in_progress: "in_progress",
99
- done: "done",
100
- cancelled: "cancelled",
101
- };
102
- const lines = [
103
- "## Current Task",
104
- "This block tracks progress for the current user request. It is not a reason to start extra work.",
105
- "If the task list is missing or empty, there is no pending work to resume. Follow the current user message only. Keep the list accurate with the todo tool.",
106
- ];
107
- if (goal) {
108
- lines.push("", "### Original goal", goal);
109
- }
110
- if (state.todos.length > 0) {
111
- lines.push("", "### Task list");
112
- for (const t of state.todos) {
113
- lines.push(`- [${icons[t.status]}] #${t.id} ${t.text}`);
114
- }
115
- }
116
- return lines.join("\n");
117
- }
118
- export function createTodoTool(opts) {
119
- const silent = opts?.silent ?? false;
120
- const store = opts?.store ?? emptyTaskState();
121
- const formatTodos = () => {
122
- if (store.todos.length === 0)
123
- return "No tasks.";
124
- const icons = {
125
- pending: "○",
126
- in_progress: "◐",
127
- done: "●",
128
- cancelled: "✕",
129
- };
130
- return store.todos.map((t) => ` ${icons[t.status]} #${t.id} ${t.text}`).join("\n");
131
- };
132
- const printTodos = () => {
133
- if (silent || store.todos.length === 0)
134
- return;
135
- console.log(`\x1b[90m ┌─ Tasks${"─".repeat(36)}\x1b[0m`);
136
- for (const t of store.todos) {
137
- const icon = t.status === "done"
138
- ? "\x1b[32m●\x1b[0m"
139
- : t.status === "in_progress"
140
- ? "\x1b[33m◐\x1b[0m"
141
- : t.status === "cancelled"
142
- ? "\x1b[90m✕\x1b[0m"
143
- : "\x1b[90m○\x1b[0m";
144
- const dim = t.status === "done" || t.status === "cancelled" ? "\x1b[90m" : "";
145
- const reset = dim ? "\x1b[0m" : "";
146
- console.log(`\x1b[90m │\x1b[0m ${icon} ${dim}#${t.id} ${t.text}${reset}`);
147
- }
148
- console.log(`\x1b[90m └${"─".repeat(44)}\x1b[0m`);
149
- };
150
- return tool({
151
- description: `Create or update tasks to track progress. The list persists across context compaction and auto-continue. Use this FREQUENTLY to:
152
- - Plan multi-step work by creating tasks upfront (research, then the deliverable)
153
- - Mark research tasks done before more searching — then write the output
154
- - Mark tasks as in_progress when starting them
155
- - Mark tasks as done when completed
156
- - Give the user visibility into your progress
157
-
158
- To create new tasks: provide items with "text" and optionally "status" (defaults to "pending").
159
- To update existing tasks: provide items with "id" and "status".
160
- You can mix creates and updates in one call.`,
161
- inputSchema: jsonSchema({
162
- type: "object",
163
- properties: {
164
- todos: {
165
- type: "array",
166
- description: "List of tasks to create or update",
167
- items: {
168
- type: "object",
169
- properties: {
170
- text: { type: "string", description: "Task description (for new tasks)" },
171
- status: { type: "string", description: "Status: pending, in_progress, done, cancelled" },
172
- id: { type: "number", description: "Task ID (for updating existing tasks)" },
173
- },
174
- },
175
- },
176
- },
177
- required: ["todos"],
178
- }),
179
- execute: async ({ todos: items }) => {
180
- const errors = [];
181
- for (const item of items) {
182
- if (item.id != null) {
183
- const existing = store.todos.find((t) => t.id === item.id);
184
- if (!existing) {
185
- errors.push(`task #${item.id} not found`);
186
- continue;
187
- }
188
- if (item.status) {
189
- if (!TODO_STATUSES.includes(item.status)) {
190
- errors.push(`invalid status "${item.status}" for task #${item.id}`);
191
- continue;
192
- }
193
- existing.status = item.status;
194
- }
195
- if (item.text)
196
- existing.text = item.text;
197
- }
198
- else if (item.text) {
199
- store.todos.push({
200
- id: store.nextId++,
201
- text: item.text,
202
- status: TODO_STATUSES.includes(item.status) ? item.status : "pending",
203
- });
204
- }
205
- else {
206
- errors.push("item needs either an id (update) or text (create)");
207
- }
208
- }
209
- printTodos();
210
- return errors.length > 0 ? `${formatTodos()}\n\nWarnings: ${errors.join("; ")}` : formatTodos();
211
- },
212
- });
213
- }