feat(telemetry,context): OTLP span export + structured StateSummary - #71
Merged
Conversation
Two independent, small fixes bundled together per the approved plan. ## OTLP exporter (nullain/telemetry/tracing.py) configure_tracing(exporter=...) previously handled "console" and silently installed NO span processor for any other value — spans were built and discarded with no error, no warning, nothing ever reaching a real backend. That was the actual bug being fixed here, not a missing feature: an unrecognized exporter value now raises ValueError instead of silently no-op'ing. "otlp" ships spans to any OTLP-compatible backend (Jaeger, Tempo, Honeycomb, ...) via HTTP/protobuf (not gRPC — no native grpcio dependency to compile/ship), batched via BatchSpanProcessor rather than one export call per span. The exporter is resolved lazily via importlib, same optional-dependency pattern as FastEmbedProvider/ QdrantStore/PostgresEventStore; new "otlp" extra in pyproject.toml. otlp_endpoint/otlp_headers/otlp_timeout default to None, letting the underlying OTel SDK's own OTEL_EXPORTER_OTLP_* env var precedence apply rather than reimplementing that logic here. 6 new tests: console exporter still works, unrecognized value raises, idempotency holds across exporter switches, missing 'otlp' extra raises a clear ImportError, and the exporter is constructed with the right endpoint/headers/timeout (or lets them default to None for env-var fallback). ## Structured StateSummary (nullain/context/manager.py) ContextManager._llm_summarize already asked the model for "key decisions, file changes, errors encountered, and outstanding work" — but only as free-text prose the caller could only treat as an opaque string. Now obtained via forced tool-calling (a synthetic emit_state_summary tool), the SAME pattern AgentLoop._generate_spec already uses for TaskSpec — not a new structured-output mechanism or a new dependency (Instructor was considered and rejected: it's built around the openai client/LiteLLM and would drag in that decision too). StateSummary is a Pydantic model with the four fields above; .render() turns it back into the same prose shape _llm_summarize used to return directly, so CompactionEvent.summary stays typed str — no ripple into events/store.py, events/repair.py, or events/conversation.py's fold logic that a typed field on CompactionEvent itself would force. Two fallback layers, preserved and extended, not replaced: a model that ignores the tool and answers in prose still works (free-text fallback, same discipline _generate_spec has); a provider failure OR a StateSummary that fails Pydantic validation both degrade to the existing honest structural summary — never propagate an exception out of compact(). _compaction_boundary (the fix for a live Ollama Cloud "400 invalid message content type" bug) is untouched. 4 new tests, including the one that matters: a model calling emit_state_summary with arguments that FAIL StateSummary validation must still degrade all the way to the structural summary, not crash compact(). All 14 pre-existing tests in test_context_and_spec.py pass unmodified, including test_context_manager_llm_summarization, which exercises the free-text fallback path (its fake provider never sets tool_calls) — confirms that path still works exactly as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Increment 4 of 4 in the approved plan (SearXNG → Crawl4AI/Wayback → Postgres batching → OTLP + StateSummary). Two small, independent fixes bundled together.
OTLP exporter
`configure_tracing(exporter=...)` previously handled `"console"` and silently installed no span processor for any other value — spans were built and discarded with no error, no warning, nothing ever reaching a real backend. That's the actual bug fixed here, not a missing feature: an unrecognized value now raises `ValueError`.
`"otlp"` ships spans to any OTLP-compatible backend (Jaeger, Tempo, Honeycomb, ...) via HTTP/protobuf — no native `grpcio` dependency — batched via `BatchSpanProcessor`. Lazy import (same pattern as `FastEmbedProvider`/`QdrantStore`/`PostgresEventStore`); new `otlp` extra. `otlp_endpoint`/`otlp_headers`/`otlp_timeout` default to `None`, letting the underlying OTel SDK's own `OTEL_EXPORTER_OTLP_*` env var precedence apply rather than reimplementing it here.
Structured StateSummary
`ContextManager._llm_summarize` already asked the model for "key decisions, file changes, errors encountered, and outstanding work" — but only as free-text prose. Now obtained via forced tool-calling (a synthetic `emit_state_summary` tool), the same pattern `AgentLoop._generate_spec` already uses for `TaskSpec` — not a new mechanism, not a new dependency (Instructor was considered and rejected: it's built around the openai client/LiteLLM and would drag in that decision too).
`StateSummary.render()` turns the structured result back into the same prose shape `_llm_summarize` used to return directly, so `CompactionEvent.summary` stays typed `str` — no ripple into `events/store.py`, `events/repair.py`, or `events/conversation.py`'s fold logic.
Two fallback layers preserved: a model ignoring the tool still gets free-text handling (same as `_generate_spec`); a provider failure or a `StateSummary` that fails Pydantic validation both degrade to the existing structural summary — never propagate out of `compact()`. `_compaction_boundary` (the fix for a live Ollama Cloud `400` bug) is untouched.
Test plan