| name | tool-generation |
| description | Guide for creating and registering new LLM tools in the DEVS platform. Use this when asked to create a new tool, add tool capabilities, or extend the tool system. |
Tool Generation for DEVS
Tools are capabilities that LLM agents can invoke during conversations. They enable agents to search documents, execute code, interact with external services, and perform specialized operations. This guide covers the complete process of creating, registering, and testing new tools.
Architecture Overview
Tool Definition (types.ts) → Tool Handler (service.ts)
↓ ↓
ToolDefinition Handler Function
(JSON Schema) (async function)
↓ ↓
Tool Registration (executor.ts)
↓
KnowledgeToolRegistry.register()
↓
Available to all agents via chat.ts
Directory Structure
Tools are organized by feature domain:
src/lib/
├── tool-executor/
│ ├── types.ts # Core tool executor types
│ ├── executor.ts # Registry, executor, registration functions
│ └── index.ts # Public exports
├── knowledge-tools/ # Document search/read tools
│ ├── types.ts # Params, results, and KNOWLEDGE_TOOL_DEFINITIONS
│ └── service.ts # Handler implementations
├── math-tools/ # Calculation tools
│ ├── types.ts # MATH_TOOL_DEFINITIONS
│ └── service.ts # calculate() handler
├── code-tools/ # Code execution tools
│ ├── types.ts # CODE_TOOL_DEFINITIONS
│ └── service.ts # execute() handler
└── features/connectors/tools/ # External service tools
├── types.ts # CONNECTOR_TOOL_DEFINITIONS
└── service.ts # Gmail, Drive, etc. handlers
Step 1: Define Types
Create parameter and result interfaces in types.ts:
import type { ToolDefinition } from '@/lib/llm/types'
export interface MyToolParams {
requiredParam: string
optionalParam?: number
filter?: ('value1' | 'value2' | 'value3')[]
}
export interface MyToolResult {
success: boolean
error?: string
data: MyToolData | null
duration_ms: number
}
export interface MyToolData {
id: string
name: string
}
export type MyToolName = 'my_tool' | 'my_other_tool'
export const MY_TOOL_DEFINITIONS: Record<MyToolName, ToolDefinition> = {
my_tool: {
type: 'function',
function: {
name: 'my_tool',
description:
'Brief description of what the tool does. ' +
'Include usage context: when to use it, what it returns. ' +
'Mention any important limitations or requirements.',
parameters: {
type: 'object',
properties: {
requiredParam: {
type: 'string',
description: 'What this parameter does and expected values',
},
optionalParam: {
type: 'integer',
description: 'Optional description (default: 10)',
minimum: 1,
maximum: 100,
},
filter: {
type: 'array',
description: 'Filter results by these values',
items: {
type: 'string',
enum: ['value1', 'value2', 'value3'],
},
},
},
required: ['requiredParam'],
},
},
},
my_other_tool: {
},
}
Step 2: Implement Handler
Create handler functions in service.ts:
import type { MyToolParams, MyToolResult } from './types'
import { db } from '@/lib/db'
export async function myTool(params: MyToolParams): Promise<MyToolResult> {
const startTime = performance.now()
try {
if (!params.requiredParam) {
return {
success: false,
error: 'requiredParam is required',
data: null,
duration_ms: performance.now() - startTime,
}
}
const data = await performOperation(params)
return {
success: true,
data,
duration_ms: performance.now() - startTime,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
data: null,
duration_ms: performance.now() - startTime,
}
}
}
export { MY_TOOL_DEFINITIONS } from './types'
Step 3: Register Tools
Add registration in src/lib/tool-executor/executor.ts:
3a. Import Dependencies
import { myTool, MY_TOOL_DEFINITIONS } from '@/lib/my-tools/service'
import type { MyToolParams, MyToolResult } from '@/lib/my-tools/types'
3b. Create Registration Function
export function registerMyTools(): void {
defaultRegistry.register<MyToolParams, MyToolResult>(
MY_TOOL_DEFINITIONS.my_tool,
async (args, context) => {
if (context.abortSignal?.aborted) {
throw new Error('Aborted')
}
return myTool(args)
},
{
tags: ['my-category'],
estimatedDuration: 500,
requiresConfirmation: false,
},
)
}
export function areMyToolsRegistered(): boolean {
return defaultRegistry.has('my_tool')
}
export function unregisterMyTools(): void {
defaultRegistry.unregister('my_tool')
}
3c. Call Registration at App Init
In src/app/App.tsx or initialization code:
import { registerMyTools } from '@/lib/tool-executor'
useEffect(() => {
registerMyTools()
}, [])
Step 4: Make Tools Available to Agents
Universal Tools (Available to All Agents)
Add to getAgentToolDefinitions in src/lib/chat.ts:
import { MY_TOOL_DEFINITIONS } from '@/lib/my-tools'
function getAgentToolDefinitions(_agent: Agent): ToolDefinition[] {
return [
...Object.values(KNOWLEDGE_TOOL_DEFINITIONS),
...Object.values(MATH_TOOL_DEFINITIONS),
...Object.values(CODE_TOOL_DEFINITIONS),
...Object.values(MY_TOOL_DEFINITIONS),
]
}
Conditional Tools (Based on Agent Config)
For tools that should only be available to specific agents:
function getAgentToolDefinitions(agent: Agent): ToolDefinition[] {
const tools = [...Object.values(KNOWLEDGE_TOOL_DEFINITIONS)]
if (agent.tools?.some((t) => t.type === 'my-feature')) {
tools.push(...Object.values(MY_TOOL_DEFINITIONS))
}
return tools
}
Step 5: Write Tests (TDD Required)
Create test file at src/test/lib/my-tools/service.test.ts:
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { myTool } from '@/lib/my-tools/service'
describe('myTool', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('parameter validation', () => {
it('should fail when requiredParam is missing', async () => {
const result = await myTool({ requiredParam: '' })
expect(result.success).toBe(false)
expect(result.error).toContain('requiredParam')
})
})
describe('successful execution', () => {
it('should return data on success', async () => {
const result = await myTool({ requiredParam: 'test' })
expect(result.success).toBe(true)
expect(result.data).not.toBeNull()
expect(result.duration_ms).toBeGreaterThan(0)
})
})
describe('error handling', () => {
it('should catch and return errors gracefully', async () => {
vi.spyOn(db, 'getAll').mockRejectedValueOnce(new Error('DB error'))
const result = await myTool({ requiredParam: 'test' })
expect(result.success).toBe(false)
expect(result.error).toBe('DB error')
})
})
})
Tool Definition Best Practices
1. Clear Descriptions
description: 'Search the knowledge base for documents matching a query. ' +
'Use this to find relevant information before answering questions. ' +
'Returns ranked results with relevance scores and text snippets.'
description: 'Search for documents.'
2. Parameter Documentation
properties: {
query: {
type: 'string',
description:
'Search query - can be keywords, phrases, or natural language questions. ' +
'If the user query is not in English, include both original terms AND translations.',
},
max_results: {
type: 'integer',
description: 'Maximum number of results to return (default: 10)',
minimum: 1,
maximum: 50,
},
}
3. Enum Values for Constrained Choices
properties: {
file_type: {
type: 'string',
description: 'Filter by file type',
enum: ['document', 'image', 'text'],
},
}
4. Required vs Optional
parameters: {
type: 'object',
properties: { },
required: ['query'],
}
Result Formatting Best Practices
1. Consistent Structure
interface ToolResult {
success: boolean
error?: string
data: SomeType | null
duration_ms: number
}
2. Truncation for Large Results
interface ListResult {
items: Item[]
total_count: number
has_more: boolean
truncated: boolean
limit: number
offset: number
}
3. Error Types
interface ErrorResult {
success: false
error: string
error_type?:
| 'validation'
| 'not_found'
| 'permission'
| 'timeout'
| 'internal'
details?: Record<string, unknown>
}
Existing Tools Reference
| Tool Module | Tools | Purpose |
|---|
knowledge-tools | search_knowledge, read_document, list_documents, get_document_summary | Knowledge base operations |
math-tools | calculate | Safe mathematical expressions |
code-tools | execute | WASM-sandboxed JS execution |
connectors/tools | gmail_*, drive_*, calendar_*, notion_*, qonto_* | External service integration |
Tool Execution Flow
1. LLM decides to call tool with arguments
2. ToolCall extracted from response
3. defaultExecutor.execute(toolCall, context)
a. Find handler in registry
b. Parse and validate arguments
c. Execute handler with timeout
d. Format result as JSON string
4. Result added to conversation
5. Continue LLM conversation with tool result
Security Considerations
- Input Validation: Always validate and sanitize parameters
- Timeout Limits: Set appropriate
estimatedDuration
- Abort Signals: Respect
context.abortSignal
- No Side Effects: Tools should be idempotent when possible
- Confirmation: Set
requiresConfirmation: true for destructive operations
Checklist for New Tools