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.
@@ -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
+ }
@@ -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
- return {
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
+ }
@@ -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
- const slice = lines.slice(start, end).join("\n");
28
- return truncateToolOutput(slice, { direction: "head" }).content;
31
+ result = lines.slice(start, end).join("\n");
29
32
  }
30
- return truncateToolOutput(content, { direction: "head" }).content;
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
+ });