Skip to content

feat(telemetry,context): OTLP span export + structured StateSummary - #71

Merged
netty-linux merged 2 commits into
masterfrom
feat/otlp-and-state-summary
Aug 9, 2026
Merged

feat(telemetry,context): OTLP span export + structured StateSummary#71
netty-linux merged 2 commits into
masterfrom
feat/otlp-and-state-summary

Conversation

@netty-linux

Copy link
Copy Markdown
Owner

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

  • 6 new tracing tests: console still works, unrecognized value raises, idempotency across exporter switches, missing extra raises clear `ImportError`, exporter constructed with the right args (or `None` defaults for env-var fallback).
  • 4 new context-manager tests, including the one that matters: a model calling `emit_state_summary` with arguments that fail `StateSummary` validation still degrades to the structural summary, doesn't crash `compact()`.
  • All 14 pre-existing `test_context_and_spec.py` tests pass unmodified — including the one exercising the free-text fallback path, confirming it still works.
  • Full suite: 661 passed, 7 skipped, 84.83% coverage.
  • `uv run ruff check .` / `uv run ruff format --check .` / `uv run pyright` — all clean.

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.
@netty-linux
netty-linux merged commit b2a1228 into master Aug 9, 2026
11 checks passed
@netty-linux
netty-linux deleted the feat/otlp-and-state-summary branch August 9, 2026 12:10
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