- README.md: Added gadget-tasks and @gadget/ai-toolbox to projects table, added Scheduled Tasks architecture section with diagram, updated monorepo structure, added gadget-tasks to dev server instructions, added doc link - docs/agent-toolbox.md: Updated to reflect @gadget/ai-toolbox extraction from @gadget/ai. Changed IAiEnvironment → GadgetToolboxEnvironment, AiTool → GadgetTool, updated import paths, config examples, and added Tool Categories section. Clarified gadget-tasks is NOT a consumer. - docs/gadget-tasks.md: New documentation covering architecture, per-task execution flow, configuration, startup/shutdown sequences, concurrency, work order tracking, heartbeat, error recovery, and getting started.
26 KiB
Agent Toolbox Documentation
Overview
The Agent Toolbox (@gadget/ai-toolbox) is a shared package providing AI agent tool implementations for the Gadget platform. It was extracted from @gadget/ai into its own package so that agent tools can be maintained independently of the AI provider abstraction layer.
Consumers: gadget-drone (the only current consumer). gadget-tasks does not use @gadget/ai-toolbox — it drives the gadget-code platform via REST API and Socket.IO as a headless IDE client, delegating all AI and tool execution to drones.
Design Philosophy
The toolbox system is built on these core principles:
- Provider Agnostic: Tools work with any AI provider (Ollama, OpenAI, etc.)
- Type Safety: Full TypeScript typing from definition to execution
- Extensibility: Easy to add new tools without modifying core infrastructure
- Security: Tools require explicit credentials configured in the environment
- Error Handling: Comprehensive error reporting with recovery hints
- Shared Implementation: One tool codebase used by all agent processes
Component Overview
┌─────────────────┐
│ AiToolbox │ ← Manages tool registration and lookup
│ - env │ ← Holds GadgetToolboxEnvironment with credentials
│ - tools │ ← Map of all registered tools
│ - modeSets │ ← Tools organized by ChatSessionMode
└────────┬────────┘
│
│ register()
▼
┌─────────────────┐
│ GadgetTool │ ← Abstract base class for all tools
│ - name │ ← Unique tool identifier
│ - category │ ← Tool category (search, file, etc.)
│ - definition │ ← JSON Schema for AI provider
│ - execute() │ ← Tool implementation
│ - toolbox │ ← Access to environment & other tools
└────────┬────────┘
│
│ extends
▼
┌─────────────────┐
│ GoogleSearch │ ← Example tool implementation
│ FileReadTool │ ← Read files from the workspace
│ ShellTool │ ← Execute shell commands
│ SubagentTool │ ← Spawn subagents for tasks
│ ... │
└─────────────────┘
Core Interfaces
GadgetToolboxEnvironment
The GadgetToolboxEnvironment interface carries configuration and credentials from the application layer (which has access to YAML configs) down to the @gadget/ai-toolbox package (which cannot read configs directly). It also carries workspace and project context that tools need.
export interface GadgetToolboxEnvironment {
NODE_ENV: string;
services?: {
google?: {
cse?: {
apiKey?: string;
engineId?: string;
};
};
};
workspace?: {
workspaceDir?: string;
projectDir?: string;
cacheDir?: string;
};
project?: IProject;
chatSessionMode?: ChatSessionMode;
}
Key Design Decisions:
- Services Object: Credentials are organized by service (google, etc.)
- Optional Everything: All fields are optional to support partial configurations
- Workspace Context: Tools that work with files need
workspaceDir,projectDir, andcacheDir - Project Context: Tools can access the current
IProjectandChatSessionMode - Extensible: Future services can be added without breaking changes
AiToolbox
The toolbox manages tool registration, organization, and retrieval:
export class AiToolbox {
constructor(env: GadgetToolboxEnvironment);
// Register a tool for use by agents
register(tool: IAiTool, modes?: string[]): void;
// Get a tool by name (for system tools)
getTool(name: string): IAiTool | undefined;
// Get all tools for a specific mode
getModeSet(mode: string): ToolSet | undefined;
// Get tool names for a specific mode
getToolNamesForMode(mode: string): string[];
// Update workspace context (called when project changes)
updateWorkspace(workspace: { workspaceDir?: string; projectDir?: string; cacheDir?: string }): void;
// Update project context (called when session mode changes)
updateProjectContext(project: IProject, mode: ChatSessionMode): void;
// Access environment credentials
get env(): GadgetToolboxEnvironment;
}
Registration Modes:
- System Tools (no modes): Called by the platform itself (e.g., auto-naming chat sessions)
- Agent Tools (with modes): Available to AI agents in specific ChatSessionModes (e.g., "code", "research", "debug")
GadgetTool
All tools extend this abstract base class:
export abstract class GadgetTool implements IAiTool {
protected _toolbox: AiToolbox;
constructor(toolbox: AiToolbox);
get toolbox(): AiToolbox;
// Unique identifier for the tool
abstract get name(): string;
// Category for organization (search, file, code, etc.)
abstract get category(): string;
// JSON Schema definition sent to AI provider
abstract get definition(): IToolDefinition;
// Execute the tool with provided arguments
abstract execute(args: IToolArguments, logger: IAiLogger): Promise<string>;
}
IToolDefinition
The tool definition follows the JSON Schema format expected by AI providers:
export interface IToolDefinition {
type: "function";
function: {
name: string;
description: string;
parameters: IToolArguments;
};
}
export interface IToolArguments {
[key: string]: unknown;
}
Tool Execution Flow
The tool execution flow demonstrates the abstraction layer between the common tool interface and provider-specific implementations:
┌─────────────────────────────────────────────────────────────┐
│ Application Layer (gadget-drone) │
│ - Reads YAML config │
│ - Constructs GadgetToolboxEnvironment │
│ - Creates AiToolbox │
│ - Registers tools │
└────────────────────┬────────────────────────────────────────┘
│
│ Pass tools to AI API
▼
┌─────────────────────────────────────────────────────────────┐
│ @gadget/ai - Provider Abstraction Layer │
│ - AiApi.chat() receives tools array │
│ - Transforms IToolDefinition → Provider format │
│ - Sends to AI provider (Ollama/OpenAI) │
│ - Receives tool_calls from response │
│ - Executes tools via executeToolCalls() │
│ - Feeds results back to AI for next iteration │
└────────────────────┬────────────────────────────────────────┘
│
│ Provider-specific SDK calls
▼
┌─────────────────────────────────────────────────────────────┐
│ AI Provider (Ollama / OpenAI) │
│ - Receives tool definitions in native format │
│ - Decides when to call tools based on conversation │
│ - Returns tool_calls in response │
└─────────────────────────────────────────────────────────────┘
Transformation Process
-
Tool Definition Transformation
Each provider has its own tool format. The
@gadget/aipackage transforms our commonIToolDefinitionto the provider's expected format:OpenAI:
const openaiTool: ChatCompletionTool = { type: tool.definition.type, function: { name: tool.definition.function.name, description: tool.definition.function.description, parameters: tool.definition.function.parameters, }, };Ollama:
const ollamaTool: Tool = { type: tool.definition.type, function: { name: tool.definition.function.name, description: tool.definition.function.description, parameters: tool.definition.function.parameters, }, }; -
Tool Call Execution
When the AI provider returns tool calls, they're executed by the base
AiApiclass:protected async executeToolCalls( toolCalls: IToolCall[], tools: AiTool[], ): Promise<IToolCallResult[]> { const results: IToolCallResult[] = []; for (const toolCall of toolCalls) { // Find the tool by name const tool = tools.find(t => t.name === toolCall.function.name); // Parse arguments and execute const args = JSON.parse(toolCall.function.arguments); const result = await tool.execute(args, this.log); results.push({ callId: toolCall.callId, functionName: toolCall.function.name, result, }); } return results; } -
Result Transformation
Tool results are converted back to the provider's message format and appended to the conversation:
OpenAI:
messages.push({ role: "tool", tool_call_id: result.callId, content: result.error || result.result, });Ollama:
messages.push({ role: "tool", content: result.error || result.result, tool_name: result.functionName, });
Iterative Execution
Both OpenAI and Ollama implementations support multiple rounds of tool calls:
- AI receives tools and conversation
- AI returns tool calls (or final response)
- Tools are executed, results collected
- Results appended to conversation as tool messages
- Loop back to step 1
- Return final response
This allows complex multi-step operations where the AI can:
- Call multiple tools in parallel
- Use results from one tool to inform the next
- Refine its approach based on tool output
Configuration
Environment Setup
Tools requiring credentials need them configured in your YAML config files.
gadget-drone.yaml
# Add to gadget-drone.yaml
google:
cse:
apiKey: "${GOOGLE_CSE_API_KEY}"
engineId: "${GOOGLE_CSE_ENGINE_ID}"
Note:
gadget-codeandgadget-tasksdo not need toolbox credentials. gadget-code builds prompts but doesn't execute tools directly. gadget-tasks is a headless IDE client that delegates all tool execution to drones via the platform.
Environment Variables
Set the actual values in your shell or deployment environment:
export GOOGLE_CSE_API_KEY="your-api-key-here"
export GOOGLE_CSE_ENGINE_ID="your-engine-id-here"
TypeScript Usage
In consumer applications, construct the environment and pass to the toolbox:
// gadget-drone/src/services/ai.ts
import { AiToolbox, GadgetToolboxEnvironment } from "@gadget/ai-toolbox";
import env from "../config/env.js";
const toolboxEnv: GadgetToolboxEnvironment = {
NODE_ENV: env.NODE_ENV,
services: {
google: {
cse: {
apiKey: env.google.cse.apiKey,
engineId: env.google.cse.engineId,
},
},
},
};
const toolbox = new AiToolbox(toolboxEnv);
// Register tools for specific chat session modes
toolbox.register(new GoogleSearchTool(toolbox), ["code", "research"]);
toolbox.register(new FileReadTool(toolbox), ["code", "plan", "build"]);
toolbox.register(new SubagentTool(toolbox), ["code"]);
Example Tool: Google Search
The GoogleSearchTool demonstrates a complete tool implementation.
Overview
Purpose: Perform Google Custom Search Engine queries to find relevant web content.
Category: search
Credentials Required:
- Google CSE API Key
- Google CSE Engine ID
Tool Definition
{
type: "function",
function: {
name: "search_google",
description: "Perform a Google search for relevant information on the web.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query string.",
},
num_results: {
type: "number",
description: "Number of search results to return (default: 10, max: 10).",
},
siteSearch: {
type: "string",
description: "Optional site to restrict the search to (e.g. github.com).",
},
dateRestrict: {
type: "string",
description: "Restrict results by date range (d1, d7, d30, d365).",
},
fileType: {
type: "string",
description: "Restrict results to file type (pdf, doc, xls, ppt).",
},
sort: {
type: "string",
enum: ["relevance", "date"],
description: "Sort order for results.",
},
start: {
type: "number",
description: "Starting index for pagination (default: 1).",
},
},
required: ["query"],
},
}
}
Usage Examples
Basic Search:
AI Agent: "Let me search for the latest TypeScript 5.0 features."
Tool Call: search_google({ query: "TypeScript 5.0 new features" })
Site-Restricted Search:
AI Agent: "I'll search the React documentation for useEffect best practices."
Tool Call: search_google({
query: "useEffect best practices",
siteSearch: "react.dev"
})
Date-Restricted Search:
AI Agent: "Let me find recent news about AI regulation from the past week."
Tool Call: search_google({
query: "AI regulation news",
dateRestrict: "d7",
sort: "date"
})
File Type Search:
AI Agent: "I'll search for Python style guide PDFs."
Tool Call: search_google({
query: "Python style guide",
fileType: "pdf"
})
Implementation Details
File: packages/ai-toolbox/src/network/search-google.ts
Key Methods:
execute(args, logger): Validates input, calls search, formats resultssearch(query, options): Makes the Google CSE API callparseCseError(error): Converts API errors to formatted tool errors
Error Handling:
The tool provides detailed error messages for common issues:
- 401 Unauthorized: Invalid API key
- 403 Forbidden: Engine ID or permission issues
- 429 Rate Limited: Quota exceeded with retry-after hint
- Network Errors: Connection failures
- Missing Parameters: Clear guidance on required fields
Result Formatting:
Search results are formatted as human-readable text:
Here are some relevant search results I found:
Title: TypeScript 5.0 Release Notes
Link: https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/
Source: devblogs.microsoft.com
Snippet: TypeScript 5.0 introduces decorators, const type parameters, and more...
Title: What's New in TypeScript 5.0
Link: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html
Snippet: A comprehensive guide to all the new features in TypeScript 5.0...
Error Handling
IToolError Interface
All tool errors follow a standardized format:
export interface IToolError {
code: ToolErrorCode;
message: string;
parameter?: string;
expected?: string;
example?: string;
recoveryHint?: string;
}
Error Codes
Available error codes in ToolErrorCode:
MISSING_PARAMETER- Required parameter not providedINVALID_PARAMETER- Parameter value is invalidNOT_FOUND- Resource not foundPERMISSION_DENIED- Insufficient permissionsOPERATION_FAILED- General operation failureOPERATION_NOT_ALLOWED- Operation not permittedVALIDATION_ERROR- Input validation failedRATE_LIMITED- Rate limit exceededLIMIT_EXCEEDED- Quota or limit exceededINVALID_CRON_SPEC- Invalid cron expressionINVALID_OPERATION- Operation not supportedTIMEOUT- Operation timed outINVALID_TOOL_ARGUMENTS- Tool arguments invalidSUBAGENT_FAILED- Subagent execution failedTOOL_EXECUTION_FAILED- General tool execution errorUNAUTHORIZED- Authentication requiredFORBIDDEN- Access deniedRATE_LIMIT_EXCEEDED- Rate limit exceededNETWORK_ERROR- Network connectivity issueSECURITY_VIOLATION- Security policy violation
formatError Function
The formatError function creates consistent error messages:
const error: IToolError = {
code: "MISSING_PARAMETER",
message: "The 'query' parameter is required.",
parameter: "query",
expected: "A non-empty string containing the search query.",
example: 'search_google(query: "latest AI news")',
recoveryHint:
"Provide a 'query' parameter with your search terms and try again.",
};
return formatError(error);
Output:
TOOL ERROR: MISSING_PARAMETER
The 'query' parameter is required.
PARAMETER: query
EXPECTED: A non-empty string containing the search query.
EXAMPLE: search_google(query: "latest AI news")
RECOVERY HINT: Provide a 'query' parameter with your search terms and try again.
Creating New Tools
Step 1: Create Tool Class
Extend GadgetTool and implement the required methods:
import { GadgetTool } from "@gadget/ai-toolbox";
import type { AiToolbox } from "@gadget/ai-toolbox";
import type { IToolArguments, IToolDefinition, IAiLogger } from "@gadget/ai";
import { formatError } from "@gadget/ai";
export class MyNewTool extends GadgetTool {
constructor(toolbox: AiToolbox) {
super(toolbox);
}
get name(): string {
return "my_tool_name";
}
get category(): string {
return "category_name"; // e.g., "search", "file", "code"
}
get definition(): IToolDefinition {
return {
type: "function",
function: {
name: this.name,
description: "Clear description of what the tool does.",
parameters: {
type: "object",
properties: {
param1: {
type: "string",
description: "Description of param1",
},
param2: {
type: "number",
description: "Description of param2",
},
},
required: ["param1"],
},
},
};
}
async execute(args: IToolArguments, logger: IAiLogger): Promise<string> {
// Validate parameters
if (!args.param1) {
return formatError({
code: "MISSING_PARAMETER",
message: "param1 is required",
parameter: "param1",
});
}
// Access credentials if needed
const apiKey = this.toolbox.env.services?.myService?.apiKey;
if (!apiKey) {
throw new Error("API key not configured in environment");
}
// Access workspace context
const projectDir = this.toolbox.env.workspace?.projectDir;
// Perform the operation
logger.debug("executing my tool", { args });
try {
const result = await this.doSomething(args.param1 as string);
return `Operation successful: ${result}`;
} catch (error) {
return formatError({
code: "OPERATION_FAILED",
message: (error as Error).message,
});
}
}
private async doSomething(param1: string): Promise<string> {
// Implementation here
return "result";
}
}
Step 2: Add Credentials (if needed)
Update GadgetToolboxEnvironment in packages/ai-toolbox/src/toolbox.ts:
export interface GadgetToolboxEnvironment {
NODE_ENV: string;
services?: {
google?: {
cse?: {
apiKey?: string;
engineId?: string;
};
};
myService?: {
apiKey?: string;
endpoint?: string;
};
};
workspace?: {
workspaceDir?: string;
projectDir?: string;
cacheDir?: string;
};
project?: IProject;
chatSessionMode?: ChatSessionMode;
}
Update config types in packages/config/src/types.ts and consumer config readers to populate the environment.
Step 3: Register the Tool
In gadget-drone startup code:
import { AiToolbox } from "@gadget/ai-toolbox";
import { MyNewTool } from "@gadget/ai-toolbox";
const toolbox = new AiToolbox(toolboxEnv);
// Register as system tool (no modes — called by the platform itself)
toolbox.register(new MyNewTool(toolbox));
// Or register for specific chat session modes
toolbox.register(new MyNewTool(toolbox), ["code", "debug"]);
Testing Tools
Unit Tests
Test tool validation and execution:
import { describe, it, expect } from "vitest";
import { GoogleSearchTool } from "../src/tools/search/google.js";
import { AiToolbox } from "../src/toolbox.js";
import { createEmptyEnvironment } from "../src/config/env.js";
describe("GoogleSearchTool", () => {
const env = createEmptyEnvironment();
const toolbox = new AiToolbox(env);
const tool = new GoogleSearchTool(toolbox);
it("should have correct name", () => {
expect(tool.name).toBe("search_google");
});
it("should have correct category", () => {
expect(tool.category).toBe("search");
});
it("should have query in required parameters", () => {
const params = tool.definition.function.parameters;
expect(params.required).toContain("query");
});
it("should return error for missing query", async () => {
const result = await tool.execute(
{},
{ debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
);
expect(result).toContain("TOOL ERROR: MISSING_PARAMETER");
expect(result).toContain("query");
});
});
Integration Tests
Test with actual credentials (use mock API):
import { describe, it, expect, vi } from "vitest";
import { GoogleSearchTool } from "../src/tools/search/google.js";
import { AiToolbox } from "../src/toolbox.js";
describe("GoogleSearchTool integration", () => {
it("should call Google CSE API with correct parameters", async () => {
const env: GadgetToolboxEnvironment = {
NODE_ENV: "test",
services: {
google: {
cse: {
apiKey: "test-key",
engineId: "test-engine",
},
},
},
};
const toolbox = new AiToolbox(env);
const tool = new GoogleSearchTool(toolbox);
// Mock the googleapis client
// ... mock setup ...
const result = await tool.execute(
{ query: "test query", num_results: 5 },
logger,
);
expect(result).toContain("search results");
// Verify API was called with correct params
});
});
Best Practices
1. Validate Early, Validate Often
Check all required parameters at the start of execute():
async execute(args: IToolArguments, logger: IAiLogger): Promise<string> {
if (!args.query || typeof args.query !== "string") {
return formatError({
code: "INVALID_PARAMETER",
message: "query must be a string",
parameter: "query",
expected: "A string containing the search query",
});
}
// ... rest of implementation
}
2. Use Structured Errors
Always use formatError() for consistent error reporting:
// ✅ Good
return formatError({
code: "NOT_FOUND",
message: "File not found",
parameter: "path",
recoveryHint: "Check that the file path exists and is accessible",
});
// ❌ Bad
throw new Error("File not found");
3. Log Appropriately
Use the logger for debugging and auditing:
logger.debug("starting tool execution", { args });
logger.info("tool completed successfully", { resultLength: result.length });
logger.warn("rate limit approaching", { remaining: 10 });
logger.error("tool failed", { error: error.message });
4. Handle Credentials Safely
Never log credentials or include them in error messages:
// ✅ Good
if (!apiKey) {
throw new Error("API key not configured");
}
// ❌ Bad - exposes credential
logger.debug("using API key", { apiKey });
5. Document Thoroughly
Include in your tool:
- Clear description in the definition
- Parameter descriptions with examples
- Error scenarios and recovery hints
- Usage examples in comments
Future Enhancements
Planned improvements to the toolbox system:
- Parallel Execution: Execute independent tool calls in parallel
- Caching: Cache tool results for repeated queries
- Rate Limiting: Built-in rate limiting per tool
- Tool Metadata: Version, author, usage statistics
- Result Streaming: Stream large results back to AI
Tool Categories
The @gadget/ai-toolbox package organizes tools into the following categories:
| Category | Directory | Tools | Description |
|---|---|---|---|
| Network | src/network/ |
GoogleSearchTool, FetchUrlTool, WebFetcherTool |
Web search and URL fetching |
| System | src/system/ |
FileReadTool, FileWriteTool, FileEditTool, FileListTool, GlobTool, GrepTool, ShellTool |
File system and shell operations |
| Plan | src/plan/ |
PlanFileReadTool, PlanFileWriteTool, PlanFileEditTool, PlanFileListTool |
Plan-mode file operations |
| Chat | src/chat/ |
SubagentTool |
Subagent spawning for multi-agent workflows |
| Project | src/project/ |
ListSkillsTool, ReadSkillTool |
Project skill access |
Related Documentation
- Configuration Guide - Setting up tool credentials
- Architecture Overview - System architecture
- AI API Reference -
@gadget/aipackage documentation - gadget-tasks Documentation - Scheduled task worker (does NOT use @gadget/ai-toolbox)