Skip to content

Handling Tool Calls

profile.tools() returns model-callable Configure functions and profile.executeTool() dispatches them. Keep your existing tool router — forward only Configure-prefixed (configure_*) calls to Configure, and run your own tools as you do today.

The token used below comes from the server-side Continue with Configure exchange or the inline Link fallback. The model never sees that token; it only sees tool schemas and tool results.

Two facts make wiring this into an existing loop a two-line change:

  • profile.tools() returns Anthropic-native schemas: { name, description, input_schema }. Pass them straight to the Anthropic SDK. For OpenAI, wrap them with toOpenAIFunctions() (exported from configure).
  • profile.executeTool() accepts either { name, arguments } (OpenAI-style) or { name, input } (Anthropic-style). Pass the tool call through in whichever shape your provider produced.

When the model calls configure_profile_read, that tool call is the personalization magic moment — and it shows up in your logs as a real tool call.

Anthropic

ts
import Anthropic from "@anthropic-ai/sdk";
import { Configure } from "configure";

const anthropic = new Anthropic();
const configure = new Configure({
  apiKey: process.env.CONFIGURE_API_KEY,
  agent: process.env.CONFIGURE_AGENT,
});

async function chat({ token, messages, system }) {
  const profile = configure.profile({ token });

  // profile.tools() is already Anthropic-shaped — spread it next to your own tools.
  const tools = [...yourTools, ...profile.tools({ connectors: ["gmail"] })];

  while (true) {
    const response = await anthropic.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 1024,
      system,
      messages,
      tools,
    });

    const toolUses = response.content.filter((block) => block.type === "tool_use");
    if (!toolUses.length) {
      return response.content.find((block) => block.type === "text")?.text ?? "";
    }

    messages.push({ role: "assistant", content: response.content });

    const toolResults = [];
    for (const call of toolUses) {
      const result = call.name.startsWith("configure_")
        ? await profile.executeTool({ name: call.name, input: call.input })
        : await executeYourTool(call);
      toolResults.push({
        type: "tool_result",
        tool_use_id: call.id,
        content: JSON.stringify(result),
      });
    }
    messages.push({ role: "user", content: toolResults });
  }
}

OpenAI

ts
import OpenAI from "openai";
import { Configure, toOpenAIFunctions } from "configure";

const openai = new OpenAI();
const configure = new Configure({
  apiKey: process.env.CONFIGURE_API_KEY,
  agent: process.env.CONFIGURE_AGENT,
});

async function chat({ token, messages }) {
  const profile = configure.profile({ token });

  // Your own tools are already OpenAI-shaped; convert only the Configure tools.
  const tools = [
    ...yourTools,
    ...toOpenAIFunctions(profile.tools({ connectors: ["gmail"] })),
  ];

  while (true) {
    const completion = await openai.chat.completions.create({
      model: "gpt-4o",
      messages,
      tools,
      tool_choice: "auto",
    });

    const message = completion.choices[0].message;
    if (!message.tool_calls?.length) {
      return message.content ?? "";
    }

    messages.push(message);

    for (const call of message.tool_calls) {
      const args = JSON.parse(call.function.arguments || "{}");
      const result = call.function.name.startsWith("configure_")
        ? await profile.executeTool({ name: call.function.name, arguments: args })
        : await executeYourTool(call);
      messages.push({
        role: "tool",
        tool_call_id: call.id,
        content: JSON.stringify(result),
      });
    }
  }
}

Verify Without A Model

To confirm the tool path is wired — in a smoke test, or when no provider key is set — call the read tool directly. This is the same dispatch the model uses, with no provider involved:

ts
const profile = configure.profile({ token });
const result = await profile.executeTool({
  name: "configure_profile_read",
  arguments: { sections: ["identity", "summary", "preferences", "imports"] },
});

A non-error result confirms your keys, agent handle, token (or externalId), and the tool path are correct.

Framework Adapters

If your framework already drives the loop and exposes an executeTool/onToolCall callback (for example the Vercel AI SDK), you do not need the loops above. Pass a thin adapter that forwards Configure-prefixed calls:

ts
executeTool: (toolCall) =>
  toolCall.name.startsWith("configure_")
    ? profile.executeTool(toolCall)
    : executeYourTool(toolCall),

This is a framework-specific shortcut, not the universal path — most providers need an explicit loop like the ones above.

Tool Set

With no options, profile.tools() returns only:

  • configure_profile_read
  • configure_profile_search
  • configure_profile_remember

Connector tools are returned only when enabled through connectors. Action tools are returned only when enabled through actions. profile.executeTool() enforces the same enabled set: a call to configure_email_send fails unless profile.tools({ actions: ["email.send"] }) was used for that runtime handle.

profile.commit() is runtime plumbing called after the model turn. It is not part of the default model tool set.

Personalization infrastructure for agents