Skip to main content
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.
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.

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.
string
required
Display name for this agent. Appears in the trace view and the agent registry. Same in both SDKs.
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.
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.
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.
string
TypeScript only. Free-form version string attached to the agent span (agent_version). There is no equivalent keyword on Python’s with_agent().
string
TypeScript only. Free-form description attached to the agent span (agent_description). There is no equivalent keyword on Python’s with_agent().
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.

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.

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

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.

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:

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

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, 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
Receiving side — extract the context on the downstream service so its spans link to the sender’s trace:
TypeScript
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.
string
Injecting side only, optional. Stored in baggage as agent.delegation.reason — why the work is being handed off to the downstream service.
string
Injecting side only, optional. Stored in baggage as agent.task — a description of the task being delegated.
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.
These same functions are re-exported as injectAutoGenContext/extractAutoGenContext for AutoGen users — they are identical functions under framework-specific names. See the AutoGen and CrewAI integration pages for how each framework uses them at the HTTP boundary.
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) 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).

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
str
required
Tool name recorded on the emitted span (operation: tool.<name>, tools_used: [name]).
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.
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.
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.