Skip to main content

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:

  1. Tool Definition: Schema describing the tool's name, description, and parameters
  2. Tool Handler: Async function that executes the tool logic
  3. 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