Skip to content

fix(voice): one ChatItem serializer that carries every declared field - #2247

Open
u9g wants to merge 3 commits into
mainfrom
test/chat-item-proto-coverage
Open

fix(voice): one ChatItem serializer that carries every declared field#2247
u9g wants to merge 3 commits into
mainfrom
test/chat-item-proto-coverage

Conversation

@u9g

@u9g u9g commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Companion to livekit/agents#6756, which found the same class of bug in Python: the ChatItem-to-proto mapping was written twice per SDK, and each copy dropped whatever the other one carried.

What was wrong here

chatItemToProto in voice/remote_session.ts dropped extra and the speaking-at metrics on messages, which the telemetry path and livekit/agents both send.

The telemetry copy was worse. telemetry/traces.ts declared its own ProtoMessage, ProtoMetricsReport, ProtoChatItem and four more interfaces instead of the generated types, so nothing tied it to the schema. It carried this:

TODO: Use actual agent_session proto types once livekit/protocol v1.43.1+ is published

agents/package.json pins @livekit/protocol at ^1.50.4, so that has been unblocked for a while. With no schema link it emitted playbackLatency, which MetricsReport does not declare, and camelCase keys where livekit/agents emits proto field names — the same chat item log body, structurally different payloads.

The three commits

refactor: move proto serialization into its own moduleagents/src/proto.ts, a leaf that neither voice nor telemetry sits above. Verbatim otherwise.

fix(voice): carry every field the wire format declares — one encoder for both paths. chatItemSpanAttribute becomes a one-liner over it, structurally identical to Python's:

return encodeChatItem(item).toJson({ useProtoFieldName: true });                  // here
return MessageToDict(encode_chat_item(item), preserving_proto_field_name=True)    # livekit/agents

test(voice): fail when a declared field is dropped — the guard, below.

Telemetry payload changes

The emitted chat item log changes in four ways, all of them toward what livekit/agents already sends. Anything querying these logs on camelCase keys or on nested arguments needs updating.

before after
keys functionCall, callId, createdAt function_call, call_id, created_at
arguments / output re-parsed into nested objects JSON strings, as the wire format has them
extra values raw ({n: 1}) stringified ({n: "1"}), as the proto map requires
playbackLatency emitted gone; not a field on MetricsReport

The re-parse came in with the original upload path in #851 and was never a considered divergence — the hand-written arguments: string \| Record<string, unknown> existed to accommodate it. A schema-driven encoder has nowhere to put a parsed object.

I checked the result rather than assuming it: encoding all five saturated item types through both SDKs and diffing produces identical payloads, key sets and values.

Tests

chat_item_proto_coverage.test.ts saturates every ChatItem, encodes it, then walks the result against the message descriptor; anything left unset is a drop, unless allowlisted as a parity gap this SDK cannot fill. Since both paths now share the encoder, the guard covers telemetry too.

Two entries are allowlisted rather than fixed, because neither is an accidental drop:

  • metrics.llm_node_tps / metrics.llm_node_ttfsfeat(metrics): broadcast llm tps + ttfs agents#6373 added these in Python and they were never ported, so MetricsReport here has no field to read them from. A test asserts every allowlist entry names a real proto field, so the list cannot rot into stale excuses.
  • agent_config_update is a case on the oneof and the encoder fills it, but chatItemsToProto filters those items out of the chat context the RemoteSession sends. livekit/agents sends them.

1593 passing across 116 files; tsc --noEmit and eslint clean at each of the three commits.

Follow-up, not in this PR

playbackLatency stops being emitted here and livekit/agents never sent it. Carrying it from either SDK needs a playback_latency field in livekit/protocol.

@u9g
u9g requested a review from a team as a code owner August 9, 2026 13:29
@changeset-bot

changeset-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 012ee25

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 39 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-azure Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-krisp Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-protoface Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

devin-ai-integration[bot]

This comment was marked as resolved.

@u9g
u9g force-pushed the test/chat-item-proto-coverage branch from 6db24d2 to ddea0ef Compare August 9, 2026 14:56
u9g added 3 commits August 9, 2026 11:12
A leaf that neither voice nor telemetry sits above, so both can share one
encoder. Verbatim otherwise.
The RemoteSession encoder dropped extra and the speaking-at metrics, which
the telemetry path and livekit/agents both send. Telemetry kept its own
hand-written interfaces with no link to the schema, so it emitted
playbackLatency, which the proto does not declare, and camelCase keys where
livekit/agents emits proto field names.

Both paths now share one encoder and the telemetry log matches livekit/agents
field for field. The dropped playbackLatency needs a livekit/protocol field
before either SDK can carry it.
Every ChatItem is saturated, encoded, then walked against the message
descriptor; anything left unset is a drop, unless allowlisted as a parity
gap this SDK cannot fill.
@u9g
u9g force-pushed the test/chat-item-proto-coverage branch from ddea0ef to 012ee25 Compare August 9, 2026 15:14

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread agents/src/proto.ts
Comment on lines +39 to +42
if (metrics.llmNodeTtft !== undefined) report.llmNodeTtft = metrics.llmNodeTtft;
if (metrics.ttsNodeTtfb !== undefined) report.ttsNodeTtfb = metrics.ttsNodeTtfb;
if (metrics.e2eLatency !== undefined) report.e2eLatency = metrics.e2eLatency;
return report;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Playback latency disappears from the recorded conversation logs

The playback delay measured for each agent turn is no longer included when the conversation entry is packaged for upload (encodeMetrics at agents/src/proto.ts:27-43), so this timing value silently vanishes from the observability data that was previously sent.
Impact: Dashboards and downstream analysis lose the per-turn playback latency measurement for every session.

Field dropped when the hand-written telemetry serializer was replaced by the shared proto encoder

The removed telemetry serializer explicitly emitted playbackLatency (old agents/src/telemetry/traces.ts:653-655) into the chat.item log body. The replacement path chatItemSpanAttribute (agents/src/telemetry/traces.ts:521-523) now serializes the protobuf message produced by encodeChatItem, and encodeMetrics never assigns playbackLatency even though MetricsReport in agents/src/llm/chat_context.ts:301 still carries it (populated at agents/src/voice/agent_activity.ts:2732 and :3202).

Either the wire schema declares playback_latency — in which case this is exactly the kind of accidental drop the PR's coverage test is meant to catch and it should be set (or allowlisted with a reason) — or the schema lacks it, in which case the telemetry log loses information it used to carry and the gap should be acknowledged deliberately.

Prompt for agents
The previous telemetry serializer in agents/src/telemetry/traces.ts emitted metrics.playbackLatency for chat items; the new shared encoder encodeMetrics in agents/src/proto.ts does not, so the telemetry chat item log silently loses that value. Check whether AgentSession.MetricsReport in @livekit/protocol declares playback_latency: if it does, set it in encodeMetrics (it is seconds in MetricsReport, same as the other latency fields) so the coverage test in agents/src/voice/chat_item_proto_coverage.test.ts stays honest; if the proto does not declare it, document the loss explicitly (e.g. a comment plus an entry in the gap allowlist rationale) so the regression is a decision rather than an oversight.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread agents/src/proto.ts
Comment on lines +23 to +25
export function msToTimestamp(ms: number): Timestamp {
return Timestamp.fromDate(new Date(ms));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New shared helpers are published without the documentation the project requires

Two newly added shared helpers are exported without any documentation comment (msToTimestamp and encodeChatItem at agents/src/proto.ts:23-45), which the repository's contribution rules require for every new function.
Impact: Generated API documentation for these helpers is empty, and future callers get no guidance on units or behavior.

Rule reference

CONTRIBUTING.md: "If writing new methods/interfaces/enums/classes, document them. This project uses TypeDoc for automatic API documentation generation, and every new addition has to be properly documented." msToTimestamp in particular takes milliseconds while its callers pass seconds-based metrics scaled by 1000 (agents/src/proto.ts:30-33), which is exactly the kind of unit contract a TSDoc comment should state.

Suggested change
export function msToTimestamp(ms: number): Timestamp {
return Timestamp.fromDate(new Date(ms));
}
/**
* Convert a millisecond epoch value (`Date.now()` format) into a proto `Timestamp`.
*/
export function msToTimestamp(ms: number): Timestamp {
return Timestamp.fromDate(new Date(ms));
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@u9g u9g changed the title test(voice): guard ChatItem proto serialization against dropped fields fix(voice): one ChatItem serializer that carries every declared field Aug 9, 2026
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