min-agent 0.5.0 → 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 (156) hide show
  1. package/README.md +32 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +25 -8
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +7 -3
  6. package/skills/self-config/reference.md +8 -4
  7. package/dist/agent.js +0 -1342
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/exec.js +0 -35
  11. package/dist/cli/commands/history.js +0 -58
  12. package/dist/cli/commands/index.js +0 -241
  13. package/dist/cli/commands/init.js +0 -18
  14. package/dist/cli/commands/mcp.js +0 -173
  15. package/dist/cli/commands/memory.js +0 -87
  16. package/dist/cli/commands/models.js +0 -21
  17. package/dist/cli/commands/permission.js +0 -12
  18. package/dist/cli/commands/rules.js +0 -33
  19. package/dist/cli/commands/sandbox.js +0 -13
  20. package/dist/cli/commands/serve.js +0 -9
  21. package/dist/cli/commands/setup.js +0 -4
  22. package/dist/cli/commands/shared.js +0 -16
  23. package/dist/cli/commands/skills.js +0 -119
  24. package/dist/cli/commands/think.js +0 -12
  25. package/dist/cli/commands/update.js +0 -7
  26. package/dist/cli/commands/write-config.js +0 -52
  27. package/dist/cli/errors.js +0 -36
  28. package/dist/cli/exec-prompt.js +0 -26
  29. package/dist/cli/option-helpers.js +0 -65
  30. package/dist/cli/program.js +0 -217
  31. package/dist/clipboard.js +0 -142
  32. package/dist/code-mode.js +0 -184
  33. package/dist/compaction.js +0 -644
  34. package/dist/config.js +0 -470
  35. package/dist/confirm.js +0 -172
  36. package/dist/context-window.js +0 -246
  37. package/dist/doom-loop.js +0 -278
  38. package/dist/fetch-timeout.js +0 -152
  39. package/dist/http-approvals.js +0 -60
  40. package/dist/http.js +0 -119
  41. package/dist/instructions.js +0 -196
  42. package/dist/logger.js +0 -95
  43. package/dist/markdown.js +0 -259
  44. package/dist/mcp.js +0 -1036
  45. package/dist/memory-cli.js +0 -33
  46. package/dist/memory.js +0 -295
  47. package/dist/model-catalog.js +0 -285
  48. package/dist/output.js +0 -76
  49. package/dist/paste-handler.js +0 -41
  50. package/dist/permission-cli.js +0 -40
  51. package/dist/plugins.js +0 -131
  52. package/dist/pricing.js +0 -119
  53. package/dist/provider.js +0 -63
  54. package/dist/question-format.js +0 -60
  55. package/dist/reasoning-stream.js +0 -158
  56. package/dist/sandbox-cli.js +0 -79
  57. package/dist/sandbox.js +0 -403
  58. package/dist/save-throttle.js +0 -45
  59. package/dist/scope.js +0 -23
  60. package/dist/serve/common.js +0 -425
  61. package/dist/serve/routes-chat.js +0 -367
  62. package/dist/serve/routes-mcp.js +0 -212
  63. package/dist/serve/routes-memory.js +0 -95
  64. package/dist/serve/routes-meta.js +0 -233
  65. package/dist/serve/routes-sessions.js +0 -61
  66. package/dist/serve/routes-skills.js +0 -70
  67. package/dist/serve.js +0 -138
  68. package/dist/sessions.js +0 -265
  69. package/dist/skills.js +0 -599
  70. package/dist/structured-output.js +0 -29
  71. package/dist/synthetic.js +0 -7
  72. package/dist/think-cli.js +0 -36
  73. package/dist/thinking-wire.js +0 -228
  74. package/dist/thinking.js +0 -142
  75. package/dist/title-gen.js +0 -55
  76. package/dist/token-display.js +0 -39
  77. package/dist/tool-display.js +0 -178
  78. package/dist/tool-output.js +0 -126
  79. package/dist/tools/apply_patch.js +0 -265
  80. package/dist/tools/atomic-file.js +0 -35
  81. package/dist/tools/backend.js +0 -61
  82. package/dist/tools/bash.js +0 -231
  83. package/dist/tools/code_search.js +0 -98
  84. package/dist/tools/edit.js +0 -68
  85. package/dist/tools/explore.js +0 -232
  86. package/dist/tools/glob.js +0 -40
  87. package/dist/tools/grep.js +0 -174
  88. package/dist/tools/index.js +0 -34
  89. package/dist/tools/question.js +0 -54
  90. package/dist/tools/read.js +0 -109
  91. package/dist/tools/search-searxng.js +0 -223
  92. package/dist/tools/search-serper.js +0 -189
  93. package/dist/tools/task.js +0 -165
  94. package/dist/tools/todo.js +0 -213
  95. package/dist/tools/web_fetch.js +0 -195
  96. package/dist/tools/web_search.js +0 -228
  97. package/dist/tools/write.js +0 -55
  98. package/dist/tui/App.js +0 -126
  99. package/dist/tui/ConfirmBar.js +0 -66
  100. package/dist/tui/InputBar.js +0 -616
  101. package/dist/tui/MessageList.js +0 -718
  102. package/dist/tui/ModelPicker.js +0 -113
  103. package/dist/tui/QuestionBar.js +0 -136
  104. package/dist/tui/SessionPicker.js +0 -79
  105. package/dist/tui/Spinner.js +0 -6
  106. package/dist/tui/StatusBar.js +0 -22
  107. package/dist/tui/ThinkPicker.js +0 -77
  108. package/dist/tui/agent-runner.js +0 -223
  109. package/dist/tui/bracketed-paste.js +0 -37
  110. package/dist/tui/caret-pos.js +0 -179
  111. package/dist/tui/caret.js +0 -69
  112. package/dist/tui/click-count.js +0 -13
  113. package/dist/tui/diff-view.js +0 -61
  114. package/dist/tui/drag-state.js +0 -49
  115. package/dist/tui/hydrate.js +0 -129
  116. package/dist/tui/index.js +0 -258
  117. package/dist/tui/input-history.js +0 -125
  118. package/dist/tui/layout.js +0 -105
  119. package/dist/tui/mouse.js +0 -46
  120. package/dist/tui/overlay-input.js +0 -12
  121. package/dist/tui/paste-draft.js +0 -173
  122. package/dist/tui/prompt-queue.js +0 -24
  123. package/dist/tui/selection.js +0 -232
  124. package/dist/tui/session-switch.js +0 -28
  125. package/dist/tui/slash-commands.js +0 -123
  126. package/dist/tui/slash-handler.js +0 -589
  127. package/dist/tui/text-width.js +0 -113
  128. package/dist/tui/theme.js +0 -12
  129. package/dist/tui/token-info.js +0 -7
  130. package/dist/tui/tool-children.js +0 -19
  131. package/dist/tui/types.js +0 -1
  132. package/dist/tui/undo-stack.js +0 -14
  133. package/dist/tui/use-sgr-mouse.js +0 -29
  134. package/dist/tui-chat.js +0 -456
  135. package/dist/updater.js +0 -116
  136. package/dist/xml-search.js +0 -194
  137. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  138. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  139. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  140. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  141. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  142. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  143. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  144. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  145. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  146. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  147. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  148. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  149. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  150. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  151. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  152. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  153. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  154. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  155. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  156. 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
- }