Tools

Tools are how the model reaches outside the conversation. The agent backend supports two sources of tools: client-supplied definitions passed at session creation, and MCP gateway providers resolved at request time.

Where tools come from

Tools can be loaded from two places:

  1. Client-supplied — passed as extensions.tools when creating a session. The client defines the schema and handles execution.
  2. MCP gateway — loaded at request time from the platform's own MCP gateway. Each model definition (or session's extensions.mcpProviders) declares which providers it cares about, and the inference server resolves them:
GET ${MCP_GATEWAY_DOMAIN}/v1/mcp/providers/${providerName}

The gateway returns tool definitions for that provider. The backend hands them to the model alongside any client-supplied tools.

Info

MCP_GATEWAY_DOMAIN points at the platform's MCP gateway, not at an arbitrary MCP server. The inference server only ever talks to our gateway. The gateway itself maintains a provider registry — that's the pluggable layer. Today NetSuite is the only registered base provider; the registry is extensible (registerMCPPlatform / MCPPlatformDefinition) but additional providers haven't been opened up for general use yet.

Defining tools

Each tool definition describes what the tool does and what data it expects and returns. Definitions use JSON Schema for input and output shapes.

Schema

interface ToolDefinition {
  /** Unique name — must match the function that executes it */
  name: string;
  /** Human-readable description — the model reads this to decide when to call */
  description: string;
  /** JSON Schema describing the input parameters */
  requestArgs: {
    properties: Record<string, JSONSchema>;
    required?: string[];
  };
  /** JSON Schema describing the response shape */
  responseShape: {
    properties: Record<string, JSONSchema>;
  };
}

Example

const getLocations = {
  name: "getLocations",
  description: "Retrieves a list of warehouse locations",
  requestArgs: {
    properties: {
      includeInactive: {
        type: "boolean",
        description: "Include inactive locations",
      },
    },
  },
  responseShape: {
    properties: {
      locations: {
        type: "array",
        items: {
          type: "object",
          properties: {
            id: { type: "number" },
            name: { type: "string" },
            useBins: { type: "boolean" },
          },
        },
      },
      error: { type: "string" },
    },
  },
};

Auto-injected fields

The platform automatically injects certain fields into every tool:

  • sessionId is added to requestArgs (marked required) — the model always knows which session it's operating in.
  • state is added to responseShape with enum COMPLETE | FAILED | PENDING — every tool response must indicate its completion status.

You don't need to include these in your definitions — they're added for you.

Passing tools to a session

Supply tool definitions when creating a session. extensions.tools is a record keyed by tool name (not an array) — the schema is Record<string, ToolDefinition>:

await fetch(`${BASE_URL}/v1/sessions`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    customizations: {
      agentId: "RfsAgent",
      extensions: {
        tools: {
          getLocations,
          getBinContents,
          createTransfer,
        },
      },
    },
  }),
});

The model can then call any of these tools during the conversation. When it does, your client receives the call and is responsible for executing it and returning the result.

Tool naming conventions

Tool names carry meaning through prefixes. The platform and client libraries use these to determine routing and authorization:

PrefixBehaviorHandled by
(none)Standard tool — read-only queries, no side effectsClient
_Internal/server-side tool (e.g. _titleSession)Server (automatic)
pre_Requires user confirmation before executionClient (with UI gate)
post_Auto-executed after a related actionClient (automatic)
priv_Private tool — requires TOTP auth via MCP GatewayServer (MCP gateway)
mcp_MCP provider toolServer (MCP gateway)
Tip

Your client only needs to handle tools classified as STANDARD (no prefix) and optionally pre_ / post_ tools. Everything else is managed by the server. Use getToolClassification() from @rfsmart/ai-agent-types to check.

Why tool calls are async

Many tools wrap operations that take seconds or minutes. The agent backend can't block waiting for those, so the protocol is explicitly two-phase:

  1. The model emits a tool_use block in its response.
  2. The caller picks it up, executes the tool out-of-band, sends heartbeats while it works, and finally submits the result.
  3. The server inserts the tool_result block and the model continues on the next call.

Lifecycle

A tool call moves through four states, persisted so you can reconnect to it from anywhere:

PENDINGinitial state

The model has emitted the tool call; nobody has picked it up yet.

PROCESSINGcaller is working

Set when the caller first sends a heartbeat. Subsequent heartbeats refresh the lock timestamp.

COMPLETEterminal — success

Set when the caller submits a result. The session can now resume.

ERRORterminal — failure

Set when the caller submits an error result. The session still resumes; the model gets to see and react to the error.

Heartbeats

The heartbeat protocol detects stuck callers. If a tool sits in PROCESSING without a heartbeat for too long, the server marks it as abandoned.

There are two heartbeat paths, and the distinction matters for disconnect behavior:

  • Server-side (MCP-routed tools). When a tool is fulfilled by the platform's MCP gateway, the gateway emits heartbeats back to the inference server during execution. The client doesn't participate, so a client disconnect does not abandon the tool. Long-running MCP-routed tools survive disconnects.
  • Client-side (client-supplied tools). When a tool is fulfilled by your own client — i.e. you defined it via extensions.tools — the heartbeat runs from the device that picked up the call (browser, mobile app, server-side worker). If that device disconnects, the heartbeat loop stops and the tool is abandoned after the timeout.

For client-side tools, send heartbeats on a timer at a regular interval while you're working. A cadence of 250ms is typical for responsive tool execution. The endpoint is cheap — it just bumps a timestamp.

const interval = setInterval(async () => {
  await fetch(`${BASE_URL}/v1/tools/request/${sessionId}/${requestId}/heartbeat`, {
    method: "POST",
    headers,
    body: JSON.stringify({ state: "PROCESSING", heartbeat: Date.now() }),
  });
}, 250);
Warning

Always heartbeat from a separate timer, not from inside the tool's main work loop. If your tool blocks, the heartbeat must still fire or the server will assume you crashed.

Info

If you need true disconnect-survivability for a long-running operation, route it through an MCP provider rather than implementing it as a client-supplied tool — that's the path where heartbeats happen server-side.

See the Tools API reference for endpoint details.

Submitting results

When the tool finishes, POST /v1/tools/response/:sessionId/:requestId with a structured result. The server validates the shape, marks the tool COMPLETE, and the next call to the messages endpoint will resume the conversation with the result fed back to the model.

// Success
await fetch(`${BASE_URL}/v1/tools/response/${sessionId}/${requestId}`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    response: { state: "COMPLETE", locations: [...] },
  }),
});
 
// Failure
await fetch(`${BASE_URL}/v1/tools/request/${sessionId}/${requestId}/heartbeat`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    state: "ERROR",
    error: "Query timed out after 30 seconds",
  }),
});

If the tool failed, submit an error via the heartbeat endpoint with state: "ERROR". The model is trained to recover gracefully — it'll often try a different approach or ask the user for help.