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.
- package/README.md +18 -18
- package/dist/cli.js +538 -12
- package/docs/API.md +6 -6
- package/package.json +6 -6
- package/skills/self-config/SKILL.md +4 -2
- package/skills/self-config/reference.md +4 -1
- package/dist/agent.js +0 -1388
- package/dist/assistant-stream.js +0 -131
- package/dist/cli/commands/chat.js +0 -13
- package/dist/cli/commands/ctx.js +0 -7
- package/dist/cli/commands/exec.js +0 -35
- package/dist/cli/commands/history.js +0 -58
- package/dist/cli/commands/index.js +0 -249
- package/dist/cli/commands/init.js +0 -18
- package/dist/cli/commands/mcp.js +0 -173
- package/dist/cli/commands/memory.js +0 -87
- package/dist/cli/commands/models.js +0 -21
- package/dist/cli/commands/permission.js +0 -12
- package/dist/cli/commands/rules.js +0 -33
- package/dist/cli/commands/sandbox.js +0 -13
- package/dist/cli/commands/serve.js +0 -9
- package/dist/cli/commands/setup.js +0 -56
- package/dist/cli/commands/shared.js +0 -25
- package/dist/cli/commands/skills.js +0 -119
- package/dist/cli/commands/think.js +0 -12
- package/dist/cli/commands/update.js +0 -7
- package/dist/cli/commands/write-config.js +0 -52
- package/dist/cli/errors.js +0 -36
- package/dist/cli/exec-prompt.js +0 -26
- package/dist/cli/option-helpers.js +0 -65
- package/dist/cli/program.js +0 -223
- package/dist/cli/setup/detect.js +0 -17
- package/dist/cli/setup/flags.js +0 -12
- package/dist/cli/setup/flow.js +0 -108
- package/dist/cli/setup/provider-form.js +0 -102
- package/dist/cli/setup/ui.js +0 -534
- package/dist/clipboard.js +0 -142
- package/dist/code-mode.js +0 -184
- package/dist/compaction.js +0 -644
- package/dist/config.js +0 -363
- package/dist/confirm.js +0 -172
- package/dist/context-window.js +0 -256
- package/dist/ctx-cli.js +0 -30
- package/dist/ctx.js +0 -80
- package/dist/doom-loop.js +0 -278
- package/dist/fetch-timeout.js +0 -152
- package/dist/http-approvals.js +0 -60
- package/dist/http.js +0 -119
- package/dist/instructions.js +0 -196
- package/dist/logger.js +0 -95
- package/dist/markdown.js +0 -259
- package/dist/mcp.js +0 -1036
- package/dist/memory-cli.js +0 -33
- package/dist/memory.js +0 -295
- package/dist/model-catalog.js +0 -285
- package/dist/ollama-model.js +0 -234
- package/dist/ollama-openai-bridge.js +0 -383
- package/dist/output.js +0 -76
- package/dist/paste-handler.js +0 -41
- package/dist/permission-cli.js +0 -40
- package/dist/plugins.js +0 -131
- package/dist/pricing.js +0 -119
- package/dist/provider.js +0 -63
- package/dist/question-format.js +0 -60
- package/dist/reasoning-stream.js +0 -158
- package/dist/sandbox-cli.js +0 -79
- package/dist/sandbox.js +0 -403
- package/dist/save-throttle.js +0 -45
- package/dist/scope.js +0 -23
- package/dist/serve/common.js +0 -425
- package/dist/serve/routes-chat.js +0 -367
- package/dist/serve/routes-mcp.js +0 -212
- package/dist/serve/routes-memory.js +0 -95
- package/dist/serve/routes-meta.js +0 -268
- package/dist/serve/routes-sessions.js +0 -61
- package/dist/serve/routes-skills.js +0 -70
- package/dist/serve.js +0 -138
- package/dist/sessions.js +0 -265
- package/dist/skills.js +0 -599
- package/dist/structured-output.js +0 -29
- package/dist/synthetic.js +0 -7
- package/dist/think-cli.js +0 -36
- package/dist/thinking-wire.js +0 -239
- package/dist/thinking.js +0 -166
- package/dist/title-gen.js +0 -55
- package/dist/token-display.js +0 -39
- package/dist/tool-display.js +0 -178
- package/dist/tool-output.js +0 -126
- package/dist/tools/apply_patch.js +0 -265
- package/dist/tools/atomic-file.js +0 -35
- package/dist/tools/backend.js +0 -61
- package/dist/tools/bash.js +0 -231
- package/dist/tools/code_search.js +0 -98
- package/dist/tools/edit.js +0 -68
- package/dist/tools/explore.js +0 -232
- package/dist/tools/glob.js +0 -40
- package/dist/tools/grep.js +0 -174
- package/dist/tools/index.js +0 -34
- package/dist/tools/question.js +0 -54
- package/dist/tools/read.js +0 -109
- package/dist/tools/search-searxng.js +0 -223
- package/dist/tools/search-serper.js +0 -189
- package/dist/tools/task.js +0 -165
- package/dist/tools/todo.js +0 -213
- package/dist/tools/web_fetch.js +0 -195
- package/dist/tools/web_search.js +0 -228
- package/dist/tools/write.js +0 -55
- package/dist/tui/App.js +0 -138
- package/dist/tui/ConfirmBar.js +0 -66
- package/dist/tui/CtxPicker.js +0 -68
- package/dist/tui/InputBar.js +0 -618
- package/dist/tui/MessageList.js +0 -718
- package/dist/tui/ModelPicker.js +0 -113
- package/dist/tui/QuestionBar.js +0 -136
- package/dist/tui/SessionPicker.js +0 -79
- package/dist/tui/Spinner.js +0 -6
- package/dist/tui/StatusBar.js +0 -22
- package/dist/tui/ThinkPicker.js +0 -75
- package/dist/tui/agent-runner.js +0 -223
- package/dist/tui/bracketed-paste.js +0 -37
- package/dist/tui/caret-pos.js +0 -179
- package/dist/tui/caret.js +0 -69
- package/dist/tui/click-count.js +0 -13
- package/dist/tui/diff-view.js +0 -61
- package/dist/tui/drag-state.js +0 -49
- package/dist/tui/hydrate.js +0 -129
- package/dist/tui/index.js +0 -264
- package/dist/tui/input-history.js +0 -125
- package/dist/tui/layout.js +0 -105
- package/dist/tui/mouse.js +0 -46
- package/dist/tui/overlay-input.js +0 -12
- package/dist/tui/paste-draft.js +0 -173
- package/dist/tui/prompt-queue.js +0 -24
- package/dist/tui/selection.js +0 -232
- package/dist/tui/session-switch.js +0 -28
- package/dist/tui/slash-commands.js +0 -129
- package/dist/tui/slash-handler.js +0 -615
- package/dist/tui/text-width.js +0 -113
- package/dist/tui/theme.js +0 -12
- package/dist/tui/token-info.js +0 -7
- package/dist/tui/tool-children.js +0 -19
- package/dist/tui/types.js +0 -1
- package/dist/tui/undo-stack.js +0 -14
- package/dist/tui/use-sgr-mouse.js +0 -29
- package/dist/tui-chat.js +0 -478
- package/dist/updater.js +0 -116
- package/dist/xml-search.js +0 -194
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
- package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
- package/docs/superpowers/plans/2026-08-23-cli-setup.md +0 -501
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
- package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +0 -282
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
- 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
|
-
}
|
package/dist/tools/task.js
DELETED
|
@@ -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
|
-
}
|
package/dist/tools/todo.js
DELETED
|
@@ -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
|
-
}
|