Skip to content

fix(observability): persist model usage on unattended runs - #230

Open
Caldalis wants to merge 1 commit into
OpenBMB:mainfrom
Caldalis:fix/unattended-run-usage-telemetry
Open

fix(observability): persist model usage on unattended runs#230
Caldalis wants to merge 1 commit into
OpenBMB:mainfrom
Caldalis:fix/unattended-run-usage-telemetry

Conversation

@Caldalis

Copy link
Copy Markdown

Intent

Model token usage exists only on spans and reaches the database through a ContextVar sink. Scheduled tasks, team wakeup and TL chat, public API runs, and handoff resume never bound one, so their execution spans — turn planning and task actions — were dropped. Only the post-turn memory-capture job, which binds its own sink, left any trace. Verified on a real scheduled run: main recorded two memory.capture events and nothing from the run itself.

Changes

  • Extract the sink closure in api/chat.py into observability.persist_spans.
  • Bind it at the five unattended entry points. In public_api/runs.py it binds to worker_db, the session the loop actually runs on.
  • Persist only scalar metrics for llm_call_* events, marking trimmed rows with bodies_omitted so an audit view cannot mistake "not retained" for "the model returned nothing". Other span types pass through unchanged.
  • Degrade to a no-op when tenant or session id is missing.

Risk

llm_call spans also carry the full prompt and response bodies — request_payload and request_messages measured 22 KB each on a real run, and are duplicated across the started and finished events. Unattended paths run at machine cadence and agent_events has no retention policy, so persisting bodies there would grow without bound and would retain user data indefinitely. Restricting to metrics measured a 28× reduction across eight real events while preserving every field existing consumers read — session_timings._ModelSpan needs task_frame_id / iteration / json_attempt / json_max_attempts, and the conversation-log view needs request_parameters. Row count grows too: a knowledge-retrieving run emits roughly 30 span rows at about 1 KB each, so a five-minute task costs ~8 MB per day against ~187 MB unprojected. A retention policy for agent_events is worth considering separately.

Writes to the caller's own Session and commits, matching the three existing sinks — and those call sites either commit immediately beforehand or, in the scheduled-task case, commit once per stream event, so transaction semantics are unchanged. emit_span_event already swallows sink errors, so a failed telemetry write cannot fail a run. The existing sinks are untouched.

Tests

12 cases in test_observability_span_persistence.py cover the projection

Tests

12 cases in test_observability_span_persistence.py cover the projection (metrics kept, bodies dropped, trimmed rows marked), the no-op degradation, sink release and failure isolation, and the wiring on the scheduled-task execution path — that last one verified to fail when the instrumentation is removed. Full suite 2018 passed (2006 before). ruff check clean on changed files.

Verified end to end against a real scheduled run: main recorded no execution spans for it, the branch recorded four (1.0–1.4 KB each, no bodies), the pre-existing memory.capture sink was untouched, and the conversation-log view renders the trimmed rows without error.

UI Validation

Not applicable — backend only, no route or role affected. The conversation-log view was exercised anyway to confirm trimmed rows render cleanly.

The non-streaming chat endpoint is out of scope: new sessions there need lazy session-id resolution, and it is an attended path.

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