> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tracepilotai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Trace tool calls, functions, and external API calls

> Use wrapToolCall to instrument any async function your agent invokes — web searches, database queries, emails, or any other external API call.

AI agents rarely stop at a single LLM call. They search the web, query databases, send emails, and call external APIs. `tp.wrapToolCall` wraps any async function the same way `tp.wrapOpenAI` wraps completions — giving you a named span in the dashboard for every tool your agent uses, linked into the same execution tree.

## How `wrapToolCall` works

`wrapToolCall` takes a tool name, an async function to execute, and the span ID of the parent span. It runs your function, records the input and output as a span, and returns both the original result and a new `spanId` you can use to nest further steps beneath it.

```typescript theme={null}
const { result, spanId } = await tp.wrapToolCall(
  toolName,     // string — appears as the span label in the dashboard
  call,         // async function returning the tool result
  parentSpanId, // spanId from a previous wrapOpenAI or wrapToolCall call
  stepOrder,    // number — controls display order within the parent
  isDestructive // optional boolean — adds a ⚠ badge in the dashboard
);
```

The function you pass to `call` can do anything: hit an HTTP endpoint, run a database query, send a message. TracePilot captures whatever it returns as the span output.

## Tracing a non-destructive tool

A web search reads data without modifying anything external. Wrap it without `isDestructive`.

```typescript theme={null}
import { TracePilot } from 'tracepilot-sdk';
import OpenAI from 'openai';

const tp = new TracePilot('tp_live_YOUR_KEY');
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function run(query: string) {
  await tp.startTrace('search-agent');

  const messages = [{ role: 'user', content: query }];

  // Step 1 — LLM decides what to search for
  const { result: plan, spanId: planSpanId } = await tp.wrapOpenAI(
    () => openai.chat.completions.create({ model: 'gpt-4o', messages }),
    messages,
    undefined,
    1
  );

  // Step 2 — web search tool, child of the LLM span
  const { result: searchResult, spanId: searchSpanId } = await tp.wrapToolCall(
    'web-search',
    () => webSearch(plan.choices[0].message.content ?? ''),
    planSpanId,  // links this span as a child of the LLM call
    2
    // isDestructive omitted — defaults to false
  );

  return searchResult;
}
```

In the dashboard, the `web-search` span appears nested under the LLM span that triggered it, and you can inspect the query it received and the results it returned.

## Tracing a destructive tool

A tool that sends an email, writes to a database, or makes a payment modifies external state. Pass `isDestructive: true` so anyone reviewing the trace knows this step had a real-world side effect.

```typescript theme={null}
const { result: emailResult, spanId: emailSpanId } = await tp.wrapToolCall(
  'send-email',
  () => sendEmail({
    to: 'user@example.com',
    subject: 'Your order has shipped',
    body: emailBody
  }),
  parentSpanId,
  3,
  true  // isDestructive — marks this span with a ⚠ Destructive badge
);
```

<Warning>
  Mark any tool as destructive when it modifies external state: database writes, outbound emails, payment charges, API mutations, file deletions, or webhook triggers. The badge is a signal to you and your team during incident review — not a guard against execution.
</Warning>

## Linking tool spans to parent LLM spans

The `parentSpanId` parameter is what connects tool calls into the same execution tree as the LLM spans that triggered them. Every `wrapOpenAI` and `wrapToolCall` call returns a `spanId`. Pass that ID as `parentSpanId` to the next call that logically follows from it.

```typescript theme={null}
// LLM produces a plan — root span, no parent
const { result: plan, spanId: planSpanId } = await tp.wrapOpenAI(
  () => openai.chat.completions.create({ model: 'gpt-4o', messages }),
  messages,
  undefined,
  1
);

// Tool executes based on the plan — child of the LLM span
const { result: data, spanId: dataSpanId } = await tp.wrapToolCall(
  'fetch-user-record',
  () => db.users.findById(userId),
  planSpanId,  // parent is the LLM span
  2
);

// Second LLM call uses the tool result — child of the tool span
const { result: response } = await tp.wrapOpenAI(
  () => openai.chat.completions.create({ model: 'gpt-4o', messages: followUp }),
  followUp,
  dataSpanId,  // parent is the tool span
  3
);
```

This builds a tree that reads like your agent's reasoning: the LLM decided to call the tool, the tool returned data, the LLM used that data to respond.

## What the dashboard shows for tool spans

Each tool span displays the tool name as its label, the arguments your function received, the return value, execution latency, and any error if the function threw. Destructive spans show a **⚠ Destructive** badge in the span header.

<AccordionGroup>
  <Accordion title="Wrapping synchronous functions">
    `wrapToolCall` expects the `call` argument to be an async function (one that returns a Promise). Wrap synchronous functions in an async arrow function.

    ```typescript theme={null}
    const { result } = await tp.wrapToolCall(
      'parse-json',
      async () => JSON.parse(rawInput),
      parentSpanId,
      2
    );
    ```
  </Accordion>

  <Accordion title="Handling errors from tool calls">
    If the function passed to `call` throws, `wrapToolCall` records the error as a failed span and re-throws the original error. Add a `try/catch` around the call if you want to recover gracefully.

    ```typescript theme={null}
    try {
      const { result } = await tp.wrapToolCall(
        'external-api',
        () => callExternalApi(payload),
        parentSpanId,
        2
      );
    } catch (err) {
      // Span is already marked as failed in the dashboard.
      console.error('Tool call failed:', err);
    }
    ```
  </Accordion>

  <Accordion title="Naming conventions for toolName">
    Use a short, lowercase, hyphen-separated string that describes what the tool does: `web-search`, `send-email`, `get-user-record`, `calculate-price`. Avoid generic names like `tool1` or `step` — the name is the primary label in the span tree.
  </Accordion>
</AccordionGroup>
