min-agent 0.1.3 → 0.1.5
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 +269 -164
- package/bin/min-agent.js +6 -0
- package/dist/agent.js +383 -16
- package/dist/cli.js +20 -1
- package/dist/clipboard.js +106 -0
- package/dist/code-mode.js +166 -0
- package/dist/compaction.js +243 -48
- package/dist/config.js +34 -7
- package/dist/context-window.js +185 -0
- package/dist/doom-loop.js +36 -0
- package/dist/instructions.js +42 -0
- package/dist/mcp.js +28 -16
- package/dist/output.js +15 -2
- package/dist/serve.js +351 -3
- package/dist/sessions.js +13 -4
- package/dist/structured-output.js +29 -0
- package/dist/title-gen.js +48 -0
- package/dist/tools/bash.js +81 -74
- package/dist/tools/code_search.js +91 -0
- package/dist/tools/explore.js +104 -0
- package/dist/tools/index.js +10 -1
- package/dist/tools/question.js +53 -0
- package/dist/tools/read.js +14 -3
- package/dist/tools/task.js +98 -0
- package/dist/tools/todo.js +88 -0
- package/docs/API.md +298 -111
- package/package.json +3 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { tool, jsonSchema } from "ai";
|
|
2
|
+
import { truncateToolOutput } from "../tool-output.js";
|
|
3
|
+
const EXA_MCP_URL = process.env.EXA_API_KEY
|
|
4
|
+
? `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(process.env.EXA_API_KEY)}`
|
|
5
|
+
: "https://mcp.exa.ai/mcp";
|
|
6
|
+
export const codeSearchTool = tool({
|
|
7
|
+
description: `Search and get relevant context for any programming task using Exa Code API.
|
|
8
|
+
Provides high-quality, fresh context for libraries, SDKs, and APIs.
|
|
9
|
+
Use this for ANY question or task related to programming — finding code examples, documentation, API references, and patterns.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
- Adjustable token count (1000-50000) for focused or comprehensive results
|
|
13
|
+
- Default 5000 tokens for balanced context
|
|
14
|
+
- Examples: 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Rust async trait implementation'`,
|
|
15
|
+
inputSchema: jsonSchema({
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
query: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Search query for APIs, libraries, SDKs. E.g. 'React useState hook examples', 'Express.js middleware'",
|
|
21
|
+
},
|
|
22
|
+
tokensNum: {
|
|
23
|
+
type: "number",
|
|
24
|
+
description: "Number of tokens to return (1000-50000). Default 5000. Use lower for focused queries, higher for comprehensive docs.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ["query"],
|
|
28
|
+
}),
|
|
29
|
+
execute: async ({ query, tokensNum }) => {
|
|
30
|
+
const tokens = Math.max(1000, Math.min(50000, tokensNum ?? 5000));
|
|
31
|
+
try {
|
|
32
|
+
const result = await callExaCode(query, tokens);
|
|
33
|
+
if (!result) {
|
|
34
|
+
return "No code snippets or documentation found. Try a different query, be more specific about the library or concept, or check spelling.";
|
|
35
|
+
}
|
|
36
|
+
return truncateToolOutput(result, { direction: "head" }).content;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return `Code search error: ${err.message}`;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Call Exa's MCP endpoint for code context.
|
|
45
|
+
* Uses JSON-RPC over HTTP with SSE response format.
|
|
46
|
+
*/
|
|
47
|
+
async function callExaCode(query, tokensNum) {
|
|
48
|
+
const body = JSON.stringify({
|
|
49
|
+
jsonrpc: "2.0",
|
|
50
|
+
id: 1,
|
|
51
|
+
method: "tools/call",
|
|
52
|
+
params: {
|
|
53
|
+
name: "get_code_context_exa",
|
|
54
|
+
arguments: { query, tokensNum },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const response = await fetch(EXA_MCP_URL, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
Accept: "application/json, text/event-stream",
|
|
62
|
+
},
|
|
63
|
+
body,
|
|
64
|
+
signal: AbortSignal.timeout(30000),
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Exa API returned ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
const text = await response.text();
|
|
70
|
+
// Parse SSE response: look for "data: {...}" lines
|
|
71
|
+
for (const line of text.split("\n")) {
|
|
72
|
+
if (!line.startsWith("data: "))
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
const data = JSON.parse(line.slice(6));
|
|
76
|
+
const content = data?.result?.content?.[0]?.text;
|
|
77
|
+
if (content)
|
|
78
|
+
return content;
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
}
|
|
82
|
+
// Try parsing as direct JSON response
|
|
83
|
+
try {
|
|
84
|
+
const data = JSON.parse(text);
|
|
85
|
+
const content = data?.result?.content?.[0]?.text;
|
|
86
|
+
if (content)
|
|
87
|
+
return content;
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { tool, jsonSchema, streamText, stepCountIs } from "ai";
|
|
2
|
+
import { resolveModel } from "../provider.js";
|
|
3
|
+
import { bashTool } from "./bash.js";
|
|
4
|
+
import { readTool } from "./read.js";
|
|
5
|
+
import { globTool } from "./glob.js";
|
|
6
|
+
import { grepTool } from "./grep.js";
|
|
7
|
+
import { stripThinkingFromAssistantText } from "../assistant-stream.js";
|
|
8
|
+
import { truncateToolOutput } from "../tool-output.js";
|
|
9
|
+
const EXPLORE_SYSTEM = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
10
|
+
|
|
11
|
+
Your strengths:
|
|
12
|
+
- Rapidly finding files using glob patterns
|
|
13
|
+
- Searching code and text with powerful regex patterns
|
|
14
|
+
- Reading and analyzing file contents
|
|
15
|
+
|
|
16
|
+
Guidelines:
|
|
17
|
+
- Use glob for broad file pattern matching
|
|
18
|
+
- Use grep for searching file contents with regex
|
|
19
|
+
- Use read when you know the specific file path
|
|
20
|
+
- Use bash ONLY for read-only operations (ls, find, cat, wc, head, tail)
|
|
21
|
+
- Adapt your search approach based on the thoroughness level specified
|
|
22
|
+
- Return file paths as absolute paths in your final response
|
|
23
|
+
- Do NOT create, modify, or delete any files
|
|
24
|
+
- Do NOT run commands that change system state
|
|
25
|
+
|
|
26
|
+
Complete the search request efficiently and report findings clearly.
|
|
27
|
+
|
|
28
|
+
Working directory: ${process.cwd()}
|
|
29
|
+
Platform: ${process.platform}`;
|
|
30
|
+
const EXPLORE_MAX_STEPS = 20;
|
|
31
|
+
export function createExploreTool(modelId) {
|
|
32
|
+
return tool({
|
|
33
|
+
description: `Deep codebase exploration agent. Use this to understand project structure, find files by patterns, search code for keywords, trace module relationships, or answer questions about the codebase.
|
|
34
|
+
|
|
35
|
+
Specify thoroughness:
|
|
36
|
+
- "quick": basic search, 1-3 tool calls
|
|
37
|
+
- "medium": moderate exploration, follow references
|
|
38
|
+
- "thorough": comprehensive analysis across multiple locations and naming conventions
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
- "Find all API route handlers" (medium)
|
|
42
|
+
- "How does the auth system work?" (thorough)
|
|
43
|
+
- "Where is the database config?" (quick)`,
|
|
44
|
+
inputSchema: jsonSchema({
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
query: { type: "string", description: "What to explore or find in the codebase" },
|
|
48
|
+
thoroughness: { type: "string", description: "Search depth: quick, medium, or thorough (default: medium)" },
|
|
49
|
+
},
|
|
50
|
+
required: ["query"],
|
|
51
|
+
}),
|
|
52
|
+
execute: async ({ query, thoroughness }) => {
|
|
53
|
+
const level = thoroughness ?? "medium";
|
|
54
|
+
console.log(`\x1b[90m ┌─ Explore (${level}): ${query.slice(0, 60)}\x1b[0m`);
|
|
55
|
+
try {
|
|
56
|
+
const result = await runExploreAgent(query, level, modelId);
|
|
57
|
+
console.log(`\x1b[90m └─ ✓ Done\x1b[0m`);
|
|
58
|
+
return truncateToolOutput(result, { direction: "head" }).content;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.log(`\x1b[90m └─ ✗ Failed: ${err.message}\x1b[0m`);
|
|
62
|
+
return `Explore error: ${err.message}`;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function runExploreAgent(query, thoroughness, modelId) {
|
|
68
|
+
const model = resolveModel(modelId);
|
|
69
|
+
// Read-only tools only
|
|
70
|
+
const tools = {
|
|
71
|
+
glob: globTool,
|
|
72
|
+
grep: grepTool,
|
|
73
|
+
read: readTool,
|
|
74
|
+
bash: bashTool, // bash is available but prompt restricts to read-only
|
|
75
|
+
};
|
|
76
|
+
const prompt = `Thoroughness level: ${thoroughness}
|
|
77
|
+
${thoroughness === "quick" ? "Do a quick search (1-3 tool calls max)." : ""}
|
|
78
|
+
${thoroughness === "medium" ? "Do a moderate exploration, follow references if needed." : ""}
|
|
79
|
+
${thoroughness === "thorough" ? "Do a comprehensive analysis. Check multiple locations, naming conventions, and cross-references." : ""}
|
|
80
|
+
|
|
81
|
+
Task: ${query}`;
|
|
82
|
+
const messages = [{ role: "user", content: prompt }];
|
|
83
|
+
const result = streamText({
|
|
84
|
+
model,
|
|
85
|
+
system: EXPLORE_SYSTEM,
|
|
86
|
+
messages,
|
|
87
|
+
tools,
|
|
88
|
+
stopWhen: stepCountIs(EXPLORE_MAX_STEPS),
|
|
89
|
+
maxRetries: 2,
|
|
90
|
+
onError() { },
|
|
91
|
+
});
|
|
92
|
+
let assistantText = "";
|
|
93
|
+
for await (const event of result.fullStream) {
|
|
94
|
+
switch (event.type) {
|
|
95
|
+
case "text-delta":
|
|
96
|
+
assistantText += event.text;
|
|
97
|
+
break;
|
|
98
|
+
case "tool-call":
|
|
99
|
+
console.log(`\x1b[90m │ ⚡ ${event.toolName}\x1b[0m`);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return stripThinkingFromAssistantText(assistantText) || "(explore agent produced no output)";
|
|
104
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -6,8 +6,11 @@ import { globTool } from "./glob.js";
|
|
|
6
6
|
import { grepTool } from "./grep.js";
|
|
7
7
|
import { webSearchTool } from "./web_search.js";
|
|
8
8
|
import { webFetchTool } from "./web_fetch.js";
|
|
9
|
+
import { todoTool } from "./todo.js";
|
|
10
|
+
import { questionTool } from "./question.js";
|
|
11
|
+
import { codeSearchTool } from "./code_search.js";
|
|
9
12
|
export function createTools() {
|
|
10
|
-
|
|
13
|
+
const tools = {
|
|
11
14
|
bash: bashTool,
|
|
12
15
|
read: readTool,
|
|
13
16
|
write: writeTool,
|
|
@@ -16,5 +19,11 @@ export function createTools() {
|
|
|
16
19
|
grep: grepTool,
|
|
17
20
|
web_search: webSearchTool,
|
|
18
21
|
web_fetch: webFetchTool,
|
|
22
|
+
todo: todoTool,
|
|
23
|
+
question: questionTool,
|
|
19
24
|
};
|
|
25
|
+
if (process.env.EXA_API_KEY) {
|
|
26
|
+
tools.codesearch = codeSearchTool;
|
|
27
|
+
}
|
|
28
|
+
return tools;
|
|
20
29
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { tool, jsonSchema } from "ai";
|
|
2
|
+
import readline from "readline";
|
|
3
|
+
export const questionTool = tool({
|
|
4
|
+
description: `Ask the user a question to get clarification before proceeding. Use this when:
|
|
5
|
+
- The task is ambiguous and you need more information
|
|
6
|
+
- There are multiple valid approaches and you want the user to choose
|
|
7
|
+
- You need confirmation before a potentially destructive action
|
|
8
|
+
- You're unsure about a requirement or preference
|
|
9
|
+
|
|
10
|
+
Provide clear, specific questions. Optionally include numbered options for the user to choose from.`,
|
|
11
|
+
inputSchema: jsonSchema({
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
15
|
+
options: {
|
|
16
|
+
type: "array",
|
|
17
|
+
items: { type: "string" },
|
|
18
|
+
description: "Optional list of choices for the user to pick from",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["question"],
|
|
22
|
+
}),
|
|
23
|
+
execute: async ({ question, options }) => {
|
|
24
|
+
console.log();
|
|
25
|
+
console.log(`\x1b[33m❓ ${question}\x1b[0m`);
|
|
26
|
+
if (options && options.length > 0) {
|
|
27
|
+
for (let i = 0; i < options.length; i++) {
|
|
28
|
+
console.log(`\x1b[90m ${i + 1}. ${options[i]}\x1b[0m`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const answer = await askUser();
|
|
32
|
+
// If user picked a number and options exist, resolve it
|
|
33
|
+
if (options && options.length > 0) {
|
|
34
|
+
const idx = parseInt(answer) - 1;
|
|
35
|
+
if (idx >= 0 && idx < options.length) {
|
|
36
|
+
return `User chose: ${options[idx]}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return `User answered: ${answer}`;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
function askUser() {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const rl = readline.createInterface({
|
|
45
|
+
input: process.stdin,
|
|
46
|
+
output: process.stdout,
|
|
47
|
+
});
|
|
48
|
+
rl.question("\x1b[36m → \x1b[0m", (answer) => {
|
|
49
|
+
rl.close();
|
|
50
|
+
resolve(answer.trim() || "(no answer)");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
package/dist/tools/read.js
CHANGED
|
@@ -2,6 +2,9 @@ import { tool, jsonSchema } from "ai";
|
|
|
2
2
|
import { readFileSync, statSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { truncateToolOutput } from "../tool-output.js";
|
|
5
|
+
import { InstructionTracker } from "../instructions.js";
|
|
6
|
+
// Shared tracker instance for context-aware instruction discovery
|
|
7
|
+
const instructionTracker = new InstructionTracker();
|
|
5
8
|
export const readTool = tool({
|
|
6
9
|
description: "Read the contents of a file. Returns the file content as text. Use this to understand code, check configurations, etc.",
|
|
7
10
|
inputSchema: jsonSchema({
|
|
@@ -20,14 +23,22 @@ export const readTool = tool({
|
|
|
20
23
|
if (stat.isDirectory())
|
|
21
24
|
return `Error: ${filePath} is a directory, not a file`;
|
|
22
25
|
const content = readFileSync(resolved, "utf-8");
|
|
26
|
+
let result;
|
|
23
27
|
if (startLine || endLine) {
|
|
24
28
|
const lines = content.split("\n");
|
|
25
29
|
const start = (startLine ?? 1) - 1;
|
|
26
30
|
const end = endLine ?? lines.length;
|
|
27
|
-
|
|
28
|
-
return truncateToolOutput(slice, { direction: "head" }).content;
|
|
31
|
+
result = lines.slice(start, end).join("\n");
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
else {
|
|
34
|
+
result = content;
|
|
35
|
+
}
|
|
36
|
+
// Context-aware: discover nearby instruction files
|
|
37
|
+
const nearbyInstructions = instructionTracker.resolveForFile(resolved);
|
|
38
|
+
if (nearbyInstructions.length > 0) {
|
|
39
|
+
result += "\n\n<system-reminder>\n" + nearbyInstructions.join("\n\n") + "\n</system-reminder>";
|
|
40
|
+
}
|
|
41
|
+
return truncateToolOutput(result, { direction: "head" }).content;
|
|
31
42
|
}
|
|
32
43
|
catch (err) {
|
|
33
44
|
return `Error reading file: ${err.message}`;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { tool, jsonSchema, streamText, stepCountIs } from "ai";
|
|
2
|
+
import { resolveModel } from "../provider.js";
|
|
3
|
+
import { createTools } from "./index.js";
|
|
4
|
+
import { getMcpTools } from "../mcp.js";
|
|
5
|
+
import { getSkillsTool, getSkills } from "../skills.js";
|
|
6
|
+
import { loadPluginTools } from "../plugins.js";
|
|
7
|
+
import { DoomLoopDetector } from "../doom-loop.js";
|
|
8
|
+
import { stripThinkingFromAssistantText } from "../assistant-stream.js";
|
|
9
|
+
import { truncateToolOutput } from "../tool-output.js";
|
|
10
|
+
const SUB_AGENT_MAX_STEPS = 15;
|
|
11
|
+
const SUB_AGENT_SYSTEM = `You are a focused sub-agent executing a specific task. Complete the task thoroughly and return a clear result.
|
|
12
|
+
|
|
13
|
+
Rules:
|
|
14
|
+
- Focus only on the assigned task
|
|
15
|
+
- Be thorough but concise
|
|
16
|
+
- Use tools as needed to complete the task
|
|
17
|
+
- Return a clear summary of what you did and the result
|
|
18
|
+
|
|
19
|
+
Working directory: ${process.cwd()}
|
|
20
|
+
Platform: ${process.platform}
|
|
21
|
+
Date: ${new Date().toDateString()}`;
|
|
22
|
+
export function createTaskTool(modelId) {
|
|
23
|
+
return tool({
|
|
24
|
+
description: `Launch a sub-agent to execute a task independently. The sub-agent has its own context and tools. Use this for:
|
|
25
|
+
- Parallel execution: call multiple tasks at once for independent work
|
|
26
|
+
- Context isolation: keep the main conversation clean while the sub-agent explores
|
|
27
|
+
- Delegation: hand off well-defined subtasks (search, analysis, file operations)
|
|
28
|
+
|
|
29
|
+
The sub-agent can read/write files, run commands, search, and use all available tools.
|
|
30
|
+
Call multiple tasks in parallel when the work is independent.`,
|
|
31
|
+
inputSchema: jsonSchema({
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
description: { type: "string", description: "Short description of the task (shown to user)" },
|
|
35
|
+
prompt: { type: "string", description: "Detailed instructions for the sub-agent" },
|
|
36
|
+
},
|
|
37
|
+
required: ["description", "prompt"],
|
|
38
|
+
}),
|
|
39
|
+
execute: async ({ description, prompt }) => {
|
|
40
|
+
console.log(`\x1b[90m ┌─ Sub-agent: ${description}\x1b[0m`);
|
|
41
|
+
try {
|
|
42
|
+
const result = await runSubAgent(prompt, modelId);
|
|
43
|
+
console.log(`\x1b[90m └─ ✓ Done\x1b[0m`);
|
|
44
|
+
return truncateToolOutput(result, { direction: "head" }).content;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.log(`\x1b[90m └─ ✗ Failed: ${err.message}\x1b[0m`);
|
|
48
|
+
return `Sub-agent error: ${err.message}`;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function runSubAgent(prompt, modelId) {
|
|
54
|
+
const model = resolveModel(modelId);
|
|
55
|
+
// Build tools for sub-agent (no task tool to prevent recursion)
|
|
56
|
+
const builtinTools = createTools();
|
|
57
|
+
const mcpTools = getMcpTools();
|
|
58
|
+
const pluginTools = await loadPluginTools();
|
|
59
|
+
const skills = getSkills();
|
|
60
|
+
const allTools = { ...builtinTools, ...pluginTools };
|
|
61
|
+
for (const [id, t] of Object.entries(mcpTools))
|
|
62
|
+
allTools[id] = t;
|
|
63
|
+
if (skills.length > 0)
|
|
64
|
+
allTools["skill"] = getSkillsTool();
|
|
65
|
+
// Remove task tool from sub-agent to prevent infinite recursion
|
|
66
|
+
delete allTools["task"];
|
|
67
|
+
const messages = [{ role: "user", content: prompt }];
|
|
68
|
+
const doomLoop = new DoomLoopDetector();
|
|
69
|
+
const result = streamText({
|
|
70
|
+
model,
|
|
71
|
+
system: SUB_AGENT_SYSTEM,
|
|
72
|
+
messages,
|
|
73
|
+
tools: allTools,
|
|
74
|
+
stopWhen: stepCountIs(SUB_AGENT_MAX_STEPS),
|
|
75
|
+
maxRetries: 2,
|
|
76
|
+
onError() { },
|
|
77
|
+
});
|
|
78
|
+
let assistantText = "";
|
|
79
|
+
for await (const event of result.fullStream) {
|
|
80
|
+
switch (event.type) {
|
|
81
|
+
case "text-delta":
|
|
82
|
+
assistantText += event.text;
|
|
83
|
+
break;
|
|
84
|
+
case "tool-call":
|
|
85
|
+
if (doomLoop.record(event.toolName, event.input)) {
|
|
86
|
+
return assistantText + "\n\n[Sub-agent stopped: doom loop detected]";
|
|
87
|
+
}
|
|
88
|
+
console.log(`\x1b[90m │ ⚡ ${event.toolName}\x1b[0m`);
|
|
89
|
+
break;
|
|
90
|
+
case "tool-result":
|
|
91
|
+
break;
|
|
92
|
+
case "error":
|
|
93
|
+
return assistantText + `\n\n[Sub-agent error: ${event.error}]`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const cleaned = stripThinkingFromAssistantText(assistantText);
|
|
97
|
+
return cleaned || "(sub-agent produced no output)";
|
|
98
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { tool, jsonSchema } from "ai";
|
|
2
|
+
let todos = [];
|
|
3
|
+
let nextId = 1;
|
|
4
|
+
export function getTodos() {
|
|
5
|
+
return todos;
|
|
6
|
+
}
|
|
7
|
+
export function resetTodos() {
|
|
8
|
+
todos = [];
|
|
9
|
+
nextId = 1;
|
|
10
|
+
}
|
|
11
|
+
function formatTodos() {
|
|
12
|
+
if (todos.length === 0)
|
|
13
|
+
return "No tasks.";
|
|
14
|
+
const icons = {
|
|
15
|
+
pending: "○",
|
|
16
|
+
in_progress: "◐",
|
|
17
|
+
done: "●",
|
|
18
|
+
cancelled: "✕",
|
|
19
|
+
};
|
|
20
|
+
return todos
|
|
21
|
+
.map((t) => ` ${icons[t.status]} #${t.id} ${t.text}`)
|
|
22
|
+
.join("\n");
|
|
23
|
+
}
|
|
24
|
+
function printTodos() {
|
|
25
|
+
if (todos.length === 0)
|
|
26
|
+
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
|
+
}
|
|
37
|
+
console.log(`\x1b[90m └${"─".repeat(44)}\x1b[0m`);
|
|
38
|
+
}
|
|
39
|
+
export const todoTool = tool({
|
|
40
|
+
description: `Create or update tasks to track progress. Use this FREQUENTLY to:
|
|
41
|
+
- Plan multi-step work by creating tasks upfront
|
|
42
|
+
- Mark tasks as in_progress when starting them
|
|
43
|
+
- Mark tasks as done when completed
|
|
44
|
+
- Give the user visibility into your progress
|
|
45
|
+
|
|
46
|
+
To create new tasks: provide items with "text" and optionally "status" (defaults to "pending").
|
|
47
|
+
To update existing tasks: provide items with "id" and "status".
|
|
48
|
+
You can mix creates and updates in one call.`,
|
|
49
|
+
inputSchema: jsonSchema({
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
todos: {
|
|
53
|
+
type: "array",
|
|
54
|
+
description: "List of tasks to create or update",
|
|
55
|
+
items: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
text: { type: "string", description: "Task description (for new tasks)" },
|
|
59
|
+
status: { type: "string", description: "Status: pending, in_progress, done, cancelled" },
|
|
60
|
+
id: { type: "number", description: "Task ID (for updating existing tasks)" },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
required: ["todos"],
|
|
66
|
+
}),
|
|
67
|
+
execute: async ({ todos: items }) => {
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
if (item.id) {
|
|
70
|
+
// Update existing
|
|
71
|
+
const existing = todos.find((t) => t.id === item.id);
|
|
72
|
+
if (existing && item.status) {
|
|
73
|
+
existing.status = item.status;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (item.text) {
|
|
77
|
+
// Create new
|
|
78
|
+
todos.push({
|
|
79
|
+
id: nextId++,
|
|
80
|
+
text: item.text,
|
|
81
|
+
status: item.status ?? "pending",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
printTodos();
|
|
86
|
+
return formatTodos();
|
|
87
|
+
},
|
|
88
|
+
});
|