min-agent 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/tools/task.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { tool, jsonSchema, streamText, stepCountIs } from "ai";
|
|
2
2
|
import { resolveModel } from "../provider.js";
|
|
3
3
|
import { createTools } from "./index.js";
|
|
4
|
-
import { getMcpTools } from "../mcp.js";
|
|
5
|
-
import {
|
|
4
|
+
import { getMcpTools, getMcpCatalogTools } from "../mcp.js";
|
|
5
|
+
import { attachSkills } from "../skills.js";
|
|
6
6
|
import { loadPluginTools } from "../plugins.js";
|
|
7
7
|
import { DoomLoopDetector } from "../doom-loop.js";
|
|
8
|
+
import { log } from "../logger.js";
|
|
9
|
+
import { getEffectiveConfig } from "../config.js";
|
|
8
10
|
import { stripThinkingFromAssistantText } from "../assistant-stream.js";
|
|
11
|
+
import { unwrapXmlSearchTags } from "../xml-search.js";
|
|
9
12
|
import { truncateToolOutput } from "../tool-output.js";
|
|
13
|
+
import { createTodoTool } from "./todo.js";
|
|
10
14
|
const SUB_AGENT_MAX_STEPS = 15;
|
|
11
|
-
|
|
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.
|
|
12
22
|
|
|
13
23
|
Rules:
|
|
14
24
|
- Focus only on the assigned task
|
|
@@ -19,7 +29,16 @@ Rules:
|
|
|
19
29
|
Working directory: ${process.cwd()}
|
|
20
30
|
Platform: ${process.platform}
|
|
21
31
|
Date: ${new Date().toDateString()}`;
|
|
22
|
-
|
|
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 = {}) {
|
|
23
42
|
return tool({
|
|
24
43
|
description: `Launch a sub-agent to execute a task independently. The sub-agent has its own context and tools. Use this for:
|
|
25
44
|
- Parallel execution: call multiple tasks at once for independent work
|
|
@@ -36,44 +55,56 @@ Call multiple tasks in parallel when the work is independent.`,
|
|
|
36
55
|
},
|
|
37
56
|
required: ["description", "prompt"],
|
|
38
57
|
}),
|
|
39
|
-
execute: async ({ description, prompt }) => {
|
|
40
|
-
|
|
58
|
+
execute: async ({ description, prompt }, options) => {
|
|
59
|
+
const parentToolCallId = options?.toolCallId;
|
|
60
|
+
logSub(`\x1b[90m ┌─ Sub-agent: ${description}\x1b[0m`);
|
|
41
61
|
try {
|
|
42
|
-
const result = await runSubAgent(prompt, modelId);
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
45
67
|
}
|
|
46
68
|
catch (err) {
|
|
47
|
-
|
|
48
|
-
|
|
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}`;
|
|
49
75
|
}
|
|
50
76
|
},
|
|
51
77
|
});
|
|
52
78
|
}
|
|
53
|
-
async function runSubAgent(prompt, modelId) {
|
|
79
|
+
async function runSubAgent(prompt, modelId, abortSignal, parentToolCallId, context = {}) {
|
|
54
80
|
const model = resolveModel(modelId);
|
|
55
|
-
// Build tools for sub-agent (no task tool to prevent recursion)
|
|
56
81
|
const builtinTools = createTools();
|
|
82
|
+
builtinTools.todo = createTodoTool({ silent: true });
|
|
57
83
|
const mcpTools = getMcpTools();
|
|
84
|
+
const catalogTools = getMcpCatalogTools();
|
|
58
85
|
const pluginTools = await loadPluginTools();
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
allTools["skill"] = getSkillsTool();
|
|
65
|
-
// Remove task tool from sub-agent to prevent infinite recursion
|
|
66
|
-
delete allTools["task"];
|
|
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;
|
|
67
91
|
const messages = [{ role: "user", content: prompt }];
|
|
68
|
-
|
|
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;
|
|
69
97
|
const result = streamText({
|
|
70
98
|
model,
|
|
71
|
-
system:
|
|
99
|
+
system: skillsPrompt ? `${subAgentSystem()}\n\n${skillsPrompt}` : subAgentSystem(),
|
|
72
100
|
messages,
|
|
73
101
|
tools: allTools,
|
|
74
|
-
stopWhen: stepCountIs(
|
|
102
|
+
stopWhen: stepCountIs(maxSteps),
|
|
75
103
|
maxRetries: 2,
|
|
76
|
-
|
|
104
|
+
abortSignal: signal,
|
|
105
|
+
onError({ error }) {
|
|
106
|
+
log("warn", `sub-agent step failed (will retry): ${String(error)}`);
|
|
107
|
+
},
|
|
77
108
|
});
|
|
78
109
|
let assistantText = "";
|
|
79
110
|
for await (const event of result.fullStream) {
|
|
@@ -81,18 +112,54 @@ async function runSubAgent(prompt, modelId) {
|
|
|
81
112
|
case "text-delta":
|
|
82
113
|
assistantText += event.text;
|
|
83
114
|
break;
|
|
84
|
-
case "tool-call":
|
|
85
|
-
|
|
86
|
-
|
|
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 };
|
|
87
123
|
}
|
|
88
|
-
|
|
124
|
+
listener?.({ parentToolCallId, name: event.toolName, input: event.input, phase: "call" });
|
|
125
|
+
logSub(`\x1b[90m │ ⚡ ${event.toolName}\x1b[0m`);
|
|
89
126
|
break;
|
|
90
|
-
|
|
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
|
+
}
|
|
91
150
|
break;
|
|
151
|
+
}
|
|
92
152
|
case "error":
|
|
93
|
-
|
|
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}]` };
|
|
94
156
|
}
|
|
95
157
|
}
|
|
96
|
-
const cleaned = stripThinkingFromAssistantText(assistantText);
|
|
97
|
-
|
|
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 };
|
|
98
165
|
}
|
package/dist/tools/todo.js
CHANGED
|
@@ -1,44 +1,142 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 };
|
|
6
7
|
}
|
|
7
|
-
export function
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export function resetTaskState(state) {
|
|
9
|
+
state.goal = "";
|
|
10
|
+
state.todos.length = 0;
|
|
11
|
+
state.nextId = 1;
|
|
10
12
|
}
|
|
11
|
-
function
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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,
|
|
19
52
|
};
|
|
20
|
-
|
|
21
|
-
|
|
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)
|
|
22
62
|
.join("\n");
|
|
23
63
|
}
|
|
24
|
-
|
|
25
|
-
|
|
64
|
+
/** Keep the first real user request so compaction cannot drop the original goal. */
|
|
65
|
+
export function captureGoal(state, messages) {
|
|
66
|
+
if (state.goal.trim())
|
|
67
|
+
return;
|
|
68
|
+
for (const msg of messages) {
|
|
69
|
+
if (msg.role !== "user")
|
|
70
|
+
continue;
|
|
71
|
+
if (isSyntheticMessage(msg))
|
|
72
|
+
continue;
|
|
73
|
+
const text = messageText(msg).trim();
|
|
74
|
+
if (!text)
|
|
75
|
+
continue;
|
|
76
|
+
state.goal = text.slice(0, GOAL_MAX_CHARS);
|
|
26
77
|
return;
|
|
27
|
-
console.log(`\x1b[90m ┌─ Tasks${"─".repeat(36)}\x1b[0m`);
|
|
28
|
-
for (const t of todos) {
|
|
29
|
-
const icon = t.status === "done" ? "\x1b[32m●\x1b[0m"
|
|
30
|
-
: t.status === "in_progress" ? "\x1b[33m◐\x1b[0m"
|
|
31
|
-
: t.status === "cancelled" ? "\x1b[90m✕\x1b[0m"
|
|
32
|
-
: "\x1b[90m○\x1b[0m";
|
|
33
|
-
const dim = t.status === "done" || t.status === "cancelled" ? "\x1b[90m" : "";
|
|
34
|
-
const reset = dim ? "\x1b[0m" : "";
|
|
35
|
-
console.log(`\x1b[90m │\x1b[0m ${icon} ${dim}#${t.id} ${t.text}${reset}`);
|
|
36
78
|
}
|
|
37
|
-
console.log(`\x1b[90m └${"─".repeat(44)}\x1b[0m`);
|
|
38
79
|
}
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
80
|
+
export function formatTaskStatePrompt(state) {
|
|
81
|
+
if (!state.goal && state.todos.length === 0)
|
|
82
|
+
return "";
|
|
83
|
+
const icons = {
|
|
84
|
+
pending: "pending",
|
|
85
|
+
in_progress: "in_progress",
|
|
86
|
+
done: "done",
|
|
87
|
+
cancelled: "cancelled",
|
|
88
|
+
};
|
|
89
|
+
const lines = [
|
|
90
|
+
"## Current Task",
|
|
91
|
+
"This block is the durable task state. It survives context compaction. Keep it accurate with the todo tool.",
|
|
92
|
+
];
|
|
93
|
+
if (state.goal) {
|
|
94
|
+
lines.push("", "### Original goal", state.goal);
|
|
95
|
+
}
|
|
96
|
+
if (state.todos.length > 0) {
|
|
97
|
+
lines.push("", "### Task list");
|
|
98
|
+
for (const t of state.todos) {
|
|
99
|
+
lines.push(`- [${icons[t.status]}] #${t.id} ${t.text}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
export function createTodoTool(opts) {
|
|
105
|
+
const silent = opts?.silent ?? false;
|
|
106
|
+
const store = opts?.store ?? emptyTaskState();
|
|
107
|
+
const formatTodos = () => {
|
|
108
|
+
if (store.todos.length === 0)
|
|
109
|
+
return "No tasks.";
|
|
110
|
+
const icons = {
|
|
111
|
+
pending: "○",
|
|
112
|
+
in_progress: "◐",
|
|
113
|
+
done: "●",
|
|
114
|
+
cancelled: "✕",
|
|
115
|
+
};
|
|
116
|
+
return store.todos.map((t) => ` ${icons[t.status]} #${t.id} ${t.text}`).join("\n");
|
|
117
|
+
};
|
|
118
|
+
const printTodos = () => {
|
|
119
|
+
if (silent || store.todos.length === 0)
|
|
120
|
+
return;
|
|
121
|
+
console.log(`\x1b[90m ┌─ Tasks${"─".repeat(36)}\x1b[0m`);
|
|
122
|
+
for (const t of store.todos) {
|
|
123
|
+
const icon = t.status === "done"
|
|
124
|
+
? "\x1b[32m●\x1b[0m"
|
|
125
|
+
: t.status === "in_progress"
|
|
126
|
+
? "\x1b[33m◐\x1b[0m"
|
|
127
|
+
: t.status === "cancelled"
|
|
128
|
+
? "\x1b[90m✕\x1b[0m"
|
|
129
|
+
: "\x1b[90m○\x1b[0m";
|
|
130
|
+
const dim = t.status === "done" || t.status === "cancelled" ? "\x1b[90m" : "";
|
|
131
|
+
const reset = dim ? "\x1b[0m" : "";
|
|
132
|
+
console.log(`\x1b[90m │\x1b[0m ${icon} ${dim}#${t.id} ${t.text}${reset}`);
|
|
133
|
+
}
|
|
134
|
+
console.log(`\x1b[90m └${"─".repeat(44)}\x1b[0m`);
|
|
135
|
+
};
|
|
136
|
+
return tool({
|
|
137
|
+
description: `Create or update tasks to track progress. The list persists across context compaction and auto-continue. Use this FREQUENTLY to:
|
|
138
|
+
- Plan multi-step work by creating tasks upfront (research, then the deliverable)
|
|
139
|
+
- Mark research tasks done before more searching — then write the output
|
|
42
140
|
- Mark tasks as in_progress when starting them
|
|
43
141
|
- Mark tasks as done when completed
|
|
44
142
|
- Give the user visibility into your progress
|
|
@@ -46,43 +144,56 @@ export const todoTool = tool({
|
|
|
46
144
|
To create new tasks: provide items with "text" and optionally "status" (defaults to "pending").
|
|
47
145
|
To update existing tasks: provide items with "id" and "status".
|
|
48
146
|
You can mix creates and updates in one call.`,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
147
|
+
inputSchema: jsonSchema({
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
todos: {
|
|
151
|
+
type: "array",
|
|
152
|
+
description: "List of tasks to create or update",
|
|
153
|
+
items: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
text: { type: "string", description: "Task description (for new tasks)" },
|
|
157
|
+
status: { type: "string", description: "Status: pending, in_progress, done, cancelled" },
|
|
158
|
+
id: { type: "number", description: "Task ID (for updating existing tasks)" },
|
|
159
|
+
},
|
|
61
160
|
},
|
|
62
161
|
},
|
|
63
162
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
163
|
+
required: ["todos"],
|
|
164
|
+
}),
|
|
165
|
+
execute: async ({ todos: items }) => {
|
|
166
|
+
const errors = [];
|
|
167
|
+
for (const item of items) {
|
|
168
|
+
if (item.id != null) {
|
|
169
|
+
const existing = store.todos.find((t) => t.id === item.id);
|
|
170
|
+
if (!existing) {
|
|
171
|
+
errors.push(`task #${item.id} not found`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (item.status) {
|
|
175
|
+
if (!TODO_STATUSES.includes(item.status)) {
|
|
176
|
+
errors.push(`invalid status "${item.status}" for task #${item.id}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
existing.status = item.status;
|
|
180
|
+
}
|
|
181
|
+
if (item.text)
|
|
182
|
+
existing.text = item.text;
|
|
183
|
+
}
|
|
184
|
+
else if (item.text) {
|
|
185
|
+
store.todos.push({
|
|
186
|
+
id: store.nextId++,
|
|
187
|
+
text: item.text,
|
|
188
|
+
status: TODO_STATUSES.includes(item.status) ? item.status : "pending",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
errors.push("item needs either an id (update) or text (create)");
|
|
74
193
|
}
|
|
75
194
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
status: item.status ?? "pending",
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
printTodos();
|
|
86
|
-
return formatTodos();
|
|
87
|
-
},
|
|
88
|
-
});
|
|
195
|
+
printTodos();
|
|
196
|
+
return errors.length > 0 ? `${formatTodos()}\n\nWarnings: ${errors.join("; ")}` : formatTodos();
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|