Skip to main content

Documentation Index

Fetch the complete documentation index at: https://tracepilot.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

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.
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.
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.
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
);
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.

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.
// 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.
wrapToolCall expects the call argument to be an async function (one that returns a Promise). Wrap synchronous functions in an async arrow function.
const { result } = await tp.wrapToolCall(
  'parse-json',
  async () => JSON.parse(rawInput),
  parentSpanId,
  2
);
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.
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);
}
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.