Implementing External Tools in Your Project
This guide shows you how to implement external tools in your project, following patterns from real-world implementations like the browsr project.
Overview
External tools allow your Distri agents to interact with your application's functionality. They can:
- Manipulate UI components
- Call backend APIs
- Access browser APIs
- Perform client-side operations
Tool Structure
A tool consists of:
- Tool Definition: Schema describing the tool's name, description, and parameters
- Tool Handler: Async function that executes the tool logic
- Tool Registration: Adding the tool to your agent's available tools
Basic Tool Implementation
1. Define the Tool Schema
import type { DistriFnTool } from '@distri/core';
export interface MyToolInput {
param1: string;
param2?: number;
}
export const createMyTool = (): DistriFnTool => {
return {
name: 'my_tool',
type: 'function',
description: 'Description of what this tool does',
parameters: {
type: 'object',
properties: {
param1: {
type: 'string',
description: 'Description of param1',
},
param2: {
type: 'number',
description: 'Description of param2',
default: 0,
},
},
required: ['param1'],
},
handler: async (input: MyToolInput) => {
// Tool implementation
const result = await performOperation(input.param1, input.param2);
return `Operation completed: ${result}`;
},
};
};
Advanced Pattern: Context-Aware Tools
Tools often need access to application state or component references:
import type { DistriFnTool, DistriPart } from '@distri/core';
export interface BrowserStepInput {
command: string;
data?: Record<string, any>;
session_id?: string;
}
export interface BrowserStepResponse {
success: boolean;
session_id: string;
summary?: string;
error?: string;
}
export function createBrowserStepTool(options?: {
threadId?: string;
browserSessionId?: string;
onStepComplete?: (response: BrowserStepResponse) => void;
apiBase?: string;
}): DistriFnTool {
let currentBrowserSessionId = options?.browserSessionId;
return {
name: 'browser_step',
type: 'function',
description: 'Execute browser commands to interact with web pages',
parameters: {
type: 'object',
properties: {
command: {
type: 'string',
enum: ['navigate', 'click', 'type', 'scroll', 'screenshot'],
description: 'The browser command to execute',
},
data: {
type: 'object',
description: 'Command-specific data',
},
session_id: {
type: 'string',
description: 'Browser session ID to use',
},
},
required: ['command'],
},
isExternal: true,
autoExecute: true,
handler: async (input: BrowserStepInput): Promise<DistriPart[]> => {
try {
const apiBase = options?.apiBase || 'http://localhost:8082';
const threadId = options?.threadId;
// Call backend API
const response = await fetch(`${apiBase}/browser_step`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: input.command,
data: input.data,
session_id: currentBrowserSessionId || input.session_id,
thread_id: threadId,
}),
});
// Update session ID from response
const sessionId = response.headers.get('browser-session-id');
if (sessionId) {
currentBrowserSessionId = sessionId;
}
if (!response.ok) {
const errorText = await response.text();
return [{
part_type: 'data',
data: {
success: false,
error: `Browser step failed: ${errorText}`,
session_id: currentBrowserSessionId,
},
}];
}
const result = await response.json() as BrowserStepResponse;
// Callback for UI updates
if (options?.onStepComplete) {
options.onStepComplete(result);
}
// Return minimal response (detailed data stored separately)
return [{
part_type: 'data',
data: {
success: result.success,
session_id: result.session_id,
summary: result.summary,
error: result.error,
},
}];
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return [{
part_type: 'data',
data: {
success: false,
error: `Browser step failed: ${errorMessage}`,
},
}];
}
},
};
}
Tool Factory Pattern
Create tool factories that generate tools with shared configuration:
import type { DistriAnyTool } from '@distri/react';
import type { DistriFnTool } from '@distri/core';
export const createDesignerTools = (options?: {
threadId?: string;
browserSessionId?: string;
onBrowserStep?: (response: BrowserStepResponse) => void;
apiBase?: string;
}): DistriAnyTool[] => {
return [
createBrowserStepTool({
threadId: options?.threadId,
browserSessionId: options?.browserSessionId,
onStepComplete: options?.onBrowserStep,
apiBase: options?.apiBase,
}),
// Add more tools here
];
};
Using Tools in React
With DistriProvider
import { DistriProvider, Chat, useAgent } from '@distri/react';
import { createDesignerTools } from './tools';
function App() {
const config = {
baseUrl: import.meta.env.VITE_DISTRI_API_URL,
};
return (
<DistriProvider config={config}>
<DesignerContent />
</DistriProvider>
);
}
function DesignerContent() {
const { agent } = useAgent({ agentIdOrDef: 'designer_agent' });
const [threadId] = useState(() => crypto.randomUUID());
const [tools, setTools] = useState<DistriAnyTool[]>([]);
useEffect(() => {
// Initialize tools when component mounts
setTools(createDesignerTools({
threadId,
apiBase: import.meta.env.VITE_API_URL,
onBrowserStep: (response) => {
console.log('Browser step completed:', response);
// Update UI based on response
},
}));
}, [threadId]);
if (!agent) return null;
return (
<Chat
agent={agent}
threadId={threadId}
externalTools={tools}
/>
);
}
Tool with Component References
Tools can interact with React component refs:
import { useRef, useEffect, useState } from 'react';
import type { DistriFnTool } from '@distri/core';
import type { DistriAnyTool } from '@distri/react';
interface MapManagerRef {
setCenter: (lat: number, lng: number) => Promise<void>;
addMarker: (lat: number, lng: number, title: string) => Promise<void>;
}
export const createMapTools = (map: MapManagerRef): DistriFnTool[] => [
{
name: 'set_map_center',
description: 'Center the map at coordinates',
type: 'function',
parameters: {
type: 'object',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' },
},
required: ['latitude', 'longitude'],
},
handler: async ({ latitude, longitude }) => {
await map.setCenter(latitude, longitude);
return `Map centered at ${latitude}, ${longitude}`;
},
},
];
function MapContent() {
const mapRef = useRef<MapManagerRef>(null);
const [tools, setTools] = useState<DistriAnyTool[]>([]);
useEffect(() => {
if (mapRef.current) {
setTools(createMapTools(mapRef.current));
}
}, []);
return (
<>
<MapComponent ref={mapRef} />
<Chat externalTools={tools} />
</>
);
}
Best Practices
1. Error Handling
Always handle errors gracefully:
handler: async (input) => {
try {
const result = await performOperation(input);
return { success: true, result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
2. Minimal Responses
Return concise responses to avoid context bloat. Store detailed data separately:
// Good: Minimal response
handler: async (input) => {
const detailedData = await fetchDetailedData(input);
// Store detailed data in session or separate storage
await storeInSession(threadId, detailedData);
// Return only summary
return {
success: true,
summary: `Processed ${detailedData.length} items`,
};
}
// Avoid: Large response
handler: async (input) => {
const detailedData = await fetchDetailedData(input);
return detailedData; // Too large!
}
3. Type Safety
Use TypeScript interfaces for tool inputs:
interface ToolInput {
param1: string;
param2?: number;
}
const tool: DistriFnTool = {
// ...
handler: async (input: ToolInput) => {
// Type-safe access to input
},
};
4. Session Management
For tools that maintain state across calls:
export function createStatefulTool(options?: {
threadId?: string;
}): DistriFnTool {
const state = new Map<string, any>();
return {
name: 'stateful_tool',
// ...
handler: async (input) => {
const key = options?.threadId || 'default';
const currentState = state.get(key) || {};
// Update state
state.set(key, { ...currentState, ...input });
return { success: true };
},
};
}
5. Tool Composition
Combine multiple related tools:
export const createToolSuite = (config: ToolConfig): DistriAnyTool[] => {
return [
createTool1(config),
createTool2(config),
createTool3(config),
];
};
Testing Tools
Test your tools independently:
import { createMyTool } from './tools';
describe('MyTool', () => {
it('should execute correctly', async () => {
const tool = createMyTool();
const result = await tool.handler({ param1: 'test' });
expect(result).toBe('Operation completed: test');
});
it('should handle errors', async () => {
const tool = createMyTool();
const result = await tool.handler({ param1: 'invalid' });
expect(result).toContain('error');
});
});
References
- Working with External Tools - Overview of external tools
- Using distri-client - Programmatic API
- browsr Implementation - Real-world example