Skip to content

feat(observability): nested typed spans for Langfuse agent traces - #402

Merged
delete-cloud merged 1 commit into
mainfrom
fix/agent-observability-nested-spans
May 30, 2026
Merged

delete-cloud merged 1 commit into
mainfrom
fix/agent-observability-nested-spans

Conversation

@delete-cloud

@delete-cloud delete-cloud commented May 30, 2026 •

Copy link
Copy Markdown
Owner

Problem

Agent sanitized observations were collected and exported, but rendered flat at the Unix epoch (1970) in Langfuse with no durations, no tree, and no observation types. Root cause: SpanRecords carried no start_time/end_time and no parent linkage, so the OTLP payload sent 0 timestamps and a fresh random spanId with no parentSpanId.

This brings agent traces up to the standard nested-timeline Langfuse experience (parent/child tree, real duration bars, GENERATION/TOOL types, token counts).

Changes

  • AgentKit Core (SpanRecord): two optional, provider-neutral fields span_id / parent_span_id. OTel concepts, backward compatible — a minimal model extension, not the "Core tracing rewrite" rejected in ADR-0049.
  • Exporter (OtlpHttpObservationSink): emit real spanId + parentSpanId so Langfuse stitches the observation tree; non-zero start/end times. Narrow int-only allowlist for gen_ai.usage.input_tokens / gen_ai.usage.output_tokens (the output substring would otherwise drop them; integer counts cannot carry raw text).
  • Recorder (AgentObservationRecorder): nested tree per turn —
    • root agent.turn.sanitized (type=agent, real start/end)
    • child agent.generation.sanitized (type=generation, gen_ai.usage.*, start inferred from previous activity since the runtime only surfaces usage at completion)
    • child agent.tool.sanitized (type=tool, real call→result duration)

No-leak boundary

Unchanged: raw tape stays local; langfuse.observation.input/output still gated by the sanitized projection schema; new attributes are token integers + a type enum + random span IDs.

Validation

  • 141 passed across observability suite, release contract, platform smoke, runtime, adapter, integration smoke. ruff clean, git diff --check clean.
  • New tests: nested+real-time+type+usage tree, OTLP parentSpanId/non-zero times, int usage allowed, non-int usage rejected.
  • Pre-existing unrelated mypy error (_DEFAULT_PROMETHEUS_RECORDER, present on main) is untouched.
  • ADR-0049 amended to record the Core-model extension.

Not included

Live deploy + Langfuse UI/API verification of a real session (requires a Langfuse-connected environment).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced observability with hierarchical span tracking: observations now include parent-child relationships for better tracing of agent operations.
    • Improved timing accuracy: operation start and end times are now recorded based on actual observable events.
    • Better OTLP export: span linkage is now properly included in exported telemetry data.
  • Tests

    • Added comprehensive test coverage for nested span structure and timing validation.

Review Change Stack

Agent sanitized observations rendered flat at the Unix epoch in Langfuse:
SpanRecords carried no start/end times and no parent linkage, so the UI
showed no durations, no tree, and no observation types.

- agentkit SpanRecord gains optional provider-neutral span_id/parent_span_id
  (OTel concepts, not a Core tracing rewrite per ADR-0049).
- OtlpHttpObservationSink emits real spanId + parentSpanId so Langfuse renders
  the parent/child observation tree, plus non-zero start/end times.
- Recorder emits a nested tree per turn: root agent.turn.sanitized
  (type=agent) with child generation spans (type=generation, gen_ai.usage.*
  token counts) and tool spans (type=tool) with real call->result durations.
- Narrow int-only allowlist for gen_ai.usage.input_tokens/output_tokens; the
  no-leak boundary is otherwise unchanged (raw tape stays local; input/output
  still gated by the sanitized projection schema).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 30, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Agent observation recording now emits per-turn nested span trees with span identifiers and parent/child relationships. SpanRecord gains optional span_id and parent_span_id fields; AgentObservationRecorder tracks per-turn span state and timing to emit agent.turn.sanitized (root), agent.generation.sanitized (child), and agent.tool.sanitized (child) spans; OTLP export uses deterministic span IDs and exports parent span relationships, while generation token counts are explicitly allowlisted for integer-only export.

Changes

Nested Span Tree for Agent Observations

Layer / File(s) Summary
SpanRecord Extension with Span Identifiers
coding-agent/src/agentkit/observability/models.py
SpanRecord dataclass adds optional span_id and parent_span_id fields to support provider-neutral OpenTelemetry span identification and parent/child linkage. Documentation clarifies that exporters treat missing values as trace roots.
Span State Tracking and Recording Infrastructure
coding-agent/src/coding_agent/agent_observability.py
AgentObservationRecorder is extended with per-turn state variables (_turn_span_id, _turn_start_time, _last_activity_time, _pending_tool_calls), _PendingToolCall dataclass to hold tool-call context between request and result, and new _emit_child_span helper for safe span recording; _record now returns the recorded event to support timing derivation.
Generation Span Emission on LLM Usage
coding-agent/src/coding_agent/agent_observability.py
observe_llm_usage records LLM usage event and emits agent.generation.sanitized child span with model/token metadata and timing derived from the last observable activity timestamp.
Tool Span Emission on Tool Execution
coding-agent/src/coding_agent/agent_observability.py
observe_tool_call sanitizes arguments and stores pending tool-call context; observe_tool_result retrieves pending context, emits agent.tool.sanitized child span with sanitized input/output shapes and real start/end times.
Root Turn Span Finalization at Completion
coding-agent/src/coding_agent/agent_observability.py
complete_turn records terminal turn observation and emits root agent.turn.sanitized span with generated span ID, no parent linkage, and explicit start/end times.
OTLP Exporter Integration and Token Allowlisting
coding-agent/src/coding_agent/observability.py
OTLP export now uses deterministic span IDs from SpanRecord.span_id (with random fallback), exports parentSpanId when parent linkage is provided, and introduces _GENERATION_USAGE_KEYS allowlist to permit integer-only generation token count attributes.
Test Coverage and ADR Documentation
coding-agent/tests/coding_agent/test_agent_observability.py, coding-agent/docs/adr/0049-agent-observation-recorder.md
Added RecordingSink test helper, tests verifying nested span hierarchy with correct parent/child relationships and non-epoch start/end times, OTLP payload tests validating deterministic span IDs and parent ID export, token-usage validation tests (accepting integers, rejecting non-integers), and ADR amendment documenting nested span tree design, span ID semantics, and OTLP export behavior.

Sequence Diagram

sequenceDiagram
  participant Agent
  participant AgentObservationRecorder
  participant ObservationSink
  
  Agent->>AgentObservationRecorder: start_turn()
  AgentObservationRecorder->>ObservationSink: SpanRecord(agent.turn.sanitized, span_id, parent_span_id=None)
  
  Agent->>AgentObservationRecorder: observe_llm_usage()
  AgentObservationRecorder->>ObservationSink: SpanRecord(agent.generation.sanitized, span_id, parent_span_id=turn_id)
  
  Agent->>AgentObservationRecorder: observe_tool_call()
  Agent->>AgentObservationRecorder: observe_tool_result()
  AgentObservationRecorder->>ObservationSink: SpanRecord(agent.tool.sanitized, span_id, parent_span_id=turn_id)
  
  Agent->>AgentObservationRecorder: complete_turn()
  Note over ObservationSink: Nested structure:<br/>turn (root)<br/>├─ generation (child)<br/>└─ tool (child)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • delete-cloud/Coding-Agent-Loop#385: Extends the existing sanitized span emission infrastructure from #385 by introducing per-turn nested span structure with explicit span ID and parent ID wiring through SpanRecord and OTLP export of parentSpanId.
  • delete-cloud/Coding-Agent-Loop#188: Both PRs modify the shared provider-neutral SpanRecord model in agentkit/observability/models.py, with this PR extending it to add OTLP-friendly span_id and parent_span_id fields for nested span relationships.

Poem

🐰 Hops through spans nested deep,
Turn as root, where children creep—
Generation counts the tokens true,
Tools link back with IDs too.
OTLP knows the parent's way,
Traces bloom another day! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(observability): nested typed spans for Langfuse agent traces' accurately and concisely describes the main change: implementing nested typed spans for Langfuse agent observability traces.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agent-observability-nested-spans

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@delete-cloud
delete-cloud merged commit 83f855d into main May 30, 2026
1 check was pending
@delete-cloud
delete-cloud deleted the fix/agent-observability-nested-spans branch May 30, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant