Skip to main content

In-Product Tools

In-product tools (also called external tools) allow your Distri agents to interact with your application's functionality. These tools execute in your frontend or backend environment, giving you full control over the implementation.

Overview

In-product tools are functions that your agent can call during execution. Unlike server-side tools, they run in your application code, allowing you to:

  • Integrate with your existing UI components
  • Access browser APIs and DOM
  • Call your backend services
  • Perform client-side operations

Defining a Tool

Tools are defined using the DistriFnTool type from @distri/core:

import type { DistriFnTool } from '@distri/core';

const myTool: DistriFnTool = {
name: 'tool_name',
description: 'What this tool does',
type: 'function',
parameters: {
type: 'object',
properties: {
param1: { type: 'string', description: 'Description of param1' },
param2: { type: 'number', description: 'Description of param2' },
},
required: ['param1'],
},
handler: async (input) => {
const result = await doSomething(input.param1, input.param2);
return `Tool executed: ${result}`;
},
};

Tool Handler Return Types

handler: async (input) => {
return 'Simple text response';
}

Example: Google Maps Tools

Here's a complete example of in-product tools for Google Maps:

import type { DistriFnTool } from '@distri/core';

interface GoogleMapsManagerRef {
setMapCenter: (opts: { latitude: number; longitude: number; zoom?: number }) => Promise<void>;
addMarker: (opts: { latitude: number; longitude: number; title: string; description?: string }) => Promise<void>;
}

export const createMapTools = (map: GoogleMapsManagerRef): DistriFnTool[] => [
{
name: 'set_map_center',
description: 'Center the map at a specific location',
type: 'function',
parameters: {
type: 'object',
properties: {
latitude: { type: 'number', description: 'Latitude coordinate' },
longitude: { type: 'number', description: 'Longitude coordinate' },
zoom: {
type: 'number',
minimum: 1,
maximum: 20,
default: 13,
description: 'Zoom level (1-20)',
},
},
required: ['latitude', 'longitude'],
},
handler: async ({ latitude, longitude, zoom }) => {
await map.setMapCenter({ latitude, longitude, zoom });
return `Map centered at ${latitude}, ${longitude}`;
},
},
{
name: 'add_marker',
description: 'Add a marker to the map',
type: 'function',
parameters: {
type: 'object',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' },
title: { type: 'string' },
description: { type: 'string' },
},
required: ['latitude', 'longitude', 'title'],
},
handler: async ({ latitude, longitude, title, description }) => {
await map.addMarker({ latitude, longitude, title, description });
return `Marker added: ${title}`;
},
},
];

Using Tools in React

Pass in-product tools to the Chat component via externalTools:

import { useRef, useEffect, useState } from 'react';
import { DistriProvider, Chat, useAgent } from '@distri/react';
import type { DistriAnyTool } from '@distri/react';
import { createMapTools } from './tools';

function App() {
return (
<DistriProvider config={{ clientId: 'YOUR_CLIENT_ID' }}>
<MapsContent />
</DistriProvider>
);
}

function MapsContent() {
const { agent } = useAgent({ agentIdOrDef: 'maps_agent' });
const [threadId] = useState(() => crypto.randomUUID());
const mapRef = useRef<GoogleMapsManagerRef>(null);
const [tools, setTools] = useState<DistriAnyTool[]>([]);

useEffect(() => {
if (mapRef.current) {
setTools(createMapTools(mapRef.current));
}
}, []);

if (!agent) return null;

return (
<>
<GoogleMapsComponent ref={mapRef} />
<Chat
agent={agent}
threadId={threadId}
externalTools={tools}
/>
</>
);
}

Agent Configuration

In your agent definition, specify that the agent uses external tools:

---
name = "maps_agent"
description = "Agent that uses Google Maps tools"

[tools]
external = ["*"] # Allow all external tools
# Or specify specific tools:
# external = ["set_map_center", "add_marker"]
---

Context-Aware Tools

Tools often need access to application state or configuration:

export function createBrowserTool(options?: {
threadId?: string;
apiBase?: string;
onComplete?: (response: any) => void;
}): DistriFnTool {
return {
name: 'browser_step',
type: 'function',
description: 'Execute browser commands',
parameters: {
type: 'object',
properties: {
command: {
type: 'string',
enum: ['navigate', 'click', 'type', 'scroll'],
description: 'The browser command to execute',
},
data: {
type: 'object',
description: 'Command-specific data',
},
},
required: ['command'],
},
handler: async (input) => {
try {
const response = await fetch(`${options?.apiBase}/browser_step`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: input.command,
data: input.data,
thread_id: options?.threadId,
}),
});

const result = await response.json();

if (options?.onComplete) {
options.onComplete(result);
}

return [{
part_type: 'data',
data: {
success: result.success,
summary: result.summary,
},
}];
} catch (error) {
return [{
part_type: 'data',
data: {
success: false,
error: error instanceof Error ? error.message : String(error),
},
}];
}
},
};
}

Tool Factory Pattern

Create tool factories that generate tools with shared configuration:

import type { DistriAnyTool } from '@distri/react';

export const createAppTools = (options?: {
threadId?: string;
apiBase?: string;
onUpdate?: (data: any) => void;
}): DistriAnyTool[] => {
return [
createMapTool(options),
createSearchTool(options),
createDataTool(options),
];
};

Best Practices

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',
};
}
}

Minimal Responses

Return concise responses to avoid context bloat:

// Good: Minimal response
handler: async (input) => {
const data = await fetchDetailedData(input);
await storeInSession(threadId, data); // Store detailed data separately
return { success: true, summary: `Processed ${data.length} items` };
}

// Avoid: Large response
handler: async (input) => {
const data = await fetchDetailedData(input);
return data; // Too large, fills up context
}

Type Safety

Use TypeScript interfaces for tool inputs:

interface SearchInput {
query: string;
limit?: number;
}

const tool: DistriFnTool = {
name: 'search',
// ...
handler: async (input: SearchInput) => {
// Type-safe access
const results = await search(input.query, input.limit ?? 10);
return results;
},
};

Clear Descriptions

Provide detailed descriptions so the agent understands when to use each tool:

{
name: 'set_map_center',
description: 'Center the map view at specific latitude/longitude coordinates. Use this when the user asks to show a location, navigate to a place, or focus the map on specific coordinates.',
// ...
}

Complete Tool Interface

The full DistriFnTool interface:

interface DistriFnTool {
type: 'function';
name: string;
description: string;
parameters: {
type: 'object';
properties: Record<string, {
type: string;
description?: string;
enum?: string[];
items?: object;
}>;
required?: string[];
};
handler: (input: any) => Promise<
string | number | boolean | null | DistriPart[] | object
>;
is_final?: boolean; // If true, terminates agent execution after this tool
autoExecute?: boolean; // If true, executes without user confirmation
isExternal?: boolean; // Marks as client-side tool (set automatically)
}

UI Tools

For tools that render custom UI components:

interface DistriUiTool {
type: 'ui';
name: string;
description: string;
parameters: object;
component: (props: {
toolCall: ToolCall;
completeTool: (result: ToolResult) => void;
}) => React.ReactNode;
}

Handler Return Types

Tool handlers can return different types:

Return TypeDescriptionExample
stringSimple text responsereturn 'Done!'
numberNumeric resultreturn 42
booleanTrue/false resultreturn true
nullEmpty responsereturn null
objectStructured datareturn { status: 'ok', count: 5 }
DistriPart[]Rich multi-part responsereturn [{ part_type: 'text', data: '...' }]

Real-World Examples

Browser Automation Tool (from Browsr)

import type { DistriFnTool } from '@distri/core';

function createBrowserStepTool(sessionId: string): DistriFnTool {
return {
name: 'browser_step',
type: 'function',
description: 'Execute browser commands to interact with web pages.',
parameters: {
type: 'object',
properties: {
commands: {
type: 'array',
description: 'Browser commands to execute',
items: {
type: 'object',
properties: {
command: {
type: 'string',
enum: ['navigate_to', 'click', 'type_text', 'scroll_to', 'extract_structured_content'],
},
data: { type: 'object' },
},
required: ['command'],
},
},
},
},
handler: async (input) => {
const response = await fetch(`/browser_step`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
...input,
}),
});
const result = await response.json();
return result.summary ?? 'Step completed';
},
};
}

Lesson Context Tool (from The Write Place)

import type { DistriFnTool } from '@distri/core';

const getLessonStateTool: DistriFnTool = {
name: 'get_lesson_state',
type: 'function',
description: 'Get the current lesson state including questions and student answers.',
parameters: {
type: 'object',
properties: {
include_evaluations: {
type: 'boolean',
description: 'Whether to include previous evaluation results',
},
},
},
handler: async (input) => {
const state = getCurrentLessonState();
return {
current_page: state.currentPage,
total_pages: state.totalPages,
questions: state.questions,
answers: state.answers,
evaluations: input.include_evaluations ? state.evaluations : undefined,
};
},
};

Tool Factory Pattern

When multiple tools share configuration:

function createProductTools(apiBase: string, authToken: string) {
const headers = {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json',
};

const searchTool: DistriFnTool = {
name: 'search_products',
type: 'function',
description: 'Search products by name or category',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
category: { type: 'string', description: 'Product category' },
},
required: ['query'],
},
handler: async (input) => {
const res = await fetch(`${apiBase}/products/search`, {
method: 'POST', headers,
body: JSON.stringify(input),
});
return res.json();
},
};

const updateTool: DistriFnTool = {
name: 'update_product',
type: 'function',
description: 'Update a product field',
parameters: {
type: 'object',
properties: {
product_id: { type: 'string' },
field: { type: 'string' },
value: { type: 'string' },
},
required: ['product_id', 'field', 'value'],
},
handler: async (input) => {
const res = await fetch(`${apiBase}/products/${input.product_id}`, {
method: 'PATCH', headers,
body: JSON.stringify({ [input.field]: input.value }),
});
return res.ok ? 'Updated successfully' : 'Update failed';
},
};

return [searchTool, updateTool];
}