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

# Agent tracing — withAgent and multi-agent systems

> Use withAgent (TypeScript) or with_agent (Python) to trace multi-agent workflows, capture planning steps, tool calls, and agent-to-agent handoffs as linked spans in the Zespan dashboard.

Zespan provides a first-class API for tracing multi-agent systems. In TypeScript, the `withAgent` function creates an agent span and gives you an `AgentContext` object to log planning steps, instrument tool calls, and record handoffs to other agents. In Python, `with_agent` is a context manager that does the same thing. All LLM calls made inside the block automatically inherit the agent's trace context — no additional wiring is required.

## The `withAgent` / `with_agent` function

`withAgent(options, fn)` starts an agent trace, runs your workflow function with an `AgentContext`, and automatically links any nested LLM calls to the same trace. `with_agent(...)` does the same as a context manager, yielding the `AgentContext` to the `with` block.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { zespan, withAgent } from "@zespan/sdk";
  import OpenAI from "openai";

  zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });
  const openai = zespan.wrapOpenAI(new OpenAI());

  await withAgent(
    {
      name: "CustomerSupportAgent",
      role: "coordinator",
      tools: [
        { name: "lookup_order", description: "Fetch order details by ID" },
        { name: "check_policy", description: "Check refund eligibility" },
      ],
    },
    async (agent) => {
      // Log the steps the agent intends to take
      agent.logPlan(["Lookup order", "Check policy", "Draft response"]);

      // Trace a tool call — args and result are captured
      const order = await agent.traceTool(
        "lookup_order",
        { id: "123" },
        () => fetchOrder("123")
      );

      // Make an LLM call — it automatically links to this agent span
      const response = await openai.chat.completions.create({
        model: "gpt-4o",
        messages: [
          { role: "system", content: "You are a support agent." },
          { role: "user", content: `Order details: ${JSON.stringify(order)}` },
        ],
      });

      // Record a handoff to a specialist agent
      agent.delegateTo("RefundPolicyAgent", "refund requested");
    }
  );
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import os
  import zespan
  from zespan import with_agent

  zespan.init(api_key=os.environ["ZESPAN_API_KEY"])
  zespan.patch_openai()

  import openai  # import after patching
  client = openai.OpenAI()

  with with_agent(
      name="CustomerSupportAgent",
      role="coordinator",
      tools=[
          {"name": "lookup_order", "description": "Fetch order details by ID"},
          {"name": "check_policy", "description": "Check refund eligibility"},
      ],
  ) as agent:
      # Log the steps the agent intends to take
      agent.log_plan(["Lookup order", "Check policy", "Draft response"])

      # Trace a tool call — args and result are captured
      order = agent.trace_tool(
          "lookup_order", {"id": "123"}, lambda: fetch_order("123")
      )

      # Make an LLM call — it automatically links to this agent span
      response = client.chat.completions.create(
          model="gpt-4o",
          messages=[
              {"role": "system", "content": "You are a support agent."},
              {"role": "user", "content": f"Order details: {order}"},
          ],
      )

      # Record a handoff to a specialist agent
      agent.delegate_to("RefundPolicyAgent", "refund requested")
  ```
</CodeGroup>

<Note>
  Python's `trace_tool()` calls `fn()` synchronously and does not `await` it — pass a plain sync callable (a `lambda` or a regular function), as shown above. There is no async variant of `trace_tool` in the Python SDK today. TypeScript's `traceTool()` accepts either a sync or async (Promise-returning) function and awaits it internally.
</Note>

## `AgentOptions` / `with_agent()` parameters

In TypeScript, `withAgent` takes a single `AgentOptions` object. In Python, `with_agent()` takes the same fields as keyword arguments directly — there is no options object.

<ParamField body="name" type="string" required>
  Display name for this agent. Appears in the trace view and the agent registry. Same in both SDKs.
</ParamField>

<ParamField body="role" type="string" default="specialist">
  Role label for this agent. Common values: `"coordinator"`, `"specialist"`, `"planner"`. Used to distinguish orchestrators from workers in multi-agent traces.

  TypeScript defaults to `"specialist"` when omitted. Python's `role` keyword has no default — it stays `None` if you don't pass it.
</ParamField>

<ParamField body="framework" type="string" default="custom">
  Framework powering this agent. Examples: `"custom"`, `"langchain"`, `"google-adk"`, `"openai-assistants"`.

  TypeScript defaults to `"custom"` when omitted. Python's `framework` keyword has no default — it stays `None` if you don't pass it.
</ParamField>

<ParamField body="tools" type="ToolDefinition[]">
  Tool definitions available to this agent. Each object should have a `name` and a `description`. These appear in the tool discovery view and are linked to tool call spans.

  In Python, pass a plain `list` of `dict` objects with the same `name`/`description` shape — there is no `ToolDefinition` class to construct.
</ParamField>

<ParamField body="version" type="string">
  TypeScript only. Free-form version string attached to the agent span (`agent_version`). There is no equivalent keyword on Python's `with_agent()`.
</ParamField>

<ParamField body="description" type="string">
  TypeScript only. Free-form description attached to the agent span (`agent_description`). There is no equivalent keyword on Python's `with_agent()`.
</ParamField>

<ParamField body="metadata" type="Record<string, unknown>">
  TypeScript only. Arbitrary key-value metadata attached to the agent span. There is no equivalent keyword on Python's `with_agent()` — Python's agent-start event does not carry a `metadata` field.
</ParamField>

## `AgentContext` methods

The `agent` object passed to your function (TypeScript) or yielded by the `with` block (Python) exposes three methods: `logPlan`/`log_plan`, `traceTool`/`trace_tool`, and `delegateTo`/`delegate_to`.

### `agent.logPlan(steps)` / `agent.log_plan(steps)`

Records a `planning` span with the list of steps the agent intends to take. Call this after deciding what to do and before executing.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  agent.logPlan([
    "Retrieve customer history",
    "Identify issue category",
    "Draft resolution",
  ]);
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  agent.log_plan([
      "Retrieve customer history",
      "Identify issue category",
      "Draft resolution",
  ])
  ```
</CodeGroup>

### `agent.traceTool(name, args, fn)` / `agent.trace_tool(name, args, fn)`

Wraps a function call and records a `tool` span containing the tool name, input arguments, and return value.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  const result = await agent.traceTool(
    "lookup_order",
    { id: "123" },
    () => fetchOrder("123")
  );
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  result = agent.trace_tool(
      "lookup_order",
      {"id": "123"},
      lambda: fetch_order("123"),
  )
  ```
</CodeGroup>

If the wrapped function throws (Python: raises), the tool span is recorded with `status: "error"` and the error is re-thrown/re-raised in both SDKs.

<Note>
  Two implementation details differ between the SDKs' tool spans:

  * **Tool definition lookup.** TypeScript's `traceTool()` matches `name` against the `tools` array passed to `withAgent` and attaches the matching entry as `tool_definitions` on the span. Python's `trace_tool()` does not currently do this lookup, so the tool span it emits has no `tool_definitions`.
  * **Operation naming.** The `operation` field on the emitted span is `tool.<name>` in TypeScript but `<agentName>.tool.<name>` in Python.

  Both SDKs record the same `tools_used`, `tool_call_args`, `tool_call_result` (or `error_message` on failure), `latency_ms`, and `status` fields.
</Note>

### `agent.delegateTo(targetName, reason?)` / `agent.delegate_to(target_agent_name, reason=None)`

Records a `handoff` span indicating that this agent is delegating work to another agent. The `reason` string is stored as `delegation_reason` on the span.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  agent.delegateTo("RefundPolicyAgent", "customer requested refund");
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  agent.delegate_to("RefundPolicyAgent", "customer requested refund")
  ```
</CodeGroup>

## Span kinds emitted

`withAgent`/`with_agent` and their methods produce four distinct span kinds, each visible as a separate row in the trace flame graph:

| Span kind  | TypeScript           | Python                | What it represents                  |
| ---------- | -------------------- | --------------------- | ----------------------------------- |
| `agent`    | `withAgent` entry    | `with_agent` entry    | The agent's overall execution scope |
| `planning` | `agent.logPlan()`    | `agent.log_plan()`    | The planned steps before execution  |
| `tool`     | `agent.traceTool()`  | `agent.trace_tool()`  | A single tool invocation            |
| `handoff`  | `agent.delegateTo()` | `agent.delegate_to()` | A delegation to another agent       |

## Nested agents

Agents can be nested inside each other. The outer agent block sets a trace context that is automatically inherited by the inner one (via `AsyncLocalStorage`-based context in TypeScript, `contextvars` in Python). The inner agent span records the outer agent's ID as `parent_agent_id`, building a parent-child hierarchy visible in the agent registry.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  await withAgent({ name: "CoordinatorAgent", role: "coordinator" }, async (coordinator) => {
    coordinator.logPlan(["Classify request", "Delegate to specialist"]);

    // The nested withAgent automatically links to the coordinator's trace
    await withAgent({ name: "RefundSpecialist", role: "specialist" }, async (specialist) => {
      specialist.logPlan(["Check eligibility", "Process refund"]);

      const eligible = await specialist.traceTool(
        "check_eligibility",
        { orderId: "123" },
        () => checkRefundEligibility("123")
      );

      if (eligible) {
        await specialist.traceTool(
          "process_refund",
          { orderId: "123", amount: 49.99 },
          () => processRefund("123", 49.99)
        );
      }
    });

    coordinator.delegateTo("RefundSpecialist", "refund request classified");
  });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  with with_agent(name="CoordinatorAgent", role="coordinator") as coordinator:
      coordinator.log_plan(["Classify request", "Delegate to specialist"])

      # The nested with_agent automatically links to the coordinator's trace
      with with_agent(name="RefundSpecialist", role="specialist") as specialist:
          specialist.log_plan(["Check eligibility", "Process refund"])

          eligible = specialist.trace_tool(
              "check_eligibility",
              {"order_id": "123"},
              lambda: check_refund_eligibility("123"),
          )

          if eligible:
              specialist.trace_tool(
                  "process_refund",
                  {"order_id": "123", "amount": 49.99},
                  lambda: process_refund("123", 49.99),
              )

      coordinator.delegate_to("RefundSpecialist", "refund request classified")
  ```
</CodeGroup>

<Tip>
  Set `role: "coordinator"` (`role="coordinator"` in Python) on the top-level agent and `role: "specialist"`/`role="specialist"` on sub-agents. This powers the coordinator-specialist breakdown in the agent analytics view.
</Tip>

<Warning>
  Always `await` `withAgent` when it wraps async work. If you do not await it, the agent span may close before inner tool or LLM spans complete, resulting in broken parent-child links in the trace. Python's `with_agent` is a synchronous context manager — the `with` block naturally runs to completion before the agent span closes, so this failure mode does not apply there.
</Warning>

<Note>
  TypeScript additionally sets `delegation_reason` to `"root"` or `"delegated"` automatically on the agent-start event, based on whether a parent agent is already active in context. Python's `with_agent` start event does not set `delegation_reason` — it is only populated there when you call `agent.delegate_to()`.
</Note>

## Cross-service agent context propagation

Nested agents work automatically when both agents run in the same process. When one agent delegates to another agent running in a **different service** — for example, a coordinator agent in one Node.js service calling an HTTP endpoint that runs a specialist agent in another service — the trace context has to be carried across the network boundary too, or the two services will show up as unrelated traces. `injectAgentContext` and `extractAgentContext` carry the current W3C trace context (plus optional delegation metadata) over outgoing HTTP headers via [Baggage](https://www.w3.org/TR/baggage/), so the receiving service's spans link back to the sender's trace.

**Sending side** — inject context into outgoing request headers before calling the downstream agent:

```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { injectAgentContext, withAgent } from "@zespan/sdk";

await withAgent({ name: "CoordinatorAgent", role: "coordinator" }, async (agent) => {
  const headers: Record<string, string> = { "content-type": "application/json" };
  injectAgentContext(headers, "refund requested", "Process refund for order 123");

  await fetch("https://refunds-service.internal/handle", {
    method: "POST",
    headers,
    body: JSON.stringify({ orderId: "123" }),
  });

  agent.delegateTo("RefundPolicyAgent", "refund requested");
});
```

**Receiving side** — extract the context on the downstream service so its spans link to the sender's trace:

```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { extractAgentContext, withAgent } from "@zespan/sdk";
import { context } from "@opentelemetry/api";

app.post("/handle", async (req, res) => {
  const parentCtx = extractAgentContext(req.headers as Record<string, string>);

  await context.with(parentCtx, async () => {
    await withAgent({ name: "RefundPolicyAgent", role: "specialist" }, async (agent) => {
      // ... process the refund, linked back to CoordinatorAgent's trace
    });
  });

  res.sendStatus(200);
});
```

<ParamField body="headers" type="Record<string, string>" required>
  On the sending side, an outgoing headers object that is mutated in place — W3C `traceparent` and `baggage` entries are added. On the receiving side, the incoming headers to read them back from.
</ParamField>

<ParamField body="delegationReason" type="string">
  Injecting side only, optional. Stored in baggage as `agent.delegation.reason` — why the work is being handed off to the downstream service.
</ParamField>

<ParamField body="taskDescription" type="string">
  Injecting side only, optional. Stored in baggage as `agent.task` — a description of the task being delegated.
</ParamField>

`extractAgentContext(headers)` returns an OpenTelemetry `Context`. Activate it with `context.with()` as shown above, or pass it as the third argument to `tracer.startSpan()`, so that any spans created on the receiving side — including a nested `withAgent` — attach to the sender's trace instead of starting a new one.

<Note>
  These same functions are re-exported as `injectAutoGenContext`/`extractAutoGenContext` for AutoGen users — they are identical functions under framework-specific names. See the [AutoGen](/sdk/integrations/autogen) and [CrewAI](/sdk/integrations/crewai) integration pages for how each framework uses them at the HTTP boundary.
</Note>

<Warning>
  `injectAgentContext`/`extractAgentContext` are **TypeScript-only** — the Python SDK has no equivalent export for carrying W3C trace context (`traceparent`/`baggage`) over HTTP headers between services. Python's `with_agent` nesting (see [Nested agents](#nested-agents)) links agents together via `contextvars`, which only works within a single process; there is currently no documented, SDK-provided way to propagate that context across a network boundary from Python. If your architecture delegates between agents running in separate Python services, you'll need to carry and re-establish trace/span IDs yourself (for example, by round-tripping `trace_id`/`span_id` through your own request payload and passing them into `with_zespan_context()` on the receiving side).
</Warning>

## Python: `trace_tool_fn` (Python-only)

The Python SDK additionally exports `trace_tool_fn(name, fn)`, a helper that instruments a raw tool function once at registration time instead of wrapping each call inline. This is useful when handing plain functions to a tool registry (for example, an ADK function-calling loop) where you want every invocation traced without changing the call site.

```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
from zespan.agent import trace_tool_fn

def lookup_order(order_id: str) -> dict:
    return fetch_order(order_id)

# Wrap once at registration time — every call emits a tool span automatically
traced_lookup_order = trace_tool_fn("lookup_order", lookup_order)

tools = [traced_lookup_order]
```

<ParamField body="name" type="str" required>
  Tool name recorded on the emitted span (`operation: tool.<name>`, `tools_used: [name]`).
</ParamField>

<ParamField body="fn" type="Callable" required>
  The raw function to instrument. `trace_tool_fn` returns a wrapped callable with the same signature — calling it runs `fn`, enqueues a `tool` span with latency and status, and re-raises on error.
</ParamField>

<Note>
  Because `trace_tool_fn` is not a method on an `AgentContext`, the span it emits picks up `trace_id`, `span_id`, `user_id`, `session_id`, and `tags` from whatever context is active (via `get_current_context()`) — including a surrounding `with_agent` block — but it does **not** set `agent_id`, `agent_name`, `agent_role`, or `agent_framework` on the event, even when called from inside one. Use `AgentContext.trace_tool()` if you need the tool span attributed to a specific agent.
</Note>

<Note>
  `trace_tool_fn` is Python-only — there is no equivalent in the TypeScript SDK. In TypeScript, use `agent.traceTool()` (documented above) to wrap a tool call inline inside a `withAgent` block.
</Note>
