feat(protocol): add runtime event envelope#2252
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a formalized RuntimeEventEnvelope struct under the deepseek-protocol runtime module to standardize the schema of SSE event payloads. It adds fields such as schema_version, kind, and created_at, while supporting forward compatibility by preserving unknown fields. The TUI runtime API has been updated to use this new envelope, and comprehensive unit tests and documentation have been added. Feedback is provided regarding the serialization fallback path in runtime_event_payload, where using .expect(...) is recommended over an invalid fallback schema since serialization is infallible.
| let event_name = event.event.clone(); | ||
| let timestamp = event.timestamp.to_rfc3339(); | ||
| let schema_version = RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION; | ||
| let seq = event.seq; | ||
| let thread_id = event.thread_id.clone(); | ||
| let envelope = RuntimeEventEnvelope { | ||
| schema_version, | ||
| seq, | ||
| event: event_name.clone(), | ||
| kind: event_name, | ||
| thread_id: event.thread_id, | ||
| turn_id: event.turn_id, | ||
| item_id: event.item_id, | ||
| timestamp: timestamp.clone(), | ||
| created_at: Some(timestamp), | ||
| payload: event.payload, | ||
| extra: Default::default(), | ||
| }; | ||
| serde_json::to_value(envelope).unwrap_or_else( | ||
| |_| json!({ "schema_version": schema_version, "seq": seq, "thread_id": thread_id }), | ||
| ) |
There was a problem hiding this comment.
The fallback path in unwrap_or_else produces a JSON object that violates the RuntimeEventEnvelope schema because it lacks required fields like event, kind, timestamp, and payload. Since RuntimeEventEnvelope only contains standard serializable types and serde_json::Value, its serialization is infallible. Using .expect(...) is safer, cleaner, and avoids an unnecessary clone of thread_id.
let event_name = event.event.clone();
let timestamp = event.timestamp.to_rfc3339();
let envelope = RuntimeEventEnvelope {
schema_version: RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION,
seq: event.seq,
event: event_name.clone(),
kind: event_name,
thread_id: event.thread_id,
turn_id: event.turn_id,
item_id: event.item_id,
timestamp: timestamp.clone(),
created_at: Some(timestamp),
payload: event.payload,
extra: Default::default(),
};
serde_json::to_value(envelope).expect("RuntimeEventEnvelope serialization is infallible")| serde_json::to_value(envelope).unwrap_or_else( | ||
| |_| json!({ "schema_version": schema_version, "seq": seq, "thread_id": thread_id }), | ||
| ) |
There was a problem hiding this comment.
Silent degradation on serialization failure drops all event data
serde_json::to_value on this struct is infallible in practice (all fields are String, u64, Option<String>, Value, BTreeMap<String, Value>), but if it ever fires, the fallback silently strips event, kind, timestamp, payload, and all other fields from the response. Clients would receive a minimal three-field object with no indication that anything went wrong. At minimum this deserves a tracing::error! or debug_assert! so failures are observable.
| serde_json::to_value(envelope).unwrap_or_else( | |
| |_| json!({ "schema_version": schema_version, "seq": seq, "thread_id": thread_id }), | |
| ) | |
| serde_json::to_value(envelope).unwrap_or_else(|err| { | |
| tracing::error!(seq, %thread_id, %err, "failed to serialize RuntimeEventEnvelope; falling back to minimal payload"); | |
| json!({ "schema_version": schema_version, "seq": seq, "thread_id": thread_id }) | |
| }) |
71e1940 to
baa47a3
Compare
baa47a3 to
d102cbd
Compare
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Independent review (Devin): Schema design — sound. serde tags — no breaking change. Struct serialization is flat, so the wire shape is identical to the previous Two real issues to address before merge:
Minor: v0.8.48 overlap: PR #2256 consolidates workspace crates (14→11) and touches |
Summary
RuntimeEventEnvelopeprotocol type for runtime event streams/v1/threads/{id}/eventsSSE payloads through the shared envelope while preserving existing SSE event namesCompatibility
event,thread_id,turn_id,item_id,seq,timestamp, andpayloadfields in the JSON bodyschema_version,kind, andcreated_atfor a more stable typed-client envelopeturn_id/item_idasnullto preserve the previous thread-level event shapeValidation
cargo fmt --all -- --checkcargo test -p codewhale-protocolcargo test -p codewhale-tui runtime_api::testscargo check -p codewhale-tui -p codewhale-protocolgit diff --checkGreptile Summary
This PR introduces a versioned
RuntimeEventEnvelopestruct in thecodewhale-protocolcrate and wires it through the TUI's/v1/threads/{id}/eventsSSE endpoint, replacing an ad-hocjson!()literal with a typed, serializable struct.RuntimeEventEnvelopewithschema_version,kind,created_at, and a#[serde(flatten)]catch-allextra: BTreeMap<String, Value>for forward-compatible unknown fields; existing fields (seq,event,thread_id,turn_id,item_id,timestamp,payload) are preserved with identical semantics.runtime_event_payloadinruntime_api.rsto construct the envelope struct and serialize it (via.expect()), and expands integration tests to assert envelope field presence and shape on the SSE stream.schema_version/kind/created_atcontract inRUNTIME_API.md, explicitly noting thatschema_versionhere tracks the API envelope version independently of the runtime store schema version.Confidence Score: 5/5
Safe to merge. The change is purely additive: it introduces a new typed envelope struct, wires it into the existing SSE path, and adds comprehensive round-trip tests. Existing event names, seq, thread_id, and payload fields are all preserved unchanged.
The protocol struct is correctly typed with serde defaults, optional null serialization for turn_id/item_id, and a forward-compat BTreeMap for unknown fields — all verified by the new parity tests. The production code path always constructs the struct with well-defined values, so the serde serialization step is reliably infallible. No regressions to the existing SSE contract are introduced. All open concerns from prior review threads are pre-existing and not worsened by this revision.
No files require special attention. The two test assertions flagged in previous review threads (pinning assert_eq before a broader event-name predicate) are the only residual rough edges and do not affect the production path.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Client participant TUI as codewhale-tui runtime_api participant Store as RuntimeEventRecord store participant Proto as codewhale-protocol Client->>TUI: "GET /v1/threads/id/events?since_seq=N" TUI->>Store: fetch events since seq N Store-->>TUI: Vec of RuntimeEventRecord loop each RuntimeEventRecord TUI->>Proto: runtime_event_payload(record) note over Proto: Constructs RuntimeEventEnvelope with schema_version and kind Proto-->>TUI: serde_json Value envelope TUI->>Client: SSE event line plus data line with envelope JSON end TUI->>Client: SSE done eventReviews (4): Last reviewed commit: "test(protocol): tighten runtime envelope..." | Re-trigger Greptile