tests: trace-shape schema and checker - #2501
Conversation
|
0a979d8 to
473c53d
Compare
473c53d to
70effd6
Compare
70effd6 to
1bb26cb
Compare
1bb26cb to
378dc23
Compare
378dc23 to
434f47e
Compare
434f47e to
0d1517e
Compare
0d1517e to
b2c5e8d
Compare
2268d2d to
f064b33
Compare
0daa94a to
92a97ea
Compare
b6eebad to
289b916
Compare
81f697d to
f0c186a
Compare
f0c186a to
aa7c911
Compare
aa7c911 to
cf82e5b
Compare
fe43d72 to
c776963
Compare
c776963 to
472c7d6
Compare
| * downloaded from LiveKit Cloud, so the same rules check a unit test and a real run: | ||
| * | ||
| * ``` | ||
| * pnpm exec tsx agents/src/telemetry/testing/trace_schema.ts path/to/traces.json |
There was a problem hiding this comment.
I don't think we install tsx automatically.
472c7d6 to
dd49427
Compare
dd49427 to
5e57bdb
Compare
There was a problem hiding this comment.
Devin Review found 2 new potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
5e57bdb to
59471a7
Compare
59471a7 to
c47581e
Compare
c47581e to
8d1afc6
Compare
Port of livekit/agents#7148. The nesting of spans is an emergent property of many call sites; a refactor can move a span under the wrong parent while every existing test passes, because each asserts one edge. agents/src/telemetry/testing/trace_schema.ts writes the rules down once: - SPAN_PARENTS: for every span the JS framework emits, the parents it may have (ROOT for none, ANY for spans that follow their caller). Unknown names are violations, so a new span must be registered. - MAY_OUTLIVE_PARENT: the child/parent edges where the child may end after its parent, each with its reason. Everything else must sit inside its parent, with 2 ms of slack. - checkTrace(): one trace id, every parent present and allowed, bounds, one agent_turn per lk.speech_id, lk.generation_count equal to the speech's own generation events, every eou_wait with an outcome. - fromReadableSpans / fromOtlpJson: an in-memory exporter or an export downloaded from LiveKit Cloud, same rules. As a CLI (`pnpm exec tsx agents/src/telemetry/testing/trace_schema.ts x.json`) it prints the span summary and the violations, tolerating orphans in a partial export. assertTraceWellFormed() now ends the full-session tests: the tool call and plain reply in agent_turn_span, the barge-in and handoff in coverage_spans, the hook and redaction sessions in eou_wait_span, the lifecycle and SIP sessions in session_lifecycle_span. The module is test support: imported by tests only, not exported from the package, and free of framework imports so the CLI runs on the source file. Schema differences from Python, all from what the JS code emits: answering_machine_detection is JS's name for `amd`; JS has no llm_fallback_adapter / tts_fallback_adapter / tts_stream_adapter spans (its adapters emit the plain request spans, which nest under the attempt's *_request_run), no wait_for_video_track, no judge_evaluation; rpc_handler may be a root, since without a session the SDK dispatches on a context carrying no span. Test hardening: the eou_wait full-session helper decided the turn 20 ms after the fake STT final was due, so a loaded host could open a second user turn (the intermittent failure seen in full-suite runs); the endpointing delay now leaves a 170 ms margin. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…etrics - EXTERNAL allowed parent: a session started inside an integrator's own span (which the session inherits) passes, whether that span is in the export or not; unknown spans with nothing of ours above them are the application's, unknown spans under ours are still reported - realtime_metrics may end after realtime_inference: the provider reports usage after the response Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
8d1afc6 to
474081a
Compare
Port of livekit/agents#7148. Stacked on #2500.
Description
The nesting of spans is an emergent property of many call sites. A refactor can move a span under the wrong parent while every existing test passes, because each test asserts the one edge it was written for. This layer writes the rules down once and checks every span in every full-session test against them, and applies the same rules to a real run's export.
agents/src/telemetry/testing/trace_schema.ts(test support, not exported from the package):SPAN_PARENTS: for every span name the JS framework emits, the parents it may have (ROOTfor none,ANYfor spans that follow their caller:rpc_call,event_loop_blocked). An unknown name is a violation, so a new span must be registered.MAY_OUTLIVE_PARENT: the child/parent edges where the child may end after its parent, each with its reason (the startup spanssession.start()does not wait for,on_enter, and stalls whose end is one heartbeat late). Everything else must sit inside its parent, with 2 ms of slack.checkTrace(): one trace id, every parent present, parent allowed by the schema, bounds, and the per-turn invariants: oneagent_turnperlk.speech_id,lk.generation_countequal to the number ofgenerationevents, everyeou_waitwith an outcome.fromReadableSpansfor an in-memory exporter,fromOtlpJsonfor an export downloaded from LiveKit Cloud. As a CLI,pnpm exec tsx agents/src/telemetry/testing/trace_schema.ts traces.jsonprints the span summary and the violations, tolerating orphans in a partial export.assertTraceWellFormed(exporter.getFinishedSpans())now ends the full-session tests of the earlier layers:agent_turn_span,coverage_spans,eou_wait_span,session_lifecycle_span.Schema differences from Python
All from what the JS code emits:
answering_machine_detectionis JS's name foramd; JS has nollm_fallback_adapter/tts_fallback_adapter/tts_stream_adapterspans (its adapters emit the plain request spans, which nest under the attempt's*_request_run), nowait_for_video_track, nojudge_evaluation;rpc_handlermay be a root, since without a session the SDK dispatches on a context carrying no span.MAY_OUTLIVE_PARENTis identical to Python's.Testing
telemetry/testing/trace_schema.test.ts(9 tests): schema self-consistency; a sound trace passes; a misparentedllm_request, aneou_detectionoutside its wait, an unknown span, a missing parent, a child outside its parent, a duplicated speech turn, a generation-count mismatch and a second trace id are each reported; the deliberate overruns pass; OTLP and in-memory sources agree; a full fake session is well-formed.eou_waitfull-session helper decided the turn 20 ms after the fake STT final was due, which under load opened a second user turn; the endpointing delay now leaves a 170 ms margin.agentssuite green.Review follow-ups
lk.generation_countis checked against everygenerationevent on the turn, matching feat(telemetry): one agent_turn span per speech handle #2500 where the count is the turn's and includes a discarded preemptive attempt's generation. The handoff itself is still validated separately: generations after the lastpreemptive_generation_discardedevent must carry the finishing speech's id (a realtime tool reply continues the tool call's id, so it passes).0 traces), not a sound trace.generationevent withoutlk.generation_idis a violation wherever it sits on the turn, a discarded attempt's before the handoff included: the framework always stamps one. Only the ownership check (the id is the finishing speech's) is limited to generations after the last handoff.🤖 Generated with Claude Code