diff --git a/.changeset/great-foxes-see.md b/.changeset/great-foxes-see.md new file mode 100644 index 000000000..4fcfe98fe --- /dev/null +++ b/.changeset/great-foxes-see.md @@ -0,0 +1,120 @@ +--- +"@voltagent/core": minor +--- + +## Introducing Toolkits for Better Tool Management + +Managing related tools and their instructions is now simpler with `Toolkit`s. + +**Motivation:** + +- Defining shared instructions for multiple related tools was cumbersome. +- The logic for deciding which instructions to add to the agent's system prompt could become complex. +- We wanted a cleaner way to group tools logically. + +**What's New: The `Toolkit`** + +A `Toolkit` bundles related tools and allows defining shared `instructions` and an `addInstructions` flag _at the toolkit level_. + +```typescript +// packages/core/src/tool/toolkit.ts +export type Toolkit = { + /** + * Unique identifier name for the toolkit. + */ + name: string; + /** + * A brief description of what the toolkit does. Optional. + */ + description?: string; + /** + * Shared instructions for the LLM on how to use the tools within this toolkit. + * Optional. + */ + instructions?: string; + /** + * Whether to automatically add the toolkit's `instructions` to the agent's system prompt. + * Defaults to false. + */ + addInstructions?: boolean; + /** + * An array of Tool instances that belong to this toolkit. + */ + tools: Tool[]; +}; +``` + +**Key Changes to Core:** + +1. **`ToolManager` Upgrade:** Now manages both `Tool` and `Toolkit` objects. +2. **`AgentOptions` Update:** The `tools` option accepts `(Tool | Toolkit)[]`. +3. **Simplified Instruction Handling:** `Agent` now only adds instructions from `Toolkit`s where `addInstructions` is true. + +This change leads to a clearer separation of concerns, simplifies the agent's internal logic, and makes managing tool instructions more predictable and powerful. + +### New `createToolkit` Helper + +We've also added a helper function, `createToolkit`, to simplify the creation of toolkits. It provides default values and basic validation: + +```typescript +// packages/core/src/tool/toolkit.ts +export const createToolkit = (options: Toolkit): Toolkit => { + if (!options.name) { + throw new Error("Toolkit name is required"); + } + if (!options.tools || options.tools.length === 0) { + console.warn(`Toolkit '${options.name}' created without any tools.`); + } + + return { + name: options.name, + description: options.description || "", // Default empty description + instructions: options.instructions, + addInstructions: options.addInstructions || false, // Default to false + tools: options.tools || [], // Default to empty array + }; +}; +``` + +**Example Usage:** + +```typescript +import { createTool, createToolkit } from "@voltagent/core"; +import { z } from "zod"; + +// Define some tools first +const getWeather = createTool({ + name: "getWeather", + description: "Gets the weather for a location.", + schema: z.object({ location: z.string() }), + run: async ({ location }) => ({ temperature: "25C", condition: "Sunny" }), +}); + +const searchWeb = createTool({ + name: "searchWeb", + description: "Searches the web for a query.", + schema: z.object({ query: z.string() }), + run: async ({ query }) => ({ results: ["Result 1", "Result 2"] }), +}); + +// Create a toolkit using the helper +const webInfoToolkit = createToolkit({ + name: "web_information", + description: "Tools for getting information from the web.", + instructions: "Use these tools to find current information online.", + addInstructions: true, // Add the instructions to the system prompt + tools: [getWeather, searchWeb], +}); + +console.log(webInfoToolkit); +/* +Output: +{ + name: 'web_information', + description: 'Tools for getting information from the web.', + instructions: 'Use these tools to find current information online.', + addInstructions: true, + tools: [ [Object Tool: getWeather], [Object Tool: searchWeb] ] +} +*/ +``` diff --git a/.changeset/young-moments-sink.md b/.changeset/young-moments-sink.md new file mode 100644 index 000000000..740af41cd --- /dev/null +++ b/.changeset/young-moments-sink.md @@ -0,0 +1,54 @@ +--- +"@voltagent/core": minor +--- + +## Introducing Reasoning Tools Helper + +This update introduces a new helper function, `createReasoningTools`, to easily add step-by-step reasoning capabilities to your agents. #24 + +### New `createReasoningTools` Helper + +**Feature:** Easily add `think` and `analyze` tools for step-by-step reasoning. + +We've added a new helper function, `createReasoningTools`, which makes it trivial to equip your agents with structured thinking capabilities, similar to patterns seen in advanced AI systems. + +- **What it does:** Returns a pre-configured `Toolkit` named `reasoning_tools`. +- **Tools included:** Contains the `think` tool (for internal monologue/planning) and the `analyze` tool (for evaluating results and deciding next steps). +- **Instructions:** Includes detailed instructions explaining how the agent should use these tools iteratively to solve problems. You can choose whether these instructions are automatically added to the system prompt via the `addInstructions` option. + +```typescript +import { createReasoningTools, type Toolkit } from "@voltagent/core"; + +// Get the reasoning toolkit (with instructions included in the system prompt) +const reasoningToolkit: Toolkit = createReasoningTools({ addInstructions: true }); + +// Get the toolkit without automatically adding instructions +const reasoningToolkitManual: Toolkit = createReasoningTools({ addInstructions: false }); +``` + +### How to Use Reasoning Tools + +Pass the `Toolkit` object returned by `createReasoningTools` directly to the agent's `tools` array. + +```typescript +// Example: Using the new reasoning tools helper +import { Agent, createReasoningTools, type Toolkit } from "@voltagent/core"; +import { VercelAIProvider } from "@voltagent/vercel-ai"; +import { openai } from "@ai-sdk/openai"; + +const reasoningToolkit: Toolkit = createReasoningTools({ + addInstructions: true, +}); + +const agent = new Agent({ + name: "MyThinkingAgent", + description: "An agent equipped with reasoning tools.", + llm: new VercelAIProvider(), + model: openai("gpt-4o-mini"), + tools: [reasoningToolkit], // Pass the toolkit +}); + +// Agent's system message will include reasoning instructions. +``` + +This change simplifies adding reasoning capabilities to your agents. diff --git a/examples/base/src/index.ts b/examples/base/src/index.ts index fd602a969..332b1c076 100644 --- a/examples/base/src/index.ts +++ b/examples/base/src/index.ts @@ -1,17 +1,26 @@ -import { VoltAgent, Agent } from "@voltagent/core"; +import { openai } from "@ai-sdk/openai"; +import { Agent, MCPConfiguration } from "@voltagent/core"; import { VercelAIProvider } from "@voltagent/vercel-ai"; -import { openai } from "@ai-sdk/openai"; +const mcpConfig = new MCPConfiguration({ + servers: { + github: { + type: "http", + url: "https://mcp.composio.dev/github/mealy-few-wire-UfUhSQ?agent=cursor", + }, + }, +}); const agent = new Agent({ - name: "Asistant", - description: "A helpful assistant that answers questions without using tools", + name: "GitHub Starrer Agent", + description: "You help users star GitHub repositories", llm: new VercelAIProvider(), model: openai("gpt-4o-mini"), + tools: await mcpConfig.getTools(), }); -new VoltAgent({ - agents: { - agent, - }, -}); +const result = await agent.streamText("Please star the repository 'composiohq/composio'"); + +for await (const chunk of result.textStream) { + process.stdout.write(chunk); +} diff --git a/examples/with-thinking-tool/.gitignore b/examples/with-thinking-tool/.gitignore new file mode 100644 index 000000000..99f7bea53 --- /dev/null +++ b/examples/with-thinking-tool/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.DS_Store +.env \ No newline at end of file diff --git a/examples/with-thinking-tool/README.md b/examples/with-thinking-tool/README.md new file mode 100644 index 000000000..492df452d --- /dev/null +++ b/examples/with-thinking-tool/README.md @@ -0,0 +1,72 @@ +
+ +435380213-b6253409-8741-462b-a346-834cd18565a9 + + +
+
+ +
+ Home Page | + Documentation | + Examples | + Discord | + Blog +
+
+ +
+ +
+ VoltAgent Example: Using Reasoning Tools (`think` & `analyze`)
+This example demonstrates how to equip a VoltAgent with `think` and `analyze` tools to enable step-by-step reasoning and analysis during task execution. +
+
+
+ +
+ +[![npm version](https://img.shields.io/npm/v/@voltagent/core.svg)](https://www.npmjs.com/package/@voltagent/core) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](CODE_OF_CONDUCT.md) +[![Discord](https://img.shields.io/discord/1361559153780195478.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://s.voltagent.dev/discord) +[![Twitter Follow](https://img.shields.io/twitter/follow/voltagent_dev?style=social)](https://twitter.com/voltagent_dev) + +
+ +
+ +## VoltAgent: With Thinking Tools Example + +This example showcases how to integrate the `think` and `analyze` tools from `@voltagent/core` into an agent. These tools allow the agent to perform structured reasoning: + +- **`think`**: Acts as a scratchpad for the agent to break down problems, plan steps, and explain its reasoning before taking action. +- **`analyze`**: Allows the agent to evaluate the results of its actions or thoughts and decide on the next step (continue, validate, or provide a final answer). + +By using these tools, agents can tackle more complex tasks, exhibit clearer thought processes, and potentially improve their accuracy and reliability. + +This example sets up a basic agent, includes the reasoning tools, and logs the reasoning steps emitted by the agent during its interaction. + +## Setup + +1. **Clone the repository (if you haven't already):** + ```bash + git clone https://github.com/voltagent/voltagent.git + cd voltagent/examples/with-thinking-tool + ``` +2. **Install dependencies:** + ```bash + npm install + ``` +3. **Set up environment variables:** + Create a `.env` file in this directory (`examples/with-thinking-tool`) and add your OpenAI API key: + ```env + OPENAI_API_KEY=your_openai_api_key_here + ``` + +## Run the Example + +```bash +npm run dev +``` + +Observe the console output. You should see the agent's final response as well as the `ReasoningStep` objects logged whenever the agent uses the `think` or `analyze` tools. diff --git a/examples/with-thinking-tool/package.json b/examples/with-thinking-tool/package.json new file mode 100644 index 000000000..c5455b4ff --- /dev/null +++ b/examples/with-thinking-tool/package.json @@ -0,0 +1,33 @@ +{ + "name": "voltagent-example-with-thinking-tool", + "private": true, + "keywords": [ + "voltagent", + "ai", + "agent", + "reasoning", + "think", + "analyze" + ], + "license": "MIT", + "author": "", + "type": "module", + "scripts": { + "build": "tsc", + "dev": "tsx watch --env-file=.env ./src", + "start": "node dist/index.js", + "volt": "volt" + }, + "dependencies": { + "@ai-sdk/openai": "^1.3.10", + "@voltagent/cli": "^0.1.2", + "@voltagent/core": "^0.1.5", + "@voltagent/vercel-ai": "^0.1.2", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + } +} diff --git a/examples/with-thinking-tool/src/index.ts b/examples/with-thinking-tool/src/index.ts new file mode 100644 index 000000000..72a43f9fb --- /dev/null +++ b/examples/with-thinking-tool/src/index.ts @@ -0,0 +1,32 @@ +import { VoltAgent, Agent, createReasoningTools, type Toolkit } from "@voltagent/core"; +import { VercelAIProvider } from "@voltagent/vercel-ai"; +import { openai } from "@ai-sdk/openai"; + +const reasoningToolkit: Toolkit = createReasoningTools({ + addInstructions: true, +}); + +const agent = new Agent({ + name: "ThinkingAssistant", + description: ` + You are an AI assistant designed for complex problem-solving and structured reasoning. + You leverage internal 'think' and 'analyze' tools to methodically work through challenges. + + Your process involves: + 1. **Understanding:** Using 'think' to clarify the goal, constraints, and break down the problem. + 2. **Planning:** Again using 'think', outlining sequential steps, identifying information needs, or exploring potential strategies before taking action (like calling other tools). + 3. **Analyzing:** Employing 'analyze' after gathering information or completing steps to evaluate progress, check if results meet requirements, and decide the next logical move (e.g., continue the plan, revise the plan, conclude). + + Your aim is to provide well-reasoned, accurate, and complete answers by thinking through the process internally. + `, + llm: new VercelAIProvider(), + model: openai("gpt-4o-mini"), + tools: [reasoningToolkit], + markdown: true, +}); + +new VoltAgent({ + agents: { + agent, + }, +}); diff --git a/examples/with-thinking-tool/tsconfig.json b/examples/with-thinking-tool/tsconfig.json new file mode 100644 index 000000000..cee90c6f3 --- /dev/null +++ b/examples/with-thinking-tool/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core/src/agent/hooks/index.spec.ts b/packages/core/src/agent/hooks/index.spec.ts index 76e057481..c1d80b6a6 100644 --- a/packages/core/src/agent/hooks/index.spec.ts +++ b/packages/core/src/agent/hooks/index.spec.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { type AgentHooks, createHooks } from "."; -import type { AgentTool } from "../../tool"; +import { createTool, type AgentTool } from "../../tool"; import { Agent } from "../index"; // Mock LLM provider @@ -34,12 +34,12 @@ describe("Agent Hooks Functionality", () => { hooks = createHooks(); agent = createTestAgent("TestAgent"); sourceAgent = createTestAgent("SourceAgent"); - tool = { + tool = createTool({ name: "test-tool", description: "A test tool", parameters: z.object({}), execute: jest.fn().mockResolvedValue("Tool result"), - }; + }); // Set hooks on the agent agent.hooks = hooks; @@ -104,11 +104,11 @@ describe("Agent Hooks Functionality", () => { }); // Add a test tool to the agent - agent.addTools([tool]); + agent.addItems([tool]); // Directly execute the hooks to test their functionality - await agent.hooks.onToolStart!(agent, tool); - await agent.hooks.onToolEnd!(agent, tool, "Tool result"); + await agent.hooks.onToolStart?.(agent, tool); + await agent.hooks.onToolEnd?.(agent, tool, "Tool result"); // Verify hooks were called with correct arguments expect(onToolStartSpy).toHaveBeenCalledWith(agent, tool); diff --git a/packages/core/src/agent/index.spec.ts b/packages/core/src/agent/index.spec.ts index fedea6de0..286c861a1 100644 --- a/packages/core/src/agent/index.spec.ts +++ b/packages/core/src/agent/index.spec.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { AgentEventEmitter } from "../events"; import type { MemoryMessage } from "../memory/types"; import { AgentRegistry } from "../server/registry"; -import type { AgentTool } from "../tool"; +import { createTool } from "../tool"; import { Agent } from "./index"; import type { BaseMessage, @@ -18,7 +18,7 @@ import type { // @ts-ignore - To simplify test types import type { AgentHistoryEntry } from "../agent/history"; -import { AgentStatus } from "./types"; +import type { AgentStatus } from "./types"; // Define a generic mock model type locally type MockModelType = { modelId: string; [key: string]: any }; @@ -516,14 +516,15 @@ describe("Agent", () => { it("should store tool-related messages in memory when tools are used", async () => { const userId = "test-user"; const message = "Use the test tool"; - const mockTool: AgentTool = { + const mockTool = createTool({ + id: "test-tool", name: "test-tool", description: "A test tool", parameters: z.object({}), execute: async () => "tool result", - }; + }); - agent.addTools([mockTool]); + agent.addItems([mockTool]); await agent.generateText(message, { userId }); @@ -589,10 +590,6 @@ describe("Agent", () => { "emitAgentUnregistered", ); - // Spy on historyManager's completeEntry method - // @ts-ignore - This method exists in HistoryManager but the TypeScript definition might be missing - const historyManager = agent.getHistoryManager(); - // Add active history entry to prepare for unregister await agent.generateText("Hello before unregister!"); @@ -619,14 +616,14 @@ describe("Agent", () => { it("should return full state with correct structure", () => { // Add a tool for better state testing - const mockTool: AgentTool = { + const mockTool = createTool({ name: "state-test-tool", description: "A test tool for state", parameters: z.object({}), execute: async () => "tool result", - }; + }); - agent.addTools([mockTool]); + agent.addItems([mockTool]); // Get full state const state = agent.getFullState(); @@ -691,14 +688,14 @@ describe("Agent", () => { // registering the agent with the registry first const spy = jest.spyOn(AgentEventEmitter.getInstance(), "createTrackedEvent"); - const mockTool: AgentTool = { + const mockTool = createTool({ name: "test-tool", description: "A test tool", parameters: z.object({}), execute: async () => "tool result", - }; + }); - agent.addTools([mockTool]); + agent.addItems([mockTool]); await agent.generateText("Use the test tool"); // Test skipped because registry integration is required diff --git a/packages/core/src/agent/index.ts b/packages/core/src/agent/index.ts index 18f61473c..79a268d92 100644 --- a/packages/core/src/agent/index.ts +++ b/packages/core/src/agent/index.ts @@ -2,11 +2,18 @@ import type { z } from "zod"; import { AgentEventEmitter } from "../events"; import type { EventStatus, EventUpdater } from "../events"; import { MemoryManager } from "../memory"; -import type { AgentTool } from "../tool"; +import type { Tool, Toolkit } from "../tool"; import { ToolManager } from "../tool"; +import type { ReasoningToolExecuteOptions } from "../tool/reasoning/types"; import { type AgentHistoryEntry, HistoryManager } from "./history"; import { type AgentHooks, createHooks } from "./hooks"; -import type { BaseMessage, BaseTool, LLMProvider, StepWithContent } from "./providers"; +import type { + BaseMessage, + BaseTool, + LLMProvider, + StepWithContent, + ToolExecuteOptions, +} from "./providers"; import { SubAgentManager } from "./subagent"; import type { AgentOptions, @@ -66,6 +73,11 @@ export class Agent }> { */ readonly voice?: Voice; + /** + * Indicates if the agent should format responses using Markdown. + */ + readonly markdown: boolean; + /** * Memory manager for the agent */ @@ -103,6 +115,7 @@ export class Agent }> { hooks?: AgentHooks; retriever?: BaseRetriever; voice?: Voice; + markdown?: boolean; }, ) { this.id = options.id || options.name; @@ -112,8 +125,9 @@ export class Agent }> { this.model = options.model; this.retriever = options.retriever; this.voice = options.voice; + this.markdown = options.markdown ?? false; - // Initialize hooks - support both AgentHooks instance and plain object + // Initialize hooks if (options.hooks) { this.hooks = options.hooks; } else { @@ -123,13 +137,13 @@ export class Agent }> { // Initialize memory manager this.memoryManager = new MemoryManager(this.id, options.memory, options.memoryOptions || {}); - // Initialize tool manager + // Initialize tool manager (tools are now passed directly) this.toolManager = new ToolManager(options.tools || []); // Initialize sub-agent manager this.subAgentManager = new SubAgentManager(this.name, options.subAgents || []); - // Initialize history manager with agent ID + // Initialize history manager this.historyManager = new HistoryManager( options.maxHistoryEntries || 0, this.id, @@ -149,7 +163,31 @@ export class Agent }> { historyEntryId: string; contextMessages: BaseMessage[]; }): Promise { - let description = this.description; + let baseDescription = this.description || ""; // Ensure baseDescription is a string + + // --- Add Instructions from Toolkits --- (Simplified Logic) + let toolInstructions = ""; + // Get only the toolkits + const toolkits = this.toolManager.getToolkits(); + for (const toolkit of toolkits) { + // Check if the toolkit wants its instructions added + if (toolkit.addInstructions && toolkit.instructions) { + // Append toolkit instructions + // Using a simple newline separation for now. + toolInstructions += `\n\n${toolkit.instructions}`; + } + } + if (toolInstructions) { + baseDescription = `${baseDescription}${toolInstructions}`; + } + // --- End Add Instructions from Toolkits --- + + // Add Markdown Instruction if Enabled + if (this.markdown) { + baseDescription = `${baseDescription}\n\nUse markdown to format your answers.`; + } + + let description = baseDescription; // If retriever exists and we have input, get context if (this.retriever && input && historyEntryId) { @@ -280,17 +318,38 @@ export class Agent }> { tools: BaseTool[]; maxSteps: number; } { - const { tools: dynamicTools } = options; - - // Get tools from tool manager - const toolsToUse = this.toolManager.prepareToolsForGeneration(dynamicTools); + const { tools: dynamicTools, historyEntryId } = options; + const baseTools = this.toolManager.prepareToolsForGeneration(dynamicTools); + + // Wrap Reasoning Tools Execution (Remove enableReasoning check) + const toolsToUse = baseTools.map((tool) => { + // Wrap 'think' and 'analyze' tools unconditionally if found by name + if (tool.name === "think" || tool.name === "analyze") { + const originalExecute = tool.execute; + return { + ...tool, + execute: async (args: any, execOptions?: ToolExecuteOptions): Promise => { + const reasoningOptions: ReasoningToolExecuteOptions = { + ...execOptions, + agentId: this.id, + historyEntryId: historyEntryId || "unknown", + }; + if (!historyEntryId) { + console.warn(`Executing reasoning tool '${tool.name}' without a historyEntryId.`); + } + return originalExecute(args, reasoningOptions); + }, + }; + } + return tool; // Return other tools unchanged + }); // If this agent has sub-agents, always create a new delegate tool with current historyEntryId if (this.subAgentManager.hasSubAgents()) { // Always create a delegate tool with the current historyEntryId const delegateTool = this.subAgentManager.createDelegateTool({ sourceAgent: this, - currentHistoryEntryId: options.historyEntryId, + currentHistoryEntryId: historyEntryId, ...options, }); @@ -302,7 +361,9 @@ export class Agent }> { toolsToUse.push(delegateTool); // Add the delegate tool to the tool manager only if it doesn't exist yet - this.toolManager.addTools([delegateTool]); + // This logic might need refinement if delegate tool should always be added/replaced + // For now, assume adding if not present is correct. + // this.toolManager.addTools([delegateTool]); // Re-consider if this is needed or handled by prepareToolsForGeneration } } @@ -1274,7 +1335,7 @@ export class Agent }> { const delegateTool = this.subAgentManager.createDelegateTool({ sourceAgent: this, }); - this.toolManager.addTools([delegateTool]); + this.toolManager.addTool(delegateTool); } } @@ -1339,22 +1400,19 @@ export class Agent }> { } /** - * Add one or more tools to the agent - * If a tool with the same name already exists, it will be replaced - * @returns Object containing added tools + * Add one or more tools or toolkits to the agent. + * Delegates to ToolManager's addItems method. + * @returns Object containing added items (difficult to track precisely here, maybe simplify return) */ - addTools(tools: AgentTool[]): { added: AgentTool[] } { - const result = { - added: [] as AgentTool[], - }; + addItems(items: (Tool | Toolkit)[]): { added: (Tool | Toolkit)[] } { + // ToolManager handles the logic of adding tools vs toolkits and checking conflicts + this.toolManager.addItems(items); - for (const tool of tools) { - try { - this.toolManager.addTool(tool); - result.added.push(tool); - } catch (error) {} - } - - return result; + // Returning the original list as 'added' might be misleading if conflicts occurred. + // A simpler approach might be to return void or let ToolManager handle logging. + // For now, returning the input list for basic feedback. + return { + added: items, + }; } } diff --git a/packages/core/src/agent/providers/base/types.ts b/packages/core/src/agent/providers/base/types.ts index 7795840e1..9c3564e8a 100644 --- a/packages/core/src/agent/providers/base/types.ts +++ b/packages/core/src/agent/providers/base/types.ts @@ -1,5 +1,6 @@ import type { z } from "zod"; import type { ProviderOptions } from "../../types"; +import { Tool } from "../../../tool"; /** * Token usage information @@ -198,12 +199,7 @@ export type ToolExecuteOptions = { [key: string]: any; }; -export type BaseTool = { - name: string; - description: string; - parameters: ToolSchema; - execute: (params: TParams, options?: ToolExecuteOptions) => Promise; -}; +export type BaseTool = Tool; export type BaseToolCall = { name: string; diff --git a/packages/core/src/agent/subagent/index.ts b/packages/core/src/agent/subagent/index.ts index a42193944..75bd58b21 100644 --- a/packages/core/src/agent/subagent/index.ts +++ b/packages/core/src/agent/subagent/index.ts @@ -4,7 +4,7 @@ import type { Agent } from "../index"; import type { BaseMessage } from "../providers"; import type { BaseTool } from "../providers"; import type { AgentHandoffOptions, AgentHandoffResult } from "../types"; - +import { createTool } from "../../tool"; /** * SubAgentManager - Manages sub-agents and delegation functionality for an Agent */ @@ -266,7 +266,8 @@ Context: ${JSON.stringify(context)}`, * Create a delegate tool for sub-agents */ public createDelegateTool(options: Record = {}): BaseTool { - return { + return createTool({ + id: "delegate_task", name: "delegate_task", description: "Delegate a task to one or more specialized agents", parameters: z.object({ @@ -352,7 +353,7 @@ Context: ${JSON.stringify(context)}`, }; } }, - }; + }); } /** diff --git a/packages/core/src/agent/types.ts b/packages/core/src/agent/types.ts index 422860f84..81132f71a 100644 --- a/packages/core/src/agent/types.ts +++ b/packages/core/src/agent/types.ts @@ -1,6 +1,6 @@ import type { BaseMessage } from "../agent/providers/base/types"; import type { Memory, MemoryOptions } from "../memory/types"; -import type { AgentTool } from "../tool"; +import type { Tool, Toolkit } from "../tool"; import type { LLMProvider } from "./providers"; import type { BaseTool } from "./providers"; import type { StepWithContent } from "./providers"; @@ -70,9 +70,9 @@ export type AgentOptions = { memoryOptions?: MemoryOptions; /** - * Tools that the agent can use + * Tools and/or Toolkits that the agent can use */ - tools?: AgentTool[]; + tools?: (Tool | Toolkit)[]; /** * Sub-agents that this agent can delegate tasks to diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c46d55b7b..a25f42eb1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -6,6 +6,7 @@ import { checkForUpdates } from "./utils/update"; export * from "./agent"; export * from "./agent/hooks"; export * from "./tool"; +export * from "./tool/reasoning/index"; export * from "./memory"; export * from "./agent/providers"; export type { AgentOptions, AgentResponse, ModelToolCall } from "./agent/types"; diff --git a/packages/core/src/mcp/client/index.spec.ts b/packages/core/src/mcp/client/index.spec.ts index ce6d1ef4f..ffb30a3ce 100644 --- a/packages/core/src/mcp/client/index.spec.ts +++ b/packages/core/src/mcp/client/index.spec.ts @@ -272,20 +272,24 @@ describe("MCPClient", () => { expect(mockListTools).toHaveBeenCalled(); expect(jsonSchemaToZod).toHaveBeenCalledTimes(2); - expect(agentTools).toEqual({ - TestClient_tool1: { - name: "TestClient_tool1", - description: "Tool 1 description", - parameters: {}, - execute: expect.any(Function), - }, - TestClient_tool2: { - name: "TestClient_tool2", - description: "Tool 2 description", - parameters: {}, - execute: expect.any(Function), - }, - }); + expect(agentTools).toEqual( + expect.objectContaining({ + TestClient_tool1: { + name: "TestClient_tool1", + id: expect.any(String), + description: "Tool 1 description", + parameters: {}, + execute: expect.any(Function), + }, + TestClient_tool2: { + id: expect.any(String), + name: "TestClient_tool2", + description: "Tool 2 description", + parameters: {}, + execute: expect.any(Function), + }, + }), + ); }); it("should handle errors when getting agent tools", async () => { @@ -308,6 +312,7 @@ describe("MCPClient", () => { expect(agentTools).toEqual({ TestClient_tool2: { + id: expect.any(String), name: "TestClient_tool2", description: "Tool 2 description", parameters: {}, diff --git a/packages/core/src/mcp/client/index.ts b/packages/core/src/mcp/client/index.ts index 38f805e9c..10edc445c 100644 --- a/packages/core/src/mcp/client/index.ts +++ b/packages/core/src/mcp/client/index.ts @@ -22,7 +22,7 @@ import type { MCPToolResult, StdioServerConfig, } from "../types"; - +import { createTool, type Tool } from "../../tool"; /** * Client for interacting with Model Context Protocol (MCP) servers * Implements MCP specification using the official SDK @@ -169,14 +169,14 @@ export class MCPClient extends EventEmitter { * Get tools converted to AgentTools with execute functions * This method transforms MCP tool definitions into AgentTools that can be used directly by an Agent */ - async getAgentTools(): Promise> { + async getAgentTools(): Promise>> { await this.ensureConnected(); try { const { tools } = await this.client.listTools(); // Convert tools to AgentTools with execute functions - const agentToolsRecord: Record = {}; + const agentToolsRecord: Record> = {}; for (const tool of tools) { try { @@ -188,7 +188,7 @@ export class MCPClient extends EventEmitter { // Create AgentTool with both parameters and inputSchema // parameters is used by Vercel AI, inputSchema is used internally - const agentTool = { + const agentTool = createTool({ name: namespacedToolName, description: tool.description || "", parameters: zodSchema, @@ -205,7 +205,7 @@ export class MCPClient extends EventEmitter { throw e; } }, - }; + }); // Store the tool using the namespaced name as key agentToolsRecord[namespacedToolName] = agentTool; diff --git a/packages/core/src/mcp/configuration/index.ts b/packages/core/src/mcp/configuration/index.ts index a396ecc53..1e6abec20 100644 --- a/packages/core/src/mcp/configuration/index.ts +++ b/packages/core/src/mcp/configuration/index.ts @@ -1,7 +1,7 @@ import { v5 as uuidv5 } from "uuid"; -import type { BaseTool } from "../../agent/providers/base/types"; import { MCPClient } from "../client/index"; import type { AnyToolConfig, MCPServerConfig, ToolsetWithTools } from "../types"; +import type { Tool } from "../../tool"; // Store MCP configuration instances to prevent duplicates const mcpConfigurationInstances = new Map(); @@ -111,11 +111,11 @@ This can lead to memory leaks. To fix this: * Agent-ready tools include executable functions. * @returns A flat array of all agent-ready tools. */ - public async getTools(): Promise { + public async getTools(): Promise[]> { this.addToInstanceCache(); // Ensure instance is cached even if only getting tools // Create an array to hold all tools - const allTools: BaseTool[] = []; + const allTools: Tool[] = []; for (const [serverName, serverConfig] of Object.entries(this.serverConfigs) as [ TServerKeys, @@ -123,11 +123,12 @@ This can lead to memory leaks. To fix this: ][]) { try { const client = await this.getConnectedClient(serverName, serverConfig); - const agentTools = (await client.getAgentTools()) as Record; + const agentTools = await client.getAgentTools(); // Convert tools to BaseTool and add to array + // biome-ignore lint/complexity/noForEach: Object.values(agentTools).forEach((tool) => { - allTools.push(tool as BaseTool); + allTools.push(tool); }); } catch (error) { console.error(`Error fetching agent tools from server ${serverName}:`, error); @@ -172,7 +173,7 @@ This can lead to memory leaks. To fix this: try { const client = await this.getConnectedClient(serverName, serverConfig); // Get tools from client - const agentTools = (await client.getAgentTools()) as Record; + const agentTools = await client.getAgentTools(); // Add toolset if it contains any tools if (Object.keys(agentTools).length > 0) { @@ -180,7 +181,7 @@ This can lead to memory leaks. To fix this: const toolsetWithTools = { ...agentTools } as ToolsetWithTools; // Add the toTools method - toolsetWithTools.getTools = () => Object.values(agentTools) as BaseTool[]; + toolsetWithTools.getTools = () => Object.values(agentTools) as Tool[]; // Store in toolsets agentToolsets[serverName] = toolsetWithTools; diff --git a/packages/core/src/mcp/types.ts b/packages/core/src/mcp/types.ts index be54aba35..fa6321481 100644 --- a/packages/core/src/mcp/types.ts +++ b/packages/core/src/mcp/types.ts @@ -1,5 +1,5 @@ import type { ClientCapabilities } from "@modelcontextprotocol/sdk/types.js"; -import type { BaseTool } from "../agent/providers/base/types"; +import type { Tool } from "../tool"; /** * Client information for MCP @@ -220,10 +220,10 @@ export type ToolsetWithTools = Record & { /** * Converts the toolset to an array of BaseTool objects. */ - getTools: () => BaseTool[]; + getTools: () => Tool[]; }; /** * Any tool configuration */ -export type AnyToolConfig = Record; +export type AnyToolConfig = Tool; diff --git a/packages/core/src/retriever/tools/index.ts b/packages/core/src/retriever/tools/index.ts index b24e131a5..ac7bcff73 100644 --- a/packages/core/src/retriever/tools/index.ts +++ b/packages/core/src/retriever/tools/index.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { AgentTool } from "../../tool"; +import { createTool, type AgentTool } from "../../tool"; import type { Retriever } from "../types"; /** @@ -33,7 +33,7 @@ export const createRetrieverTool = ( options.description || "Searches for relevant information in the knowledge base based on the query."; - return { + return createTool({ name: toolName, description: toolDescription, parameters: z.object({ @@ -44,5 +44,5 @@ export const createRetrieverTool = ( return result; }, - }; + }); }; diff --git a/packages/core/src/tool/index.spec.ts b/packages/core/src/tool/index.spec.ts index eb5535e34..3498c05aa 100644 --- a/packages/core/src/tool/index.spec.ts +++ b/packages/core/src/tool/index.spec.ts @@ -75,7 +75,7 @@ describe("Tool", () => { execute: jest.fn(), } as any; - expect(() => new Tool(options)).toThrow("Tool parameters schema is required"); + expect(() => new Tool(options)).toThrow("Tool 'testTool' parameters schema is required"); }); it("should throw error if execute is missing", () => { @@ -85,7 +85,7 @@ describe("Tool", () => { description: "A test tool", } as any; - expect(() => new Tool(options)).toThrow("Tool execute function is required"); + expect(() => new Tool(options)).toThrow("Tool 'testTool' execute function is required"); }); }); }); diff --git a/packages/core/src/tool/index.ts b/packages/core/src/tool/index.ts index 71c8b31fa..32a44c63b 100644 --- a/packages/core/src/tool/index.ts +++ b/packages/core/src/tool/index.ts @@ -1,9 +1,11 @@ import { v4 as uuidv4 } from "uuid"; import type { BaseTool, ToolExecuteOptions, ToolSchema } from "../agent/providers/base/types"; -import { z } from "zod"; +import type { z } from "zod"; // Export ToolManager and related types export { ToolManager, ToolStatus, ToolStatusInfo } from "./manager"; +// Also export Toolkit +export type { Toolkit } from "./toolkit"; /** * Tool definition compatible with Vercel AI SDK @@ -37,13 +39,13 @@ export type ToolOptions = { /** * Function to execute when the tool is called */ - execute: (args: z.infer, options?: ToolExecuteOptions) => Promise; + execute: (args: z.infer, options?: ToolExecuteOptions) => Promise; }; /** * Tool class for defining tools that agents can use */ -export class Tool implements BaseTool> { +export class Tool /* implements BaseTool> */ { /** * Unique identifier for the tool */ @@ -67,7 +69,7 @@ export class Tool implements BaseTool, options?: ToolExecuteOptions) => Promise; + readonly execute: (args: z.infer, options?: ToolExecuteOptions) => Promise; /** * Create a new tool @@ -76,16 +78,19 @@ export class Tool implements BaseTool { let toolManager: ToolManager; // Create sample tools for testing - const mockTool1: AgentTool = { + const mockTool1 = createTool({ name: "tool1", description: "Test tool 1", parameters: z.object({ param1: z.string().describe("Parameter 1"), }), execute: jest.fn().mockResolvedValue("Tool 1 result"), - }; + }); - const mockTool2: AgentTool = { + const mockTool2 = createTool({ name: "tool2", description: "Test tool 2", parameters: z.object({ param2: z.number().describe("Parameter 2"), }), execute: jest.fn().mockResolvedValue("Tool 2 result"), - }; + }); beforeEach(() => { toolManager = new ToolManager(); @@ -56,14 +56,14 @@ describe("ToolManager", () => { it("should replace an existing tool with the same name", () => { toolManager.addTool(mockTool1); - const updatedTool: AgentTool = { + const updatedTool = createTool({ name: "tool1", description: "Updated test tool 1", parameters: z.object({ newParam: z.string().describe("New parameter"), }), execute: jest.fn().mockResolvedValue("Updated tool 1 result"), - }; + }); const result = toolManager.addTool(updatedTool); expect(result).toBe(true); // should return true when replacing @@ -85,9 +85,9 @@ describe("ToolManager", () => { }); }); - describe("addTools", () => { + describe("addItems", () => { it("should add multiple tools", () => { - toolManager.addTools([mockTool1, mockTool2]); + toolManager.addItems([mockTool1, mockTool2]); const tools = toolManager.getTools(); expect(tools.length).toBe(2); @@ -98,7 +98,7 @@ describe("ToolManager", () => { describe("removeTool", () => { it("should remove a tool by name", () => { - toolManager.addTools([mockTool1, mockTool2]); + toolManager.addItems([mockTool1, mockTool2]); const result = toolManager.removeTool("tool1"); expect(result).toBe(true); @@ -116,7 +116,7 @@ describe("ToolManager", () => { describe("prepareToolsForGeneration", () => { it("should return a copy of all tools", () => { - toolManager.addTools([mockTool1, mockTool2]); + toolManager.addItems([mockTool1, mockTool2]); const preparedTools = toolManager.prepareToolsForGeneration(); expect(preparedTools.length).toBe(2); @@ -141,7 +141,7 @@ describe("ToolManager", () => { describe("getToolsForApi", () => { it("should return simplified tool information for API", () => { - toolManager.addTools([mockTool1, mockTool2]); + toolManager.addItems([mockTool1, mockTool2]); const apiTools = toolManager.getToolsForApi(); expect(apiTools).toEqual([ diff --git a/packages/core/src/tool/manager/index.ts b/packages/core/src/tool/manager/index.ts index de0c961bf..74088a9cc 100644 --- a/packages/core/src/tool/manager/index.ts +++ b/packages/core/src/tool/manager/index.ts @@ -1,6 +1,7 @@ import type { BaseTool, ToolExecuteOptions } from "../../agent/providers/base/types"; import { zodSchemaToJsonUI } from "../../utils/toolParser"; -import type { AgentTool } from "../index"; +import { createTool, type AgentTool } from "../index"; +import type { Toolkit } from "../toolkit"; /** * Status of a tool at any given time @@ -22,129 +23,303 @@ export type ToolStatusInfo = { }; /** - * Manager class to handle all tool-related operations + * Type guard to check if an object is a Toolkit + */ +function isToolkit(item: AgentTool | Toolkit): item is Toolkit { + // Check for the 'tools' array property which is specific to Toolkit + return (item as Toolkit).tools !== undefined && Array.isArray((item as Toolkit).tools); +} + +/** + * Manager class to handle all tool-related operations, including Toolkits. */ export class ToolManager { /** - * Tools that this manager manages + * Standalone tools managed by this manager. */ private tools: BaseTool[] = []; + /** + * Toolkits managed by this manager. + */ + private toolkits: Toolkit[] = []; /** - * Creates a new ToolManager + * Creates a new ToolManager. + * Accepts both individual tools and toolkits. */ - constructor(tools: AgentTool[] = []) { - // Convert AgentTool[] to BaseTool[] and add them - this.addTools(tools); + constructor(items: (AgentTool | Toolkit)[] = []) { + this.addItems(items); } /** - * Get all tools managed by this manager + * Get all individual tools and tools within toolkits as a flattened list. */ getTools(): BaseTool[] { - return [...this.tools]; // Return a copy to prevent direct modification + const allTools = [...this.tools]; // Start with standalone tools + for (const toolkit of this.toolkits) { + // Add tools from the toolkit, converting them to BaseTool if necessary + // Assuming Toolkit.tools are AgentTool or compatible (like Tool) + allTools.push( + ...toolkit.tools.map( + (tool) => + ({ + name: tool.name, + description: tool.description || tool.name, + parameters: tool.parameters, + execute: tool.execute, + }) as BaseTool, + ), + ); // Explicit cast can help ensure compatibility + } + return allTools; + } + + /** + * Get all toolkits managed by this manager. + */ + getToolkits(): Toolkit[] { + return [...this.toolkits]; // Return a copy } /** - * Add a tool to the manager - * If a tool with the same name already exists, it will be replaced - * @returns true if the tool was successfully added or replaced + * Add an individual tool to the manager. + * If a standalone tool with the same name already exists, it will be replaced. + * A warning is issued if the name conflicts with a tool inside a toolkit, but the standalone tool is still added/replaced. + * @returns true if the tool was successfully added or replaced. */ addTool(tool: AgentTool): boolean { - if (!tool.execute) { + if (!tool || !tool.name) { + throw new Error("Cannot add an invalid or unnamed tool."); + } + if (!tool.execute || typeof tool.execute !== "function") { throw new Error(`Tool ${tool.name} must have an execute function`); } - const baseTool: BaseTool = { + // Check for conflict with tools *inside* toolkits and issue a warning + const conflictsWithToolkitTool = this.toolkits.some((toolkit) => + toolkit.tools.some((t) => t.name === tool.name), + ); + if (conflictsWithToolkitTool) { + console.warn( + `[ToolManager] Warning: Standalone tool name '${tool.name}' conflicts with a tool inside an existing toolkit.`, + ); + } + + // Convert AgentTool to BaseTool + const baseTool = createTool({ name: tool.name, description: tool.description || tool.name, parameters: tool.parameters, execute: tool.execute, - }; + }); - // Check if a tool with the same name already exists + // Check if tool exists in the standalone list and replace or add const existingIndex = this.tools.findIndex((t) => t.name === tool.name); - if (existingIndex !== -1) { // Replace the existing tool this.tools[existingIndex] = baseTool; + console.log(`[ToolManager] Replaced standalone tool: ${tool.name}`); } else { // Add the new tool this.tools.push(baseTool); + console.log(`[ToolManager] Added standalone tool: ${tool.name}`); + } + return true; // Always returns true on success (add or replace) + } + + /** + * Add a toolkit to the manager. + * If a toolkit with the same name already exists, it will be replaced. + * Also checks if any tool within the toolkit conflicts with existing standalone tools or tools in other toolkits. + * @returns true if the toolkit was successfully added or replaced. + */ + addToolkit(toolkit: Toolkit): boolean { + if (!toolkit || !toolkit.name) { + throw new Error("Toolkit must have a name."); + } + if (!toolkit.tools || !Array.isArray(toolkit.tools)) { + throw new Error(`Toolkit '${toolkit.name}' must have a 'tools' array.`); + } + + // Check for name conflicts with standalone tools or tools in *other* toolkits + for (const tool of toolkit.tools) { + if (!tool || !tool.name) { + throw new Error(`Toolkit '${toolkit.name}' contains an invalid or unnamed tool.`); + } + if (!tool.execute || typeof tool.execute !== "function") { + throw new Error( + `Tool '${tool.name}' in toolkit '${toolkit.name}' must have an execute function`, + ); + } + // Check conflict only against standalone tools and tools in OTHER toolkits + if ( + this.tools.some((t) => t.name === tool.name) || + this.toolkits + .filter((tk) => tk.name !== toolkit.name) + .some((tk) => tk.tools.some((t) => t.name === tool.name)) + ) { + console.warn( + `[ToolManager] Warning: Tool '${tool.name}' in toolkit '${toolkit.name}' conflicts with an existing tool. Toolkit not added/replaced.`, + ); + return false; + } } + const existingIndex = this.toolkits.findIndex((tk) => tk.name === toolkit.name); + if (existingIndex !== -1) { + // Before replacing, ensure no name conflicts are introduced by the *new* toolkit's tools + // (This check is already done above, but double-checking can be safer depending on logic complexity) + this.toolkits[existingIndex] = toolkit; + console.log(`[ToolManager] Replaced toolkit: ${toolkit.name}`); + } else { + this.toolkits.push(toolkit); + console.log(`[ToolManager] Added toolkit: ${toolkit.name}`); + } return true; } /** - * Add multiple tools to the manager - * If a tool with the same name already exists, it will be replaced + * Add multiple tools or toolkits to the manager. */ - addTools(tools: AgentTool[]): void { - for (const tool of tools) { - this.addTool(tool); + addItems(items: (AgentTool | Toolkit)[]): void { + if (!items) return; // Handle null or undefined input + for (const item of items) { + // Basic validation of item + if (!item || !("name" in item)) { + console.warn("[ToolManager] Skipping invalid item in addItems:", item); + continue; + } + + if (isToolkit(item)) { + // Ensure toolkit structure is valid before adding + if (item.tools && Array.isArray(item.tools)) { + this.addToolkit(item); + } else { + console.warn( + `[ToolManager] Skipping toolkit '${item.name}' due to missing or invalid 'tools' array.`, + ); + } + } else { + // Ensure tool structure is valid (has execute) + if (typeof item.execute === "function") { + this.addTool(item); + } else { + console.warn( + `[ToolManager] Skipping tool '${item.name}' due to missing or invalid 'execute' function.`, + ); + } + } } } /** - * Remove a tool by name - * @returns true if the tool was removed, false if it wasn't found + * Remove a standalone tool by name. Does not remove tools from toolkits. + * @returns true if the tool was removed, false if it wasn't found. */ removeTool(toolName: string): boolean { - const index = this.tools.findIndex((t) => t.name === toolName); - if (index === -1) return false; + const initialLength = this.tools.length; + this.tools = this.tools.filter((t) => t.name !== toolName); + const removed = this.tools.length < initialLength; + if (removed) { + console.log(`[ToolManager] Removed standalone tool: ${toolName}`); + } + return removed; + } - this.tools.splice(index, 1); - return true; + /** + * Remove a toolkit by name. + * @returns true if the toolkit was removed, false if it wasn't found. + */ + removeToolkit(toolkitName: string): boolean { + const initialLength = this.toolkits.length; + this.toolkits = this.toolkits.filter((tk) => tk.name !== toolkitName); + const removed = this.toolkits.length < initialLength; + if (removed) { + console.log(`[ToolManager] Removed toolkit: ${toolkitName}`); + } + return removed; } /** - * Prepare tools for text generation + * Prepare tools for text generation (includes tools from toolkits). */ prepareToolsForGeneration(dynamicTools?: BaseTool[]): BaseTool[] { - // Create a copy of tools to avoid modifying the original array - let toolsToUse = [...this.tools]; - - // Add dynamic tools if provided + let toolsToUse = this.getTools(); // Get the flattened list if (dynamicTools?.length) { - toolsToUse = [...toolsToUse, ...dynamicTools]; + // Filter valid dynamic tools before adding + const validDynamicTools = dynamicTools.filter( + (dt) => dt?.name && dt?.parameters && typeof dt?.execute === "function", // Apply optional chaining + ); + if (validDynamicTools.length !== dynamicTools.length) { + console.warn( + "[ToolManager] Some dynamic tools provided to prepareToolsForGeneration were invalid and ignored.", + ); + } + toolsToUse = [...toolsToUse, ...validDynamicTools]; } - return toolsToUse; } /** - * Get agent's tools for API exposure + * Get agent's tools (including those in toolkits) for API exposure. */ getToolsForApi() { - return this.tools.map((tool) => ({ + // Map the flattened list of tools for the API + return this.getTools().map((tool) => ({ name: tool.name, description: tool.description, - parameters: zodSchemaToJsonUI(tool.parameters), + // Use optional chaining for cleaner syntax + parameters: tool.parameters ? zodSchemaToJsonUI(tool.parameters) : undefined, })); } /** - * Has tool by name + * Check if a tool with the given name exists (either standalone or in a toolkit). */ hasTool(toolName: string): boolean { - return this.tools.some((tool) => tool.name === toolName); + if (!toolName) return false; + // Check standalone tools first + if (this.tools.some((tool) => tool.name === toolName)) { + return true; + } + // Check tools within toolkits + return this.toolkits.some((toolkit) => toolkit.tools.some((tool) => tool.name === toolName)); } /** - * Get tool by name + * Get a tool by name (searches standalone tools and tools within toolkits). * @param toolName The name of the tool to get - * @returns The tool or undefined if not found + * @returns The tool (as BaseTool) or undefined if not found */ getToolByName(toolName: string): BaseTool | undefined { - return this.tools.find((tool) => tool.name === toolName); + if (!toolName) return undefined; + // Find in standalone tools + const standaloneTool = this.tools.find((tool) => tool.name === toolName); + if (standaloneTool) { + return standaloneTool; + } + // Find in toolkits + for (const toolkit of this.toolkits) { + const toolInToolkit = toolkit.tools.find((tool) => tool.name === toolName); + if (toolInToolkit) { + // Convert AgentTool/Tool from toolkit to BaseTool format if needed + // (Assuming the structure is compatible or already BaseTool-like) + return { + name: toolInToolkit.name, + description: toolInToolkit.description || toolInToolkit.name, + parameters: toolInToolkit.parameters, + execute: toolInToolkit.execute, + } as BaseTool; + } + } + return undefined; // Not found } /** * Execute a tool by name * @param toolName The name of the tool to execute * @param args The arguments to pass to the tool - * @param signal Optional AbortSignal to abort the execution + * @param options Optional execution options like signal * @returns The result of the tool execution * @throws Error if the tool doesn't exist or fails to execute */ @@ -154,10 +329,20 @@ export class ToolManager { throw new Error(`Tool not found: ${toolName}`); } + // Ensure the execute function exists on the found object + if (typeof tool.execute !== "function") { + throw new Error(`Tool '${toolName}' found but has no executable function.`); + } + try { + // We assume the tool object retrieved by getToolByName has the correct execute signature return await tool.execute(args, options); } catch (error) { - throw new Error(`Failed to execute tool ${toolName}: ${error}`); + // Log the specific error for better debugging + console.error(`[ToolManager] Error executing tool '${toolName}':`, error); + // Re-throw a more informative error + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to execute tool ${toolName}: ${errorMessage}`); } } } diff --git a/packages/core/src/tool/reasoning/index.spec.ts b/packages/core/src/tool/reasoning/index.spec.ts new file mode 100644 index 000000000..a1b74354f --- /dev/null +++ b/packages/core/src/tool/reasoning/index.spec.ts @@ -0,0 +1,129 @@ +import { createReasoningTools, DEFAULT_INSTRUCTIONS, FEW_SHOT_EXAMPLES } from "./index"; +// No need to import from 'jest' directly; it's usually globally available + +// Mock the base tools using jest.mock +// Place mocks at the top level before imports if they mock modules used by the tested module itself, +// but here './index' doesn't directly use the mocked './tools' exports in its top-level code, +// so placing it before the describe block is fine. +jest.mock("./tools", () => ({ + thinkTool: { name: "think", description: "Think tool mock", parameters: {}, execute: jest.fn() }, + analyzeTool: { + name: "analyze", + description: "Analyze tool mock", + parameters: {}, + execute: jest.fn(), + }, +})); + +describe("createReasoningTools", () => { + // Optional: Clear mocks before each test if needed, though not strictly necessary here + // beforeEach(() => { + // jest.clearAllMocks(); + // }); + + it("should create a toolkit with default options", () => { + const toolkit = createReasoningTools(); + + expect(toolkit.name).toBe("reasoning_tools"); + expect(toolkit.tools).toHaveLength(2); + // Check if both tools are present (order doesn't matter) + expect(toolkit.tools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: "think" }), + expect.objectContaining({ name: "analyze" }), + ]), + ); + expect(toolkit.addInstructions).toBe(true); + expect(toolkit.instructions).toContain(DEFAULT_INSTRUCTIONS); + expect(toolkit.instructions).toContain(FEW_SHOT_EXAMPLES); + expect(toolkit.instructions?.startsWith("")).toBe(true); + expect(toolkit.instructions?.endsWith("")).toBe(true); + }); + + it("should create a toolkit without instructions", () => { + const toolkit = createReasoningTools({ addInstructions: false }); + + expect(toolkit.tools).toHaveLength(2); // Still includes tools by default + expect(toolkit.addInstructions).toBe(false); + expect(toolkit.instructions).toBeUndefined(); + }); + + it("should create a toolkit without the think tool", () => { + const toolkit = createReasoningTools({ think: false }); + + expect(toolkit.tools).toHaveLength(1); + expect(toolkit.tools).toEqual( + expect.arrayContaining([expect.objectContaining({ name: "analyze" })]), + ); + // Ensure 'think' tool is NOT present + expect(toolkit.tools).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: "think" })]), + ); + expect(toolkit.instructions).toBeDefined(); // Instructions are still added by default + }); + + it("should create a toolkit without the analyze tool", () => { + const toolkit = createReasoningTools({ analyze: false }); + + expect(toolkit.tools).toHaveLength(1); + expect(toolkit.tools).toEqual( + expect.arrayContaining([expect.objectContaining({ name: "think" })]), + ); + // Ensure 'analyze' tool is NOT present + expect(toolkit.tools).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: "analyze" })]), + ); + expect(toolkit.instructions).toBeDefined(); // Instructions are still added by default + }); + + it("should create a toolkit without few-shot examples", () => { + const toolkit = createReasoningTools({ addFewShot: false }); + + expect(toolkit.tools).toHaveLength(2); + expect(toolkit.addInstructions).toBe(true); + expect(toolkit.instructions).toBeDefined(); + expect(toolkit.instructions).toContain(DEFAULT_INSTRUCTIONS); + expect(toolkit.instructions).not.toContain(FEW_SHOT_EXAMPLES); + expect(toolkit.instructions?.startsWith("")).toBe(true); + expect(toolkit.instructions?.endsWith("")).toBe(true); + }); + + it("should create a toolkit with custom few-shot examples", () => { + const customExamples = "## Custom Example\n*Example content*"; + const toolkit = createReasoningTools({ fewShotExamples: customExamples }); + + expect(toolkit.tools).toHaveLength(2); + expect(toolkit.addInstructions).toBe(true); + expect(toolkit.instructions).toBeDefined(); + expect(toolkit.instructions).toContain(DEFAULT_INSTRUCTIONS); + expect(toolkit.instructions).toContain(customExamples); + expect(toolkit.instructions).not.toContain(FEW_SHOT_EXAMPLES); + expect(toolkit.instructions?.startsWith("")).toBe(true); + expect(toolkit.instructions?.endsWith("")).toBe(true); + }); + + it("should create an empty toolkit when all creation options are false", () => { + const toolkit = createReasoningTools({ + addInstructions: false, + think: false, + analyze: false, + // addFewShot: false, // This won't have an effect if addInstructions is false + }); + + expect(toolkit.name).toBe("reasoning_tools"); + expect(toolkit.tools).toHaveLength(0); + expect(toolkit.addInstructions).toBe(false); + expect(toolkit.instructions).toBeUndefined(); + }); + + it("should create toolkit without few-shot examples if addInstructions is false, even if addFewShot is true", () => { + const toolkit = createReasoningTools({ + addInstructions: false, + addFewShot: true, // Should be ignored because addInstructions is false + }); + + expect(toolkit.tools).toHaveLength(2); // Tools are still added + expect(toolkit.addInstructions).toBe(false); + expect(toolkit.instructions).toBeUndefined(); + }); +}); diff --git a/packages/core/src/tool/reasoning/index.ts b/packages/core/src/tool/reasoning/index.ts new file mode 100644 index 000000000..3ad6334fe --- /dev/null +++ b/packages/core/src/tool/reasoning/index.ts @@ -0,0 +1,182 @@ +import type { Tool } from ".."; +import { thinkTool as baseThinkTool, analyzeTool as baseAnalyzeTool } from "./tools"; +import type { Toolkit } from "../toolkit"; +import { createToolkit } from "../toolkit"; + +export * from "./types"; + +export const DEFAULT_INSTRUCTIONS = ` +You have access to the 'think' and 'analyze' tools to work through problems step-by-step and structure your thought process. You should ALWAYS 'think' before making tool calls or generating a response. + +1. **Think** (scratchpad): + * Purpose: Use the 'think' tool as a scratchpad to break down complex problems, outline steps, and decide on immediate actions within your reasoning flow. Use this to structure your internal monologue. + * Usage: Call 'think' multiple times if needed to break down the problem. Explain your reasoning and specify the intended action (e.g., "make a tool call", "perform calculation", "ask clarifying question"). + +2. **Analyze** (evaluation): + * Purpose: Evaluate the result of a think step or a set of tool calls. Assess if the result is expected, sufficient, or requires further investigation. + * Usage: Call 'analyze' AFTER a set of tool calls or a thought process. Determine the 'next_action' based on your analysis: 'continue' (more reasoning needed), 'validate' (seek external confirmation/validation if possible), or 'final_answer' (ready to conclude). + * Explain your reasoning highlighting whether the result is correct/sufficient. + +## IMPORTANT GUIDELINES +* **Always Think First:** You MUST use the 'think' tool before making other tool calls or generating a response, unless the request is extremely simple. Use 'think' multiple times to break down complex problems. +* **Iterate to Solve:** Use the 'think' and 'analyze' tools iteratively to build a clear reasoning path. The typical flow is Think -> [Think -> ...] -> [Tool Calls if needed] -> [Analyze if needed] -> ... -> final_answer. Repeat this cycle until you reach a satisfactory conclusion. +* **Make multiple tool calls in parallel:** After a 'think' step, you can make multiple tool calls in parallel if needed. +* **Keep Thoughts Internal:** The reasoning steps (thoughts and analyses) are for your internal process only. Do not share them directly with the user unless asked to explain your reasoning. +* **Conclude Clearly:** When your analysis determines the 'next_action' is 'final_answer', provide a concise and accurate final answer to the user based on your reasoning steps. +`; + +export const FEW_SHOT_EXAMPLES = ` +## Examples + +**Example 1: Simple Fact Retrieval** + +*User Request:* How many continents are there on Earth? + +*Agent's Internal Process:* +\`\`\`json +{ + "tool_call": { + "name": "think", + "arguments": { + "title": "Understand Request", + "thought": "The user wants to know the standard number of continents on Earth. This is a common piece of knowledge.", + "action": "Recall or verify the number of continents.", + "confidence": 0.95 + } + } +} +\`\`\` +*--(Agent internally recalls the fact)--* +\`\`\`json +{ + "tool_call": { + "name": "analyze", + "arguments": { + "title": "Evaluate Fact", + "result": "Standard geographical models list 7 continents: Africa, Antarctica, Asia, Australia, Europe, North America, South America.", + "analysis": "The recalled information directly answers the user's question accurately.", + "next_action": "final_answer", + "confidence": 1.0 + } + } +} +\`\`\` + +*Agent's Final Answer to User:* +There are 7 continents on Earth: Africa, Antarctica, Asia, Australia, Europe, North America, and South America. + +**Example 2: Multi-Step Information Gathering** + +*User Request:* What is the capital of France and its current population? + +*Agent's Internal Process:* +\`\`\`json +{ + "tool_call": { + "name": "think", + "arguments": { + "title": "Plan Information Retrieval", + "thought": "The user needs two pieces of information: the capital of France and its current population. I should break this down. First, find the capital.", + "action": "Search for the capital of France.", + "confidence": 0.95 + } + } +} +\`\`\` +*--(Tool call: search(query="capital of France"))--* +*--(Tool Result: "Paris")--* +\`\`\`json +{ + "tool_call": { + "name": "analyze", + "arguments": { + "title": "Analyze Capital Search Result", + "result": "The search result indicates Paris is the capital of France.", + "analysis": "This provides the first piece of requested information. Now I need to find the population of Paris.", + "next_action": "continue", + "confidence": 1.0 + } + } +} +\`\`\` +\`\`\`json +{ + "tool_call": { + "name": "think", + "arguments": { + "title": "Plan Population Retrieval", + "thought": "The next step is to find the current population of Paris.", + "action": "Search for the population of Paris.", + "confidence": 0.95 + } + } +} +\`\`\` +*--(Tool call: search(query="population of Paris current"))--* +*--(Tool Result: "Approximately 2.1 million (city proper, estimate for early 2024)")--* +\`\`\`json +{ + "tool_call": { + "name": "analyze", + "arguments": { + "title": "Analyze Population Search Result", + "result": "The search provided an estimated population figure for Paris.", + "analysis": "I now have both the capital and its estimated population. I can provide the final answer.", + "next_action": "final_answer", + "confidence": 0.9 + } + } +} +\`\`\` + +*Agent's Final Answer to User:* +The capital of France is Paris. Its estimated population (city proper) is approximately 2.1 million as of early 2024. +`; + +export type CreateReasoningToolsOptions = { + addInstructions?: boolean; + think?: boolean; + analyze?: boolean; + addFewShot?: boolean; + fewShotExamples?: string; +}; + +/** + * Factory function to create a Toolkit containing reasoning tools and instructions. + */ +export const createReasoningTools = (options: CreateReasoningToolsOptions = {}): Toolkit => { + const { + addInstructions = true, + think = true, + analyze = true, + addFewShot = true, + fewShotExamples, + } = options; + + const enabledTools: Tool[] = []; + let generatedInstructions: string | undefined = undefined; + + if (addInstructions) { + generatedInstructions = `\n${DEFAULT_INSTRUCTIONS}`; + if (addFewShot) { + generatedInstructions += `\n${fewShotExamples ?? FEW_SHOT_EXAMPLES}`; + } + generatedInstructions += "\n"; + } + + if (think) { + enabledTools.push({ ...baseThinkTool }); + } + if (analyze) { + enabledTools.push({ ...baseAnalyzeTool }); + } + + const reasoningToolkit = createToolkit({ + name: "reasoning_tools", + tools: enabledTools, + instructions: generatedInstructions, + addInstructions: addInstructions, + }); + + return reasoningToolkit; +}; diff --git a/packages/core/src/tool/reasoning/tools.ts b/packages/core/src/tool/reasoning/tools.ts new file mode 100644 index 000000000..b3c899453 --- /dev/null +++ b/packages/core/src/tool/reasoning/tools.ts @@ -0,0 +1,151 @@ +import { z } from "zod"; +import { v4 as uuidv4 } from "uuid"; +import { createTool } from ".."; +import type { ToolExecuteOptions } from "../../agent/providers/base/types"; +import { + NextAction, + type ReasoningStep, + ReasoningStepSchema, + type ReasoningToolExecuteOptions, +} from "./types"; + +const REASONING_INSTRUCTIONS = ` +You have access to the 'think' and 'analyze' tools to work through problems step-by-step and structure your thought process. You should ALWAYS 'think' before making tool calls or generating a response. + +1. **Think** (scratchpad): + * Purpose: Use the 'think' tool as a scratchpad to break down complex problems, outline steps, and decide on immediate actions within your reasoning flow. Use this to structure your internal monologue. + * Usage: Call 'think' BEFORE making other tool calls or generating a response. Explain your reasoning and specify the intended action (e.g., "make a tool call", "perform calculation", "ask clarifying question"). + +2. **Analyze** (evaluation): + * Purpose: Evaluate the result of a think step or a set of tool calls. Assess if the result is expected, sufficient, or requires further investigation. + * Usage: Call 'analyze' AFTER a set of tool calls or a thought process. Determine the 'next_action' based on your analysis: 'continue' (more reasoning needed), 'validate' (seek external confirmation/validation if possible), or 'final_answer' (ready to conclude). + * Explain your reasoning highlighting whether the result is correct/sufficient. + +## IMPORTANT GUIDELINES +* **Always Think First:** You should use the 'think' tool before making other tool calls or generating a response, unless the request is extremely simple. +* **Iterate to Solve:** Use the 'think' and 'analyze' tools iteratively to build a clear reasoning path. The typical flow is Think -> [Tool Calls if needed] -> [Analyze if needed] -> ... -> final_answer. Repeat this cycle until you reach a satisfactory conclusion. +* **Make multiple tool calls in parallel:** After a 'think' step, you can make multiple tool calls in parallel if needed. +* **Keep Thoughts Internal:** The reasoning steps (thoughts and analyses) are for your internal process only. Do not share them directly with the user unless asked to explain your reasoning. +* **Conclude Clearly:** When your analysis determines the 'next_action' is 'final_answer', provide a concise and accurate final answer to the user based on your reasoning steps. +`; + +// --- Think Tool --- + +const thinkParametersSchema = z.object({ + title: z.string().describe("A concise title for this thinking step"), + thought: z.string().describe("Your detailed thought or reasoning for this step"), + action: z + .string() + .optional() + .describe("Optional: What you plan to do next based on this thought"), + confidence: z + .number() + .min(0) + .max(1) + .optional() + .default(0.8) + .describe("Optional: How confident you are about this thought (0.0 to 1.0)"), +}); + +export const thinkTool = createTool({ + name: "think", + description: + "Use this tool as a scratchpad to reason about the task and work through it step-by-step. Helps break down problems and track reasoning. Use it BEFORE making other tool calls or generating the final response.", + parameters: thinkParametersSchema, + execute: async (args, options?: ToolExecuteOptions): Promise => { + const { title, thought, action, confidence } = args; + const reasoningOptions = options as ReasoningToolExecuteOptions | undefined; + const { agentId, historyEntryId } = reasoningOptions || {}; + + if (!agentId || !historyEntryId) { + console.error("Think tool requires agentId and historyEntryId in options."); + return "Error: Missing required agentId or historyEntryId in execution options."; + } + + const step: ReasoningStep = { + id: uuidv4(), + type: "thought", + title, + reasoning: thought, + action, + confidence, + timestamp: new Date().toISOString(), + agentId, + historyEntryId, + // result and next_action are not applicable for 'thought' + }; + + try { + ReasoningStepSchema.parse(step); + + return `Thought step "${title}" recorded successfully.`; + } catch (error) { + console.error("Error processing or emitting thought step:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + return `Error recording thought step: ${errorMessage}`; + } + }, +}); + +// --- Analyze Tool --- + +const analyzeParametersSchema = z.object({ + title: z.string().describe("A concise title for this analysis step"), + result: z + .string() + .describe("The outcome or result of the previous action/thought being analyzed"), + analysis: z.string().describe("Your analysis of the result"), + next_action: z + .nativeEnum(NextAction) + .describe( + `What to do next based on the analysis: "${NextAction.CONTINUE}", "${NextAction.VALIDATE}", or "${NextAction.FINAL_ANSWER}"`, + ), + confidence: z + .number() + .min(0) + .max(1) + .optional() + .default(0.8) + .describe("Optional: How confident you are in this analysis (0.0 to 1.0)"), +}); + +export const analyzeTool = createTool({ + name: "analyze", + description: + "Use this tool to analyze the results from a previous reasoning step or tool call and determine the next action.", + parameters: analyzeParametersSchema, + execute: async (args, options?: ToolExecuteOptions): Promise => { + const { title, result, analysis, next_action, confidence } = args; + const reasoningOptions = options as ReasoningToolExecuteOptions | undefined; + const { agentId, historyEntryId } = reasoningOptions || {}; + + if (!agentId || !historyEntryId) { + console.error("Analyze tool requires agentId and historyEntryId in options."); + return "Error: Missing required agentId or historyEntryId in execution options."; + } + + const step: ReasoningStep = { + id: uuidv4(), + type: "analysis", + title, + reasoning: analysis, + result, + next_action, // Already validated as NextAction enum by Zod + confidence, + timestamp: new Date().toISOString(), + agentId, + historyEntryId, + // action is not applicable for 'analysis' + }; + + try { + ReasoningStepSchema.parse(step); + + return `Analysis step "${title}" recorded successfully. Next action: ${next_action}.`; + } catch (error) { + console.error("Error processing or emitting analysis step:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + return `Error recording analysis step: ${errorMessage}`; + } + }, +}); diff --git a/packages/core/src/tool/reasoning/types.ts b/packages/core/src/tool/reasoning/types.ts new file mode 100644 index 000000000..ed8ac6ce7 --- /dev/null +++ b/packages/core/src/tool/reasoning/types.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; +import type { ToolExecuteOptions } from "../../agent/providers/base/types"; + +/** + * Enum defining the next action to take after a reasoning step. + */ +export enum NextAction { + CONTINUE = "continue", + VALIDATE = "validate", + FINAL_ANSWER = "final_answer", +} + +/** + * Zod schema for the ReasoningStep data structure. + */ +export const ReasoningStepSchema = z.object({ + id: z.string().uuid(), // Unique ID for the step + type: z.enum(["thought", "analysis"]), // Type of step + title: z.string(), // Concise title for the step + reasoning: z.string(), // The detailed thought or analysis + action: z.string().optional(), // The action planned based on the thought (for 'thought' type) + result: z.string().optional(), // The result being analyzed (for 'analysis' type) + next_action: z.nativeEnum(NextAction).optional(), // What to do next (for 'analysis' type) + confidence: z.number().min(0).max(1).optional().default(0.8), // Confidence level + timestamp: z.string().datetime(), // Timestamp of the step creation + historyEntryId: z.string(), // Link to the main history entry + agentId: z.string(), // ID of the agent performing the step +}); + +/** + * TypeScript type inferred from the ReasoningStepSchema. + */ +export type ReasoningStep = z.infer; + +/** + * Options specific to reasoning tool execution, extending base ToolExecuteOptions. + */ +export interface ReasoningToolExecuteOptions extends ToolExecuteOptions { + agentId: string; + historyEntryId: string; +} diff --git a/packages/core/src/tool/toolkit.ts b/packages/core/src/tool/toolkit.ts new file mode 100644 index 000000000..37f518102 --- /dev/null +++ b/packages/core/src/tool/toolkit.ts @@ -0,0 +1,62 @@ +import type { ToolSchema } from "../agent/providers/base/types"; +import type { Tool } from "./index"; + +/** + * Represents a collection of related tools with optional shared instructions. + */ +export type Toolkit = { + /** + * Unique identifier name for the toolkit. Used for management and potentially logging. + */ + name: string; + + /** + * A brief description of what the toolkit does or what tools it contains. + * Optional. + */ + description?: string; + + /** + * Shared instructions for the LLM on how to use the tools within this toolkit. + * These instructions are intended to be added to the system prompt if `addInstructions` is true. + * Optional. + */ + instructions?: string; + + /** + * Whether to automatically add the toolkit's `instructions` to the agent's system prompt. + * If true, the instructions from individual tools within this toolkit might be ignored + * by the Agent's system message generation logic to avoid redundancy. + * Defaults to false. + */ + addInstructions?: boolean; + + /** + * An array of Tool instances that belong to this toolkit. + */ + tools: Tool[]; +}; + +/** + * Helper function for creating a new toolkit. + * Provides default values and ensures the basic structure is met. + * + * @param options - The configuration options for the toolkit. + * @returns A Toolkit object. + */ +export const createToolkit = (options: Toolkit): Toolkit => { + if (!options.name) { + throw new Error("Toolkit name is required"); + } + if (!options.tools || options.tools.length === 0) { + console.warn(`Toolkit '${options.name}' created without any tools.`); + } + + return { + name: options.name, + description: options.description || "", // Default empty description + instructions: options.instructions, + addInstructions: options.addInstructions || false, // Default to false + tools: options.tools || [], // Default to empty array if not provided (though warned above) + }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 183b82290..41411685b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,6 +289,34 @@ importers: specifier: ^5.8.2 version: 5.8.2 + examples/with-thinking-tool: + dependencies: + '@ai-sdk/openai': + specifier: ^1.3.10 + version: 1.3.10(zod@3.24.2) + '@voltagent/cli': + specifier: ^0.1.2 + version: link:../../packages/cli + '@voltagent/core': + specifier: ^0.1.5 + version: link:../../packages/core + '@voltagent/vercel-ai': + specifier: ^0.1.2 + version: link:../../packages/vercel-ai + zod: + specifier: ^3.24.2 + version: 3.24.2 + devDependencies: + '@types/node': + specifier: ^22.13.5 + version: 22.14.0 + tsx: + specifier: ^4.19.3 + version: 4.19.3 + typescript: + specifier: ^5.8.2 + version: 5.8.2 + examples/with-tools: dependencies: '@ai-sdk/openai': diff --git a/website/docs/agents/overview.md b/website/docs/agents/overview.md index 73822e06f..26b2598ac 100644 --- a/website/docs/agents/overview.md +++ b/website/docs/agents/overview.md @@ -69,37 +69,46 @@ async function chat(input: string) { // Use streamText for interactive responses const response = await agent.streamText(input); - process.stdout.write("Assistant: "); - // Process the stream containing text deltas, tool calls, etc. - for await (const delta of response.stream) { - switch (delta.type) { - case "text-delta": - process.stdout.write(delta.textDelta); - break; - case "tool-call": - // Log when the agent decides to call a tool - console.log(`\n[Tool Call: ${delta.toolName} Args: ${JSON.stringify(delta.args)}]`); - break; - case "tool-result": - // Log the result after the tool executes - console.log(`\n[Tool Result: ${delta.toolName} Result: ${JSON.stringify(delta.result)}]`); - // The agent continues generating text after processing the tool result - process.stdout.write("Assistant (continuing): "); - break; - // Handle other delta types (error, finish, etc.) as needed - } + for await (const chunk of stream.textStream) { + console.log(chunk); } - console.log("\n--- Interaction Finished ---"); } // Example usage that might trigger the weather tool -// await chat("What's the weather like in London?"); +await chat("What's the weather like in London?"); // Example using generateText for a complete response -// const completeResponse = await agent.generateText("Explain machine learning briefly."); -// console.log("\nComplete Response:", completeResponse.text); +const completeResponse = await agent.generateText("Explain machine learning briefly."); +console.log("Complete Response:", completeResponse.text); +``` + +#### Markdown Formatting + +**Why?** To have the agent automatically format its text responses using Markdown for better readability and presentation. + +By setting the `markdown` property to `true` in the agent's configuration, you instruct the LLM to use Markdown syntax (like headings, lists, bold text, etc.) when generating text responses. VoltAgent adds a corresponding instruction to the system prompt automatically. + +```ts +import { Agent } from "@voltagent/core"; +import { VercelAIProvider } from "@voltagent/vercel-ai"; +import { openai } from "@ai-sdk/openai"; + +const agent = new Agent({ + name: "Markdown Assistant", + description: "A helpful assistant that formats answers clearly.", + llm: new VercelAIProvider(), + model: openai("gpt-4o"), + markdown: true, // Enable automatic Markdown formatting +}); + +// Now, when you call generateText or streamText, +// the agent will attempt to format its response using Markdown. +const response = await agent.generateText("Explain the steps to make a cup of tea."); +console.log(response.text); ``` +This is particularly useful when displaying agent responses in UIs that support Markdown rendering. + ### Structured Data Generation (`generateObject`/`streamObject`) Use these methods when you need the LLM to generate output conforming to a specific structure (defined by a Zod schema). This is ideal for data extraction, function calling based on schema, or generating predictable JSON. @@ -129,23 +138,24 @@ const personSchema = z.object({ }); // Example using generateObject -// const objectResponse = await agent.generateObject( -// "Create a profile for a talented software developer named Alex.", -// personSchema -// ); -// console.log("Complete object:", objectResponse.object); +const objectResponse = await agent.generateObject( + "Create a profile for a talented software developer named Alex.", + personSchema +); +console.log("Complete object:", objectResponse.object); // Example using streamObject -// console.log("\nStreaming object generation:"); -// const streamObjectResponse = await agent.streamObject( -// "Generate details for a data scientist named Jamie.", -// personSchema -// ); -// for await (const partial of streamObjectResponse.objectStream) { -// console.log("Received update:", partial); // Shows the object being built incrementally -// } -// const finalObject = await streamObjectResponse.object; -// console.log("Final streamed object:", finalObject); +const streamObjectResponse = await agent.streamObject( + "Generate details for a data scientist named Jamie.", + personSchema +); + +for await (const partial of streamObjectResponse.objectStream) { + console.log("Received update:", partial); // Shows the object being built incrementally +} + +const finalObject = await streamObjectResponse.object; +console.log("Final streamed object:", finalObject); ``` ## Advanced Features @@ -218,8 +228,9 @@ const agent = new Agent({ }); // Example: Call streamText and the agent might use the tool -// await agent.streamText("What's the weather in London?"); -// The agent should call the 'get_weather' tool during the stream. +const response = await agent.generateText("What's the weather in London?"); +console.log(response.text); +// The agent should call the 'get_weather' tool during the generation. ``` [Learn more about Tools](./tools.md) @@ -250,7 +261,8 @@ const mainAgent = new Agent({ }); // Example: Call streamText on the main agent -// await mainAgent.streamText("Write a blog post about quantum computing."); +const response = await mainAgent.generateText("Write a blog post about quantum computing."); +console.log(response.text); // The Coordinator might decide to use the delegate_task tool to involve researchAgent and writingAgent. ``` @@ -358,7 +370,8 @@ const agent = new Agent({ }); // Example: Ask a question using streamText -// const response = await agent.streamText("What are Retrievers in VoltAgent?"); +const response = await agent.generateText("What are Retrievers in VoltAgent?"); +console.log(response.text); // The agent will use SimpleRetriever *before* calling the LLM, // then generate an answer based on the retrieved context. ``` @@ -413,8 +426,11 @@ const xsaiAgent = new Agent({ }); // Use the agents (example) -// const response = await vercelOpenAIAgent.streamText("Hello OpenAI via Vercel!"); -// const response2 = await xsaiAgent.generateText("Hello XsAI!"); +const response = await vercelOpenAIAgent.generateText("Hello OpenAI via Vercel!"); +console.log(response.text); + +const response2 = await xsaiAgent.generateText("Hello XsAI!"); +console.log(response2.text); ``` [Learn more about Providers](../providers/overview.md) @@ -522,7 +538,8 @@ const agent = new Agent({ }); // Example: Call streamText -// await agent.streamText("Use the external analysis tool on this data..."); +const response = await agent.generateText("Use the external analysis tool on this data..."); +console.log(response.text); // The agent can now potentially call tools hosted on 'myModelServer'. ``` @@ -553,14 +570,14 @@ const openaiVoice = new OpenAIVoiceProvider({ }); // Text to Speech (TTS) -> Returns a Readable stream of audio data -// const audioStream = await openaiVoice.speak("Hello from OpenAI voice!"); +const audioStream = await openaiVoice.speak("Hello from OpenAI voice!"); // Example: Pipe the audio stream to a file -// await pipeline(audioStream, createWriteStream("openai_output.mp3")); +await pipeline(audioStream, createWriteStream("openai_output.mp3")); // Speech to Text (STT) -> Takes an audio source (e.g., Readable stream) -// const audioFileStream = createReadStream("input.mp3"); -// const transcript = await openaiVoice.listen(audioFileStream); -// console.log("OpenAI Transcript:", transcript); +const audioFileStream = createReadStream("input.mp3"); +const transcript = await openaiVoice.listen(audioFileStream); +console.log("OpenAI Transcript:", transcript); // Option 2: ElevenLabs Voice const elevenLabsVoice = new ElevenLabsVoiceProvider({ @@ -569,8 +586,8 @@ const elevenLabsVoice = new ElevenLabsVoiceProvider({ }); // TTS with ElevenLabs -// const elAudioStream = await elevenLabsVoice.speak("Hello from ElevenLabs!"); -// await pipeline(elAudioStream, createWriteStream("elevenlabs_output.mp3")); +const elAudioStream = await elevenLabsVoice.speak("Hello from ElevenLabs!"); +await pipeline(elAudioStream, createWriteStream("elevenlabs_output.mp3")); // --- Integrating Voice with an Agent --- @@ -592,8 +609,9 @@ if (agent.voice && textResponse.text) { // 3. Call the 'speak' method on the agent's voice provider instance. console.log("Generating voice output..."); const agentAudioStream = await agent.voice.speak(textResponse.text); + // Example: Save the agent's spoken response to a file - // await pipeline(agentAudioStream, createWriteStream("agent_story.mp3")); + await pipeline(agentAudioStream, createWriteStream("agent_story.mp3")); console.log("Generated voice output stream."); } else { console.log("Agent response:", textResponse.text); @@ -630,7 +648,7 @@ try { } // Note: If an error occurs *during* the stream, the loop might finish, // but the final history entry status will indicate an error. - console.log("\nInteraction finished processing stream."); + console.log("Interaction finished processing stream."); } catch (error) { // Catches errors from the initial await agent.streamText() call console.error("Agent interaction failed during setup:", error); @@ -654,14 +672,14 @@ To observe or react to these asynchronous errors, you can: ```ts // Example with streamText - // await agent.streamText("Another request", { - // provider: { - // onError: async (error) => { - // console.error("onError callback: Stream encountered an error:", error); - // // Implement specific error handling for this call - // } - // } - // }); + const response = await agent.streamText("Another request", { + provider: { + onError: async (error) => { + console.error("onError callback: Stream encountered an error:", error); + // Implement specific error handling for this call + }, + }, + }); ``` By combining `try...catch` for initial errors and using the per-call `onError` callback or checking history for stream errors, you can effectively manage issues during agent interactions. diff --git a/website/docs/tools/overview.md b/website/docs/tools/overview.md new file mode 100644 index 000000000..5096721c4 --- /dev/null +++ b/website/docs/tools/overview.md @@ -0,0 +1,119 @@ +--- +title: Overview +--- + +# Tools & Toolkits + +VoltAgent allows you to extend the capabilities of your AI agents by providing them with **Tools**. Tools enable agents to interact with external APIs, perform calculations, access databases, or execute virtually any custom code. This guide covers how to define and use individual tools and the new `Toolkit` concept for managing related tools. + +## Defining a Single Tool + +The most basic way to define a tool is using the `createTool` helper function (or instantiating the `Tool` class directly). + +A tool requires: + +- `name`: A unique name for the tool (used by the LLM to call it). +- `description`: A clear description of what the tool does (used by the LLM to decide when to use it). +- `parameters`: A Zod schema defining the input arguments the tool expects. +- `execute`: An asynchronous function that contains the tool's logic, taking the validated arguments as input. + +```typescript +import { Agent, createTool, z } from "@voltagent/core"; +import { VercelAIProvider } from "@voltagent/vercel-ai"; +import { openai } from "@ai-sdk/openai"; + +// Define a simple weather tool +const getWeatherTool = createTool({ + name: "get_weather", + description: "Fetches the current weather for a given location.", + parameters: z.object({ + location: z.string().describe("The city and state, e.g., San Francisco, CA"), + }), + execute: async ({ location }) => { + // In a real scenario, you would call a weather API here + console.log(`Fetching weather for ${location}...`); + if (location.toLowerCase().includes("tokyo")) { + return { temperature: "15°C", condition: "Cloudy" }; + } + return { temperature: "22°C", condition: "Sunny" }; + }, +}); + +const agent = new Agent({ + name: "WeatherAgent", + description: "An agent that can fetch weather information.", + llm: new VercelAIProvider(), + model: openai("gpt-4o-mini"), + tools: [getWeatherTool], // Add the tool to the agent +}); + +// Now the agent can use the 'get_weather' tool when asked about weather. +``` + +## Grouping Tools with Toolkits + +Often, several tools work together logically. For instance, tools for step-by-step reasoning (`think`, `analyze`) or a set of tools interacting with the same API. For these scenarios, VoltAgent provides **Toolkits**. + +A `Toolkit` allows you to: + +1. **Group related tools:** Keep your tool management organized. +2. **Define shared instructions:** Provide common guidance to the LLM on how to use all tools within the toolkit. +3. **Control instruction injection:** Decide if the toolkit's shared instructions should be automatically added to the agent's system prompt. + +### Defining a Toolkit + +A `Toolkit` is an object with the following structure: + +```typescript +import { createTool, createToolkit, type Tool, type Toolkit } from "@voltagent/core"; + +const myCalculatorToolkit = createToolkit({ + name: "calculator_toolkit", + description: "Tools for performing basic arithmetic operations.", + // Optional instructions for the LLM + instructions: `Use these tools for calculations. Always use 'add' for addition, 'subtract' for subtraction.`, + // Set to true to add the above instructions to the system prompt + addInstructions: true, + tools: [ + createTool({ + /* ... definition for 'add' tool ... */ + }), + createTool({ + /* ... definition for 'subtract' tool ... */ + }), + // ... other calculator tools + ], +}); +``` + +**Important:** With the introduction of Toolkits, individual `Tool` instances no longer have their own `instructions` or `addInstructions` properties. Instructions are managed at the Toolkit level. + +### Adding Tools and Toolkits to an Agent + +The `tools` option in the `Agent` constructor now accepts an array containing both individual `Tool` objects and `Toolkit` objects. The `ToolManager` handles both seamlessly. + +```typescript +import { Agent, createTool, createToolkit, type Toolkit } from "@voltagent/core"; +// ... import other tools and toolkits ... + +const agent = new Agent({ + name: "MultiToolAgent", + description: "An agent with various tools and toolkits.", + llm: /* ... */, + model: /* ... */, + tools: [ + getWeatherTool, // Add an individual tool + myCalculatorToolkit, // Add a toolkit + // ... other tools or toolkits + ], +}); +``` + +### Automatic Instructions + +When an agent is initialized, its `getSystemMessage` method checks all the `Toolkit`s provided in the `tools` array. If a `Toolkit` has `addInstructions: true` and defines an `instructions` string, those instructions will be automatically appended to the agent's base description, forming part of the final system prompt sent to the LLM. + +## Next Steps + +- See the [Reasoning Tools](/docs/tools/reasoning-tool/) documentation for a practical example of a pre-built toolkit. +- Explore creating your own tools to connect agents to your specific data sources and APIs. diff --git a/website/docs/tools/reasoning-tool.mdx b/website/docs/tools/reasoning-tool.mdx new file mode 100644 index 000000000..a33a4db92 --- /dev/null +++ b/website/docs/tools/reasoning-tool.mdx @@ -0,0 +1,191 @@ +import GitHubExampleLink from "@site/src/components/blog-widgets/GitHubExampleLink"; + +# Reasoning Tools (`think` & `analyze`) + +VoltAgent offers `think` and `analyze` tools, bundled via a `Toolkit` helper, to give agents step-by-step reasoning abilities. This helps agents break down problems, plan, analyze results internally, and structure their thought process before responding. + +## What are the `think` and `analyze` tools? + +Inspired by structured reasoning techniques, these tools allow the agent to perform an internal monologue ("stop and think") during complex tasks. Instead of attempting a direct response, the agent performs explicit reasoning steps: + +- **Break Down Complexity:** Deconstruct multi-step problems or unclear requests using `think`. +- **Plan Actions:** Decide the next steps, necessary tool calls, or required information using `think`. +- **Analyze Information:** Evaluate information gathered from other tools or previous steps using `analyze`. +- **Improve Reliability:** Verify intermediate steps and logic to reduce errors before finalizing the response. +- **Handle Complex Instructions:** Follow detailed guidelines or policies step-by-step using `think` and `analyze`. + +**When to Use:** + +Use the `reasoning_tools` toolkit when the agent needs to: + +1. **Perform Sequential Tool Calls:** Plan sequences (`think`) and evaluate intermediate results (`analyze`). +2. **Analyze Tool Outputs Carefully:** Process, verify, or synthesize results from tools before proceeding (`analyze`). +3. **Navigate Complex Rules/Policies:** Use `think` to understand rules and plan compliant actions, then `analyze` to check outcomes. +4. **Make Sequential Decisions:** Reduce errors in multi-step processes (e.g., complex bookings, calculations, data processing). +5. **Plan Complex Tasks:** Break down ambiguous problems and determine how to gather information incrementally (`think`). + +**When Less Necessary:** + +- Simple, single-step tasks that require only one tool call or a direct answer. +- Straightforward instructions without complex decisions or dependencies. + +## The `createReasoningTools` Helper + +Use the `@voltagent/core` helper `createReasoningTools` to easily add the reasoning tools to your agent: + +```typescript +import { createReasoningTools, type Toolkit } from "@voltagent/core"; + +// Basic usage - includes both tools and adds instructions/examples +const reasoningToolkit: Toolkit = createReasoningTools(); + +// Customized usage - e.g., only include 'think' and don't add instructions +const thinkOnlyToolkit: Toolkit = createReasoningTools({ + analyze: false, + addInstructions: false, +}); +``` + +This returns a `Toolkit` named `"reasoning_tools"`. Toolkits are a convenient way to bundle related tools and manage shared instructions. The `reasoning_tools` toolkit contains: + +1. **`think` Tool:** An internal scratchpad for the agent to plan, outline steps, and structure its thoughts. +2. **`analyze` Tool:** A tool for the agent to evaluate results (from `think` or other tools) and decide the next move (`continue`, `validate`, or `final_answer`). +3. **Detailed Instructions & Examples (Optional):** Explains how the agent should use the tools iteratively. These are added to the agent's system prompt if `addInstructions: true` (which is the default). + +### Options for `createReasoningTools` + +```typescript +type CreateReasoningToolsOptions = { + /** + * Add detailed instructions and few-shot examples to the agent's system prompt. + * @default true + */ + addInstructions?: boolean; + /** + * Include the 'think' tool in the toolkit. + * @default true + */ + think?: boolean; + /** + * Include the 'analyze' tool in the toolkit. + * @default true + */ + analyze?: boolean; + /** + * Include default few-shot examples along with instructions (if addInstructions is true). + * @default true + */ + addFewShot?: boolean; + /** + * Provide custom few-shot examples instead of the default ones. + * Ignored if addInstructions or addFewShot is false. + * @default Predefined examples (see code) + */ + fewShotExamples?: string; +}; +``` + +## Key Usage Instructions & Guidelines + +If `addInstructions` is `true` (default), the agent's system prompt is augmented with guidance. Key guidelines include: + +> - **Always Think First:** You MUST use the 'think' tool before making other tool calls or generating a response, unless the request is extremely simple. Use 'think' multiple times to break down complex problems. +> - **Iterate to Solve:** Use the 'think' and 'analyze' tools iteratively. The typical flow is `Think` -> [`Think` -> ...] -> [Tool Calls if needed] -> [`Analyze` if needed] -> ... -> `final_answer`. Repeat this cycle until you reach a satisfactory conclusion. +> - **Make multiple tool calls in parallel:** After a 'think' step planning multiple actions, you can make multiple tool calls in parallel if needed. +> - **Keep Thoughts Internal:** The reasoning steps (thoughts and analyses) are for your internal process only. Do not share them directly with the user unless asked to explain your reasoning. +> - **Conclude Clearly:** When your analysis determines the `next_action` is `final_answer`, provide a concise and accurate final answer to the user based on your reasoning steps. + +_(These instructions are accompanied by few-shot examples demonstrating the flow if `addFewShot` is `true`)_. + +## How the Agent Uses the Tools (Internal Flow) + +The power of these tools lies in how the agent uses them _internally_ before generating a response for the user. Here's a simplified example based on the default few-shot examples provided to the agent: + +**User Request:** What is the capital of France and its current population? + +**Agent's Internal Process (simplified):** + +1. **Initial Thought:** The agent calls `think` to break down the request. + - `think({ title: "Plan Information Retrieval", thought: "User needs two facts: capital and population. First, find the capital.", action: "Search for capital of France" })` +2. **(Optional) External Tool Call:** The agent might call a search tool: `search({ query: "capital of France" })`. Assume the result is "Paris". +3. **Analysis & Next Step:** The agent calls `analyze` to process the result. + - `analyze({ title: "Analyze Capital Result", result: "Search result: Paris", analysis: "Got the capital (Paris). Now need population.", next_action: "continue" })` +4. **Further Thought:** Since the `next_action` was `continue`, the agent calls `think` again. + - `think({ title: "Plan Population Retrieval", thought: "Next step: find population of Paris.", action: "Search for population of Paris" })` +5. **(Optional) External Tool Call:** Agent calls `search({ query: "population of Paris" })`. Assume result is "~2.1 million". +6. **Final Analysis:** Agent analyzes the second result. + - `analyze({ title: "Analyze Population Result", result: "Search result: ~2.1 million", analysis: "Have both capital and population. Ready to answer.", next_action: "final_answer" })` + +**Agent's Final Answer to User:** (Generated _after_ the internal reasoning) + +> The capital of France is Paris. Its estimated population is approximately 2.1 million. + +### Key Tool Parameters + +When the agent calls `think` or `analyze`, it uses parameters like: + +- `title`: A short label for the reasoning step (useful for tracing). +- `thought` (`think` only): The agent's detailed reasoning or plan. +- `action` (`think` only): The intended next step (e.g., call a specific tool, formulate the final answer). +- `result` (`analyze` only): The outcome being analyzed (e.g., output from another tool). +- `analysis` (`analyze` only): The agent's evaluation of the `result`. +- `next_action` (`analyze` only): The crucial decision: `continue` (more steps needed), `validate` (seek external confirmation), or `final_answer` (ready to respond to user). +- `confidence`: An optional score (0.0-1.0) indicating the agent's confidence in its thought or analysis. + +## Example Agent Setup + +![River Crossing Puzzle](https://cdn.voltagent.dev/docs/reasoning-demo.gif) + +```typescript +import { Agent, createReasoningTools, type Toolkit } from "@voltagent/core"; +import { VercelAIProvider } from "@voltagent/vercel-ai"; +import { openai } from "@ai-sdk/openai"; + +// Get toolkit, automatically adding instructions & examples to system prompt +const reasoningToolkit: Toolkit = createReasoningTools(); // Uses defaults + +const agent = new Agent({ + name: "ThinkingAgent", + description: ` + You are an AI assistant designed for complex problem-solving using structured reasoning. + You MUST use the 'think' and 'analyze' tools internally to break down problems, plan steps, + evaluate information, and decide on the best course of action before responding. + Always think step-by-step. + `, + // Agent description reinforced to use the tools + llm: new VercelAIProvider(), + model: openai("gpt-4o-mini"), // Ensure model supports tool use + tools: [ + reasoningToolkit, + // ... add other tools the agent can call after thinking/analyzing + // e.g., searchTool, databaseQueryTool, etc. + ], + markdown: true, +}); + +// Example invocation (River Crossing Puzzle) +const result = await agent.generateText(` + Three project managers (PMs) and three engineers (ENGs) need to cross a river. + The boat holds only two people. At no point, on either river bank, can the engineers + outnumber the project managers (otherwise, chaos ensues!). How can they all cross safely? +`); + +console.log(result); // The agent will perform think/analyze steps internally +``` + + + +Using these tools encourages a structured, internal thought process, leading to more reliable and explainable agent behavior, especially for complex tasks. + +## Best Practices + +1. **Leverage Defaults:** Start with `createReasoningTools()` which enables all features (`think`, `analyze`, instructions, examples). This provides the best initial guidance to the agent. +2. **Reinforce in Agent Description:** Briefly mention in the agent's `description` that it should use the `think` and `analyze` tools for structured reasoning, complementing the detailed instructions added to the system prompt. +3. **Provide Other Tools:** Reasoning tools are most effective when the agent can plan (`think`) to use _other_ tools (like search, database access, calculations) and then evaluate (`analyze`) their results. +4. **Use Capable Models:** Ensure the underlying LLM (`model`) is proficient at following instructions and using tools effectively (e.g., GPT-4, Claude 3). +5. **Custom Examples for Nuance:** If the default few-shot examples aren't sufficient for your specific domain or complex workflows, provide tailored examples using the `fewShotExamples` option. +6. **Monitor and Refine:** Observe the agent's internal reasoning steps (tool calls like `think` and `analyze`) using tools like the [VoltAgent Developer Console](/docs/observability/developer-console/) to identify areas where its logic could be improved. Refine the agent's `description` or provide custom `fewShotExamples` based on these observations. +7. **Start with Complex Cases:** Introduce reasoning tools for tasks where the agent struggles with multi-step logic, planning, or managing information from multiple sources. diff --git a/website/sidebars.ts b/website/sidebars.ts index 02811892d..f636c33d1 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -46,6 +46,11 @@ const sidebars: SidebarsConfig = { label: "Observability", items: ["observability/overview", "observability/developer-console"], }, + { + type: "category", + label: "Tools", + items: ["tools/overview", "tools/reasoning-tool"], + }, { type: "category", label: "Utils",