Remote tools

In shortExecute client-side tools from the agent graph, handling geolocation, clipboard, and other browser capabilities.

  • 7 min read
  • 10 sections
  • Updated
  • v0.10.0
  • Markdown

Remote tools let your agent call functions that run in the browser or on the client machine. Use them for capabilities that only the client owns: geolocation, clipboard access, DOM manipulation, local file system access, or device sensors. Keep database, secrets, and backend work in server-side tools defined in your Python graph.

When the agent asks for a remote tool, 10xGraph automatically detects it, runs your registered handler on the client, collects the result, and feeds it back to the agent, all without you managing the handoff.

How it works

The flow is transparent. When you invoke or stream the agent:

  1. The Python graph calls a remote tool (one you declared in 10xgraph.json).
  2. The server returns a RemoteToolCallBlock to the client.
  3. The client SDK finds your registered handler by name.
  4. Your handler executes locally and returns a result.
  5. The SDK wraps the result in a tool result message and sends it back.
  6. The server continues the graph with the tool result.

This loop repeats until the graph is done or hits the recursion limit (default 25 calls).

Step 1: Declare schemas in 10xgraph.json

Add a remote_tools array to your config. Each entry describes a tool the server will advertise to the model and the agent will be allowed to call:

JSON
{
  "agent": "graph.agent:app",
  "remote_tools": [
    {
      "node": "tools",
      "name": "get_location",
      "description": "Read the browser's current location using geolocation.",
      "parameters": {
        "type": "object",
        "properties": {
          "high_accuracy": {
            "type": "boolean",
            "description": "Request high-accuracy coordinates (slower)."
          }
        },
        "required": []
      }
    },
    {
      "node": "tools",
      "name": "read_clipboard",
      "description": "Read text from the user's clipboard.",
      "parameters": {
        "type": "object"
      }
    }
  ]
}

Schema validation happens at server startup. The server checks that:

  • Each entry has a non-blank node, name and description. (node_name is accepted as an alias for node.)
  • The name is unique within your remote tools list.
  • parameters is an object schema: type must be "object", properties an object, required a list of strings. Missing keys default to {"type": "object", "properties": {}, "required": []}.
  • No unknown fields are present.

At startup the server attaches each schema to the named ToolNode in your graph.

Restart the API after changing schemas. To validate syntax before restarting, run:

Terminal
10xgraph audit

This command validates the remote_tools entries in your 10xgraph.json and other environment and configuration checks.

Step 2: Register matching handlers

On the client, register a handler function for each declared tool. Do this before calling invoke(), stream(), or wsStream():

TypeScript
import { TenxGraphClient, Message } from '10xgraph-client';

const client = new TenxGraphClient({
  baseUrl: 'http://localhost:8000',
  authToken: 'your-bearer-token', // if auth is enabled
});

// Geolocation handler
client.registerToolHandler('get_location', async ({ high_accuracy = false }) => {
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(
      (position) => {
        resolve({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
          accuracy: position.coords.accuracy,
        });
      },
      reject,
      { enableHighAccuracy: high_accuracy }
    );
  });
});

// Clipboard handler
client.registerToolHandler('read_clipboard', async () => {
  const text = await navigator.clipboard.readText();
  return { content: text };
});

Handler requirements:

  • Signature: async (args: any) => Promise<any>: the async function receives the tool arguments as a single object and must return a JSON-serializable value.
  • Arguments: The model generates arguments based on the schema you declared in 10xgraph.json. Destructure them as shown, or access them as properties: args.high_accuracy.
  • Return value: Must be serializable to JSON (objects, arrays, strings, numbers, booleans, null). If your result is not serializable, the tool fails.
  • Errors: If the handler throws, the error is caught and converted to a failed tool result. The agent sees the error message and can retry or handle it.

Register handlers before any invoke() or stream() call. The SDK runs handlers automatically when the response contains a RemoteToolCallBlock.

Step 3: Invoke normally

Call invoke() or stream() as usual. The client detects remote tool calls and handles them internally:

TypeScript
// Simple invoke
const result = await client.invoke([
  Message.text_message("Where am I right now?", "user")
]);

console.log(result.messages);
// The agent has seen the location and replied.

Or stream with an async loop:

TypeScript
const stream = client.stream([
  Message.text_message("What's on my clipboard?", "user")
]);

for await (const chunk of stream) {
  if (chunk.event === 'message') {
    console.log('Agent:', chunk.message?.content);
  } else if (chunk.event === 'updates') {
    console.log('Update:', chunk.data);
  }
}

The SDK automatically:

  1. Detects RemoteToolCallBlock chunks.
  2. Finds and runs the matching handler.
  3. Sends the result back to the server.
  4. Continues streaming or invoking.

If a handler is missing, the SDK creates a failed tool result with the message Tool '{name}' not found. The agent sees this and may retry or ask for clarification.

registerToolHandler(name, handler)

Register a single tool handler.

Parameter Type Description
name string Must exactly match a configured remote_tools[].name in 10xgraph.json. Case-sensitive.
handler (args: any) => Promise<any> An async function that executes locally. Receives model-generated arguments and returns a serializable result.

Example with error handling:

TypeScript
client.registerToolHandler('fetch_external_data', async ({ url }) => {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    // Re-throwing here causes the SDK to mark the tool as failed.
    // The error message is sent to the agent.
    throw new Error(`Failed to fetch ${url}: ${(error as Error).message}`);
  }
});

registerTool({ name, handler, node?, description?, parameters? }) is also available and takes optional metadata. registerToolHandler() is the right choice when the schema lives in 10xgraph.json, because the server owns the trusted schema.

Execution behavior

When a response from the server contains one or more RemoteToolCallBlock objects, the client:

  1. Finds the handler by name from your registered handlers.
  2. Executes the handler with the arguments the model generated.
  3. Wraps the result in a ToolResultBlock (or error block if the handler threw).
  4. Sends it back in a tool result message.
  5. Continues invoking until all remote calls are resolved or the recursion limit is reached.

The recursion limit (default 25) applies to each invoke() or stream() call. Pass a custom limit in options:

TypeScript
const result = await client.invoke(
  [Message.text_message("...", "user")],
  { recursion_limit: 50 }
);

const stream = client.stream(
  [Message.text_message("...", "user")],
  { recursion_limit: 10 }
);

Errors and missing handlers

If a handler throws an exception or a handler is not registered:

  • The exception is caught at the client.
  • A failed ToolResultBlock is created with is_error: true and the error message.
  • The result is sent back to the server as a failed tool call.
  • The agent sees the failure and can retry, ask for clarification, or continue.

Example:

TypeScript
// Handler throws
client.registerToolHandler('risky_tool', async () => {
  throw new Error('Something went wrong');
});

// When called, the error is caught and sent to the agent as a ToolResultBlock:
// output: { error: 'Something went wrong' }, is_error: true, status: 'failed'

Missing handlers produce a similar failure message:

TypeScript
// No handler registered for 'unregistered_tool'
// Agent receives output { error: "Tool 'unregistered_tool' not found" }, is_error: true, status: 'failed'

WebSocket streaming

Both stream() (HTTP) and wsStream() (WebSocket) automatically handle remote tools. Choose based on your needs:

  • stream(): HTTP streaming with one request per tool-call loop. Simpler; good for low latency and single-tool calls.
  • wsStream(): Single persistent WebSocket for the full lifecycle. Better for multi-turn interactions or frequent tool calls.

Both have identical APIs and tool-handling behavior:

TypeScript
// Identical code; only the method name differs
const httpStream = client.stream([userMessage]);
const wsStream = client.wsStream([userMessage]);

for await (const chunk of httpStream) {
  // Handle chunk
}

Troubleshooting

Symptom Cause Fix
Agent never calls the tool Tool not declared in remote_tools or wrong node Add it to 10xgraph.json with the correct ToolNode name, then restart the API. Run 10xgraph audit to validate.
Tool 'X' not found error Handler not registered or name mismatch Register the handler with the exact name from remote_tools[].name before calling invoke() or stream(). Names are case-sensitive.
API startup fails with validation error Typo in schema, duplicate tool name, or malformed parameters Fix the remote_tools entry in 10xgraph.json. Run 10xgraph audit to identify the error.
Handler result fails silently Result is not JSON-serializable (e.g., contains a Function or circular reference) Return only JSON-safe data: objects, arrays, strings, numbers, booleans, null. Use JSON.stringify(result) to test before returning.
WebSocket closes unexpectedly Network issue, timeout, or server error Check browser console and server logs for details. The stream error propagates to your catch block or for await error handler. Reconnect and retry.
Tool is called repeatedly in a loop Agent’s reasoning or parameters trigger a retry loop Review the tool description and parameters in 10xgraph.json. Make sure the tool description clearly states its purpose and output format. If the tool should not be retried, ensure your handler returns a clear result.

Next steps

Last updated for v0.10.0Edit this page on GitHubReport an issue