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
//! v2.81.0 — the flow-execution RESULT, in a module that reaches nothing.
//!
//! **THE THIRD INSTANCE OF THE SMELL, and the largest so far.** `AXON_VERSION`
//! (v2.81.0) lived in the flow executor; `IngestProvenance` (v2.81.0) lived in
//! the OOXML reader; `ServerExecutionResult` and `EnforcementSummaryWire` lived
//! in `axon_server.rs` — 29,734 lines of `axum` router. Same shape every time: a
//! general concept parked in the specific module that first needed it, silently
//! chaining everything downstream to that module's dependencies.
//!
//! What it chained here was not a leaf. `ServerExecutionResult` is the input of
//! [`crate::wire_envelope::FlowEnvelope::from_execution_result`] (v2.0.0), and
//! `EnforcementSummaryWire` is threaded through `flow_dispatcher`,
//! `flow_dispatcher::pure_shape` and `streaming_via_dispatcher` — **the core
//! execution path**. So `axon run`, which opens no socket, could not compile
//! without the HTTP server, for two structs that contain no HTTP: eight counters,
//! a policy slug, and an aggregation of step names and token totals.
//!
//! Neither type is server-specific. `server_execute` was simply the first caller
//! to need a place to put the answer. The name `ServerExecutionResult` is kept
//! verbatim — it is crate-public since v2.0.0 and named by
//! `tests/wire_envelope_integration.rs` and
//! `tests/epistemic_ownership_integration.rs` — and `axon_server`
//! re-exports both, so every existing call site (including `axon-enterprise`,
//! which consumes `axon::axon_server::ServerExecutionResult`) keeps resolving.
//!
//! **This module must never acquire a dependency.** Its whole value is that the
//! execution path can name its own result type without linking a web framework.
use serde::Serialize;
/// Server-side execution result.
///
/// v2.0.0 — promoted from `struct` to `pub struct` (and all fields
/// to `pub`) so the new `crate::wire_envelope::FlowEnvelope` module
/// can consume it as the converter input. Pre-39.b this type was
/// internal to `axon_server`; v2.0.0 elevates it to a crate-public
/// shape because it is the canonical input of the wire envelope
/// builder. It is intentionally NOT part of the JSON wire (the
/// FlowEnvelope is); it remains a runtime-internal aggregation step.
#[derive(Debug, Clone, Serialize)]
pub struct ServerExecutionResult {
pub success: bool,
pub flow_name: String,
pub source_file: String,
pub backend: String,
pub steps_executed: usize,
pub latency_ms: u64,
pub tokens_input: u64,
pub tokens_output: u64,
pub anchor_checks: usize,
pub anchor_breaches: usize,
pub errors: usize,
/// v2.87.0 — **the type errors, in words.**
///
/// `server_execute` type-checks and then executes anyway ("non-fatal for
/// execution"), and until this field existed it **collected the diagnostics
/// and threw them away**: the caller received `success: false` and
/// `errors: 1`, with no way to learn what the 1 was. Every other consumer of
/// the type-checker surfaces its messages — `axon check`, `axon compile`,
/// `axon run`, `/deploy`, `/execute/dry-run` and `flow_inspect` all do.
/// This one path was the exception, and it is the path an `/v1/execute`
/// caller, an MCP client and a SCHEDULED DAEMON take.
///
/// A count is not a diagnostic. It tells an adopter that their program is
/// wrong and refuses to say how — while the flow runs to completion and its
/// `persist` / `emit` / `deliver` / `notify` steps take real outward action.
///
/// Empty on the clean path, so the wire shape is unchanged for every
/// program that type-checks.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub type_errors: Vec<String>,
pub step_names: Vec<String>,
pub step_results: Vec<String>,
pub trace_id: u64,
/// v1.24.0 — Per-step stream-effect policies declared in the
/// source. Each entry is `(step_name, policy_slug)` where slug is
/// one of the closed catalog `{drop_oldest, degrade_quality,
/// pause_upstream, fail}`. Empty when no step in the flow declares
/// a `<stream:<policy>>` effect. Surfaced on the SSE
/// `axon.complete` wire envelope so adopters can observe the
/// policy is bound to runtime.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effect_policies: Vec<(String, String)>,
/// v1.24.0 — Per-step `EnforcementSummary` from the
/// `StreamPolicyEnforcer` runs. Empty in two cases:
/// 1. Legacy synchronous path (deleted in 33.z.e) —
/// the enforcer is not run; the wire stays byte-identical
/// with v1.24.0 (D4 byte-compat).
/// 2. Async streaming path where no step in the flow has a
/// declared `<stream:<policy>>` effect — the enforcer is
/// not constructed (no policy to enforce); D2 contract.
/// Surfaced on the SSE `axon.complete` wire envelope so adopters
/// can observe whether the declared policy actually fired in
/// production (a `drop_oldest` policy that never fires under
/// sustained load is a configuration smell).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enforcement_summaries: Vec<(String, EnforcementSummaryWire)>,
/// v1.24.0 — Closed-catalog runtime warnings. Populated
/// only when `server_execute_streaming` falls back to the
/// legacy synchronous path; carries one `axon-W002
/// streaming-not-supported` warning with the specific
/// `FallbackMode` tag identifying WHY. Empty on the happy
/// (async-streaming-active) path = D4 byte-compat preserved
/// (wire field elided when empty).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub runtime_warnings: Vec<crate::runtime_warnings::RuntimeWarning>,
/// v2.0.0 — semantic provenance events from the runtime
/// walk (`retrieve:<store>`, `shield:<name>`, `mutate:<store>`,
/// etc.). Merged into the `FlowEnvelope.provenance_chain` by
/// the converter. Empty for flows with no taxonomy-participating
/// steps.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub provenance_events: Vec<String>,
/// v2.0.0 — surfaced blame attribution when the flow
/// proceeded on degraded posture (anchor breach / shield
/// rejection / store breach / backend soft-fail / type mismatch).
/// `None` on clean happy path; the converter writes this slot
/// into the wire envelope's `blame_attribution` field verbatim.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blame_attribution: Option<crate::wire_envelope::BlameContext>,
/// v2.7.0 — per-tool epistemic envelopes (`base`, `scope`,
/// `confidence`) for every flow-level `use <Tool>` whose tool declares
/// an `epistemic:<level>` effect. Propagated from the runner's
/// IR-derived capture and written into
/// `FlowEnvelope.epistemic_envelopes` by the converter; the streaming
/// path derives the identical set via
/// `resolve_epistemic_envelopes_for_flow`. Empty (and elided from the
/// wire) for flows that dispatch no epistemic-annotated tool.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub epistemic_envelopes: Vec<crate::epistemic_capture::EpistemicEnvelope>,
/// v2.15.0 — the HONEST hard-failure detail when a node's
/// `DispatchError` aborted the non-streaming flow (a failing
/// `persist`/`mutate`/`purge` store write, a backend error, etc.):
/// `Some("flow 'F' failed at persist into 'S': <cause>")`, naming the
/// failing node + the underlying cause. Byte-parity with the streaming
/// dispatcher's `FlowError.error` (v1.32.0/D6). `None` on the clean path;
/// the converter writes this slot into `FlowEnvelope.error` verbatim and
/// counts it as one `errors` so the wire envelope's certainty bounds to
/// the derived ceiling. Closes the v2.15.0 silent-abort regression (a
/// pre-insert store failure used to present as `success:false` + empty
/// result + zero diagnostic). Elided from the wire when `None`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
/// v2.46.0 — the run's temporal record when any step rendered a
/// declared `now:` (`captured_utc` + `tzdb_version` + `zones` — the
/// replayability triple of `time_is_an_explicit_input`, v2.27.0/v2.46.0). The
/// converter writes it into `FlowEnvelope.temporal_context` verbatim.
/// `None` — and elided from the wire — for every `now:`-less flow, so
/// every pre-v2.46.0 envelope stays byte-identical.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temporal_context: Option<crate::temporal_context::TemporalRecord>,
}
/// v1.24.0 — Wire-serializable mirror of
/// [`crate::stream_effect_dispatcher::EnforcementSummary`] published
/// on `axon.complete` per the D2 contract.
///
/// `policy_slug` is the closed-catalog slug of the policy that the
/// enforcer ran (`drop_oldest` / `degrade_quality` / `pause_upstream`
/// / `fail`); `pushed`/`delivered` count chunks the enforcer's input
/// stream produced + the consumer drained respectively. The four
/// `*_hits` / `*_blocks` / `*_overflows` counters surface
/// policy-specific activations so adopters can verify the declared
/// policy actually fired (D2 contract — declaration ⟺ runtime
/// behavior).
///
/// All counters are `u64` so high-throughput long-running flows
/// don't risk overflow. `failed` is set only when the enforcer's
/// internal stream surfaced `BackpressurePolicy::Fail` overflow.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct EnforcementSummaryWire {
pub policy_slug: String,
pub chunks_pushed: u64,
pub chunks_delivered: u64,
pub drop_oldest_hits: u64,
pub degrade_quality_hits: u64,
pub pause_upstream_blocks: u64,
pub fail_overflows: u64,
pub failed: bool,
}
impl EnforcementSummaryWire {
/// Project from the rich internal `EnforcementSummary` (which has
/// `policy: Option<&'static str>`) into the wire-stable shape.
pub fn from_summary(
s: &crate::stream_effect_dispatcher::EnforcementSummary,
) -> Self {
Self {
policy_slug: s.policy.unwrap_or("").to_string(),
chunks_pushed: s.chunks_pushed,
chunks_delivered: s.chunks_delivered,
drop_oldest_hits: s.drop_oldest_hits,
degrade_quality_hits: s.degrade_quality_hits,
pause_upstream_blocks: s.pause_upstream_blocks,
fail_overflows: s.fail_overflows,
failed: s.failed,
}
}
}