diff --git a/CHANGELOG.md b/CHANGELOG.md index f515755..e16187d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,11 +24,14 @@ adheres to [Semantic Versioning](https://semver.org/). the wire format: `usage.cacheRead`/`cacheWrite`/`totalTokens`, `errorMessage`, and `providerMetadata`. Session files and `save_messages` blobs written by older versions still load (`serde` aliases accept the old - names); files **written** by 0.13 use the new names, so they are not - readable by yoagent < 0.13. The full nested payload shape (message, - content blocks, usage) is now frozen by an exact-JSON snapshot test. -- `serde` minimum version is now `1.0.181` (needed for `rename_all_fields` - on the event wire format). Released July 2023; no practical impact. + names). Files **written** by 0.13 load in older versions *without error*, + but the renamed fields are silently dropped there: cache/total token + counts read as 0, and `errorMessage`/`providerMetadata` — including + Gemini thought signatures — are lost. Don't round-trip session files + through yoagent < 0.13. The full nested payload shape (message, content + blocks, usage) is frozen by an exact-JSON snapshot test. +- `serde` minimum version is now `1.0.177` (the release that added + `rename_all_fields`, July 2023); no practical impact. ## 0.12.0 diff --git a/Cargo.toml b/Cargo.toml index 1ef5d55..b4434fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,8 @@ exclude = ["docs/images/*", "docs/theme/*", ".github/*", "scripts/*"] tokio = { version = "1", features = ["rt", "sync", "fs", "macros", "process", "time", "io-util"] } tokio-util = "0.7" tokio-stream = "0.1" -# 1.0.181 floor: rename_all_fields on the AgentEvent wire format -serde = { version = "1.0.181", features = ["derive"] } +# 1.0.177 floor: the release that added rename_all_fields (AgentEvent wire format) +serde = { version = "1.0.177", features = ["derive"] } serde_json = "1" async-trait = "0.1" thiserror = "2" diff --git a/docs/concepts/messages-events.md b/docs/concepts/messages-events.md index f58057d..5ab1b53 100644 --- a/docs/concepts/messages-events.md +++ b/docs/concepts/messages-events.md @@ -148,10 +148,14 @@ This shape is a **public contract** frozen by snapshot tests — variant tags, field names, and the tagging scheme won't change in minor releases. Streaming semantics: clients accumulate text from each `MessageUpdate`'s -`delta`; the `message` field during streaming is a placeholder (its `content` -fills in only when the message completes). The full message arrives with -`MessageEnd` — a client that misses events (e.g. a lagged websocket -subscriber) resyncs from the next `MessageEnd` without replay. +`delta`; the `message` field during streaming is an empty-content +placeholder (the complete message arrives as a new value in `MessageEnd`). +Reset accumulation on each `MessageStart` — after a transient provider +error the stream restarts from a fresh `MessageStart` with no closing +`MessageEnd` for the abandoned attempt, so a client that doesn't reset +duplicates the replayed text. A client that misses events entirely (e.g. a +lagged websocket subscriber) resyncs from the next `MessageEnd` without +replay. ## StreamDelta diff --git a/docs/concepts/persistence.md b/docs/concepts/persistence.md index e0240b3..d74679f 100644 --- a/docs/concepts/persistence.md +++ b/docs/concepts/persistence.md @@ -50,12 +50,16 @@ Messages serialize as a JSON array. Each message is tagged by role: "stopReason": "stop", "model": "claude-sonnet-5", "provider": "anthropic", - "usage": {"input": 100, "output": 50, "cache_read": 0, "cache_write": 0, "total_tokens": 150}, + "usage": {"input": 100, "output": 50, "cacheRead": 0, "cacheWrite": 0, "totalTokens": 150}, "timestamp": 1700000001000 } ] ``` +As of 0.13 all field names are camelCase; the pre-0.13 snake_case names +(`cache_read`, `cache_write`, `total_tokens`, `error_message`, +`provider_metadata`) are still accepted when loading older files. + Extension messages use a nested structure: ```json diff --git a/tests/serialization_test.rs b/tests/serialization_test.rs index 1a82c0b..453ce02 100644 --- a/tests/serialization_test.rs +++ b/tests/serialization_test.rs @@ -376,7 +376,9 @@ fn test_stream_delta_every_variant_roundtrips() { /// The frozen `"type"` tag for every `AgentEvent` variant. Exhaustive match /// with NO wildcard arm on purpose: adding a variant fails to compile here -/// until its wire tag is pinned (and a sample added to `all_agent_events`). +/// until its wire tag is pinned. Keep [`EVENT_VARIANT_COUNT`] and the sample +/// in `all_agent_events` in step — the count assertion below fails until the +/// new variant is actually exercised, closing the other half of the freeze. /// A tag change is a breaking change for wire clients — do not edit casually. fn expected_event_tag(event: &AgentEvent) -> &'static str { match event { @@ -404,9 +406,19 @@ fn expected_delta_tag(delta: &StreamDelta) -> &'static str { } } +/// Number of arms in `expected_event_tag` — bump together with the match. +const EVENT_VARIANT_COUNT: usize = 12; + #[test] fn test_agent_event_type_tags_are_frozen() { - for event in all_agent_events() { + let events = all_agent_events(); + assert_eq!( + events.len(), + EVENT_VARIANT_COUNT, + "a variant was added to expected_event_tag without a sample in all_agent_events — \ + the new variant's tag and round-trip are untested until one is added" + ); + for event in events { let v: serde_json::Value = serde_json::to_value(&event).expect("serialize"); assert_eq!( v["type"],