1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
//! v1.24.0 — Closed-catalog runtime warnings for the streaming
//! production path.
//!
//! D5 contract: when the production async streaming path can't
//! activate (flow shape unsupported, backend unknown, etc.), the
//! runtime emits a closed-catalog warning instead of silently
//! degrading. Adopters see the warning on `axon.complete.warnings`
//! AND on the `/v1/replay/<trace_id>` audit row, so a `Stream<T>`
//! declaration that falls through to legacy synchronous-burst
//! delivery is OBSERVABLE — never silent.
//!
//! # Closed-catalog discipline
//!
//! `WarningCode` is a closed enum. Adding a new variant requires:
//! 1. Updating the enum here (compiler enforces exhaustive `slug`
//! match).
//! 2. Updating the slug-uniqueness pin in
//! [`closed_catalog_pins`].
//! 3. Updating the warning-surface drift gate in
//! `tests/warning_catalog.rs`.
//!
//! The "axon-W001" slot is reserved for the X-Axon-Stream-Available
//! HTTP-header diagnostic that shipped in v1.22.0 (kept as a
//! header-level surface rather than a wire-body warning to preserve
//! legacy adopter parsers). 33.x.g introduces "axon-W002" as the
//! first wire-body-surfaced warning code.
//!
//! # Pillar trace
//!
//! - **MATHEMATICS** — closed catalog ⟹ exhaustive match ⟹ adding
//! a new code breaks the build. Compiler enforces.
//! - **LOGIC** — every legacy-fallback case has a specific
//! [`FallbackMode`] tag. No "unknown" catch-all that hides
//! real edge cases.
//! - **PHILOSOPHY** — no silent degradation. An adopter who declares
//! `Stream<T>` and ends up with synthetic chunking sees the
//! warning on the wire + on the audit row.
//! - **COMPUTING** — warnings ride a side-channel on
//! `StreamingExecution`; the consumer reads them at FlowComplete
//! and projects onto the wire JSON. Zero cost on the happy path
//! (empty Vec, elided via `skip_serializing_if`).
use serde::{Deserialize, Serialize};
/// Closed catalog of runtime warning codes that can surface on the
/// production SSE wire.
///
/// As of 33.x.g there is exactly one wire-body code: `AxonW002`.
/// W001 was reserved by v1.22.0 but ships as the
/// `X-Axon-Stream-Available` HTTP header (header-level diagnostic),
/// NOT as a wire-body warning. Future codes (W003+) require an
/// explicit founder sign-off and a closed-catalog drift gate
/// update — adding a variant here is not silent.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WarningCode {
/// `axon-W002 streaming-not-supported` — emitted when the
/// adopter's flow declared `output: Stream<T>` but the
/// production async streaming path could not activate, and
/// the runtime fell back to the legacy synchronous-burst
/// delivery. The accompanying [`FallbackMode`] tag captures
/// the specific reason (flow shape unsupported, backend
/// unknown, etc.).
///
/// Wire surface: `axon.complete.warnings[*]` (D4-compatible
/// optional array, elided when empty).
/// Audit surface: `replay.runtime_warnings[*]` on the
/// `AxonendpointReplayEntry`.
#[serde(rename = "axon-W002")]
AxonW002,
}
impl WarningCode {
/// Stable wire slug. Closed catalog — adding a variant above
/// requires updating this match (compiler enforces
/// exhaustiveness).
pub fn slug(&self) -> &'static str {
match self {
Self::AxonW002 => "axon-W002",
}
}
/// Human-readable summary surfaced on the wire alongside the
/// slug. The accompanying `FallbackMode` provides the
/// machine-readable detail.
pub fn message(&self) -> &'static str {
match self {
Self::AxonW002 => "streaming-not-supported",
}
}
}
/// Closed catalog of "why streaming did not activate" tags. Each
/// tag corresponds to a specific decision point in
/// `server_execute_streaming`. Adding a variant requires updating
/// the catalog pin tests.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FallbackMode {
// v1.24.0 — `UnsupportedFlowShape` variant DELETED. The
// per-IRFlowNode dispatcher (v1.24.0 45/45) covers every shape
// the planner could previously reject; W002 cannot fire for
// "unsupported flow shape" because no shape is unsupported.
/// `resolve_streaming_backend` returned `None` for the
/// requested backend name (after `auto` resolution). The
/// dispatcher's BackendError surfaces as `axon.error`.
UnknownBackend,
/// Source could not be parsed (lex / parse / type-check / IR-
/// generation error). The dispatcher's compilation-error path
/// surfaces the diagnostic via `axon.error`.
SourceCompilationFailed,
/// Reserved for future scenarios where a custom adopter-
/// provided backend implements `Backend::complete()` but not
/// `Backend::stream()`. Not reachable today (all 8 dispatched
/// backends implement `stream()` per v1.18.0 + v1.24.0); kept
/// here so the catalog covers the conceptual case adopters
/// hit when extending the registry.
BackendLacksStream,
}
impl FallbackMode {
/// Stable kebab-case slug for wire serialization. Mirrors
/// `serde(rename_all = "snake_case")` but exposed
/// programmatically for adopters that don't go through serde.
pub fn slug(&self) -> &'static str {
match self {
Self::UnknownBackend => "unknown_backend",
Self::SourceCompilationFailed => "source_compilation_failed",
Self::BackendLacksStream => "backend_lacks_stream",
}
}
}
/// One runtime warning entry. Immutable once minted. Surfaces on
/// `axon.complete.warnings[*]` (wire) and
/// `replay.runtime_warnings[*]` (audit).
///
/// # Required fields (per D5 + plan vivo section 1)
///
/// - `code` — closed-catalog [`WarningCode`].
/// - `flow_name` — the flow whose streaming declaration fell back.
/// - `backend` — the backend name (post-`auto` resolution) that
/// was attempted.
/// - `fallback_mode` — the [`FallbackMode`] tag identifying WHY.
/// - `step_name` — optional step name when the warning is
/// step-scoped (e.g. one step has an unsupported feature, the
/// others are fine). `None` for flow-scoped warnings.
/// - `declared_output` — the adopter-source `output:` declaration
/// (e.g. `"Stream<Token>"`) that triggered the streaming
/// expectation. Empty when the warning is not tied to a
/// specific declaration.
/// - `message` — human-readable summary. Equals
/// `code.message()` by default; adopters can include
/// context-specific detail (e.g. the exact `IRFlowNode` variant
/// that triggered `UnsupportedFlowShape`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeWarning {
pub code: WarningCode,
pub flow_name: String,
pub backend: String,
pub fallback_mode: FallbackMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub step_name: Option<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub declared_output: String,
pub message: String,
/// Unix-millis timestamp when the warning was minted.
/// Monotonic within a flow execution; multiple warnings
/// preserve insertion order via the carrying `Vec`.
pub timestamp_ms: u64,
}
impl RuntimeWarning {
/// Construct a W002 streaming-not-supported warning with the
/// canonical message.
pub fn streaming_not_supported(
flow_name: impl Into<String>,
backend: impl Into<String>,
fallback_mode: FallbackMode,
detail: impl Into<String>,
) -> Self {
let detail = detail.into();
let message = if detail.is_empty() {
WarningCode::AxonW002.message().to_string()
} else {
format!("{}: {}", WarningCode::AxonW002.message(), detail)
};
Self {
code: WarningCode::AxonW002,
flow_name: flow_name.into(),
backend: backend.into(),
fallback_mode,
step_name: None,
declared_output: String::new(),
message,
timestamp_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0),
}
}
}
// ────────────────────────────────────────────────────────────────────
// Tests — closed-catalog pins
// ────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod closed_catalog_pins {
use super::*;
#[test]
fn warning_code_catalog_has_exactly_one_variant() {
// Compile-time evidence: pattern-match exhaustiveness +
// the test ensures any addition is intentional. Update
// this slot when adding W003+ (deliberate founder sign-
// off, NOT silent).
let all = [WarningCode::AxonW002];
assert_eq!(all.len(), 1);
}
#[test]
fn warning_code_slug_is_kebab_case_with_axon_w_prefix() {
for code in [WarningCode::AxonW002] {
let slug = code.slug();
assert!(
slug.starts_with("axon-W"),
"WarningCode slug MUST start with 'axon-W'; got {slug:?}"
);
assert!(
slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'),
"WarningCode slug MUST be ASCII alphanumeric + dash; got {slug:?}"
);
}
}
#[test]
fn warning_code_slugs_are_unique() {
let all = [WarningCode::AxonW002];
let mut slugs: Vec<&str> = all.iter().map(|c| c.slug()).collect();
slugs.sort();
let mut unique = slugs.clone();
unique.dedup();
assert_eq!(slugs.len(), unique.len(), "WarningCode slugs must be unique");
}
#[test]
fn fallback_mode_catalog_has_three_variants_post_33_z_e() {
// v1.24.0 — `UnsupportedFlowShape` retired; catalog
// shrinks from 4 to 3. The dispatcher path covers every
// IRFlowNode variant; no shape is "unsupported".
let all = [
FallbackMode::UnknownBackend,
FallbackMode::SourceCompilationFailed,
FallbackMode::BackendLacksStream,
];
assert_eq!(all.len(), 3);
}
#[test]
fn fallback_mode_slugs_are_snake_case_unique() {
let all = [
FallbackMode::UnknownBackend,
FallbackMode::SourceCompilationFailed,
FallbackMode::BackendLacksStream,
];
let mut slugs: Vec<&str> = all.iter().map(|m| m.slug()).collect();
// Snake-case predicate.
for slug in &slugs {
assert!(
slug.chars()
.all(|c| c.is_ascii_lowercase() || c == '_' || c.is_ascii_digit()),
"FallbackMode slug MUST be snake_case; got {slug:?}"
);
}
slugs.sort();
let mut unique = slugs.clone();
unique.dedup();
assert_eq!(slugs.len(), unique.len(), "FallbackMode slugs must be unique");
}
#[test]
fn streaming_not_supported_constructor_sets_canonical_message() {
let w = RuntimeWarning::streaming_not_supported(
"Chat",
"anthropic",
FallbackMode::UnknownBackend,
"",
);
assert_eq!(w.code, WarningCode::AxonW002);
assert_eq!(w.flow_name, "Chat");
assert_eq!(w.backend, "anthropic");
assert_eq!(w.fallback_mode, FallbackMode::UnknownBackend);
assert_eq!(w.message, "streaming-not-supported");
assert!(w.timestamp_ms > 0);
}
#[test]
fn streaming_not_supported_constructor_includes_detail_in_message() {
let w = RuntimeWarning::streaming_not_supported(
"Chat",
"stub",
FallbackMode::SourceCompilationFailed,
"parse: missing closing brace",
);
assert_eq!(
w.message,
"streaming-not-supported: parse: missing closing brace"
);
}
#[test]
fn runtime_warning_serializes_with_kebab_code_and_snake_fallback() {
let w = RuntimeWarning::streaming_not_supported(
"F",
"stub",
FallbackMode::UnknownBackend,
"",
);
let json = serde_json::to_value(&w).unwrap();
assert_eq!(json["code"], "axon-W002");
assert_eq!(json["fallback_mode"], "unknown_backend");
assert_eq!(json["flow_name"], "F");
assert_eq!(json["backend"], "stub");
// step_name + declared_output are elided when empty.
assert!(json.get("step_name").is_none());
assert!(json.get("declared_output").is_none());
}
#[test]
fn runtime_warning_round_trips_via_serde() {
let w = RuntimeWarning::streaming_not_supported(
"F",
"anthropic",
FallbackMode::BackendLacksStream,
"adopter's custom backend",
);
let json = serde_json::to_string(&w).unwrap();
let parsed: RuntimeWarning = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.code, WarningCode::AxonW002);
assert_eq!(parsed.flow_name, "F");
assert_eq!(parsed.backend, "anthropic");
assert_eq!(parsed.fallback_mode, FallbackMode::BackendLacksStream);
}
}