Skip to main content
A trajectory is one run of your agent: from the user’s prompt to your final response, with every model call, tool call, retry, and planning step in between grouped together. In the dashboard, a trajectory is one row. Click into it and you see the full step-by-step. You open one with bento.begin(...) or @bento.interaction. Every track_ai and tool_span call made while it’s open joins that trajectory.

When to use what

Reach for bento.begin(...) when you have a multi-step agent run and want every step grouped under one row in the dashboard. When you make a single LLM call with no surrounding agent loop, bento.track_ai(...) is enough on its own. You don’t need begin for that case, because a bare track_ai is already a one-span trajectory.

Opening a trajectory

How children attach

Spans emitted while a trajectory is open become its children automatically. The parent context lives in a Python ContextVar, so it follows your code through:
  • Synchronous calls in the same thread
  • async/await in the same task
  • asyncio.create_task(...) (the new task inherits the active context at creation)
Context does not cross into a new threading.Thread, a ThreadPoolExecutor worker, or a subprocess. Spans emitted from those become their own root trajectories. See Threading model for the full rules and manual forwarding.

Updating mid-flight

update is additive. Existing properties are preserved unless you overwrite them by key. finish(output=, properties=) is a final update plus closing the span.

Lifecycle rules

Nested trajectories finish in reverse order. The context manager handles this for you; an out-of-order finish() raises RuntimeError. Calling finish() twice on the same trajectory is a no-op the second time. Both begin and track_ai detach from any outer OTel context, so customer FastAPI or Django spans won’t become parents of Bento spans by accident.

Span kinds inside a trajectory

A trajectory contains spans of any kind. The two the SDK emits directly:
  • LLM call (default kind): bento.track_ai(...). Carries gen_ai.* attributes.
  • Tool call: bento.tool_span(...), interaction.tool_span(...), or @bento.tool. Carries openinference.span.kind="tool".
The two track_ai spans and the tool_span all parent to the trajectory.

What a trajectory is not

  • Not a session. A session is the set of trajectories that share a convo_id. See Sessions and users.
  • Not a request. One HTTP request can open zero, one, or many trajectories.
  • Not opened by an integration. bento.instrument() captures spans inside whatever turn you’ve opened with bento.begin(...), but it never opens a trajectory on its own. You decide where each turn begins and ends.