Expand description
Translation of AgentEvent into Langfuse ingestion events.
TraceProjector is a stateful, per-session projector (isomorphic to
defect-storage’s RecordProjector). The main loop calls TraceProjector::project
for each incoming AgentEvent and returns 0..N IngestionEvents to the reporter.
§Hierarchy (one trace per turn)
trace (turn)
└── step (span) One per turn: one llm_call + the tools it triggers
├── llm_call (generation)
└── tool (span) Sibling of llm_call under the same step
└── (spawn_agent) → subagent (span)
└── step (span) Recursive isomorphic structure inside subagent
├── llm_call
└── tool / spawn_agent → subagent → ...Key point: the step span is the container for each turn. Both llm_call
(generation) and the tools triggered in that turn hang under it as siblings. This
makes the generation duration reflect pure LLM call time (it ends at
LlmCallFinished, no longer delayed to the next turn or wrapping tool execution
time); tool duration lives inside the step.
§Recursive subagent (flattened ancestor_path)
AgentEvent::Subagent carries an ancestor_path (a chain of ToolCallIds from the
top-level spawn_agent tool call to the current layer), and inner is always a leaf
event. The projector deterministically derives all span ids from this chain, so no
per-layer anchoring is needed; arbitrary depth is handled uniformly. Each subagent
layer is an independent scope (sharing the same step/gen/tool projection logic as
the top-level turn).
§ID strategy
- traceId: Generated once with
Uuid::new_v4()atTurnStarted, reused within the turn. Must not use an auto-incrementing{session}-turn-{seq}— resuming would cause id collisions. - scope prefix: Top-level =
{trace}; subagent path[A,B]={trace}-sub-A-sub-B. The subagent span’s id is its scope prefix. - step / generation / tool span id: Derived from the scope prefix + sequence
number /
ToolCallId, globally unique and deterministic — the parent of a subagent span (the tool span that spawned it) is computed directly from the path. - anchor: The only state that needs to be stored is the top-level
spawn_agenttool call id → trace_id (trace_id is random and not derivable). All subagents in the same turn share that trace_id, so only the top-level anchor is needed. - envelope id: One
Uuid::new_v4()per ingestion event, for Langfuse deduplication.
§Timestamps
AgentEvent carries no timestamps; the caller passes now (an RFC3339 string). The
projector does not read the clock itself, making it easier to test and deterministic.
Structs§
- Trace
Projector - Per-session projection state.