Skip to main content

axon/
execution_result.rs

1//! v2.81.0 — the flow-execution RESULT, in a module that reaches nothing.
2//!
3//! **THE THIRD INSTANCE OF THE SMELL, and the largest so far.** `AXON_VERSION`
4//! (v2.81.0) lived in the flow executor; `IngestProvenance` (v2.81.0) lived in
5//! the OOXML reader; `ServerExecutionResult` and `EnforcementSummaryWire` lived
6//! in `axon_server.rs` — 29,734 lines of `axum` router. Same shape every time: a
7//! general concept parked in the specific module that first needed it, silently
8//! chaining everything downstream to that module's dependencies.
9//!
10//! What it chained here was not a leaf. `ServerExecutionResult` is the input of
11//! [`crate::wire_envelope::FlowEnvelope::from_execution_result`] (v2.0.0), and
12//! `EnforcementSummaryWire` is threaded through `flow_dispatcher`,
13//! `flow_dispatcher::pure_shape` and `streaming_via_dispatcher` — **the core
14//! execution path**. So `axon run`, which opens no socket, could not compile
15//! without the HTTP server, for two structs that contain no HTTP: eight counters,
16//! a policy slug, and an aggregation of step names and token totals.
17//!
18//! Neither type is server-specific. `server_execute` was simply the first caller
19//! to need a place to put the answer. The name `ServerExecutionResult` is kept
20//! verbatim — it is crate-public since v2.0.0 and named by
21//! `tests/wire_envelope_integration.rs` and
22//! `tests/epistemic_ownership_integration.rs` — and `axon_server`
23//! re-exports both, so every existing call site (including `axon-enterprise`,
24//! which consumes `axon::axon_server::ServerExecutionResult`) keeps resolving.
25//!
26//! **This module must never acquire a dependency.** Its whole value is that the
27//! execution path can name its own result type without linking a web framework.
28
29use serde::Serialize;
30
31/// Server-side execution result.
32///
33/// v2.0.0 — promoted from `struct` to `pub struct` (and all fields
34/// to `pub`) so the new `crate::wire_envelope::FlowEnvelope` module
35/// can consume it as the converter input. Pre-39.b this type was
36/// internal to `axon_server`; v2.0.0 elevates it to a crate-public
37/// shape because it is the canonical input of the wire envelope
38/// builder. It is intentionally NOT part of the JSON wire (the
39/// FlowEnvelope is); it remains a runtime-internal aggregation step.
40#[derive(Debug, Clone, Serialize)]
41pub struct ServerExecutionResult {
42    pub success: bool,
43    pub flow_name: String,
44    pub source_file: String,
45    pub backend: String,
46    pub steps_executed: usize,
47    pub latency_ms: u64,
48    pub tokens_input: u64,
49    pub tokens_output: u64,
50    pub anchor_checks: usize,
51    pub anchor_breaches: usize,
52    pub errors: usize,
53    /// v2.87.0 — **the type errors, in words.**
54    ///
55    /// `server_execute` type-checks and then executes anyway ("non-fatal for
56    /// execution"), and until this field existed it **collected the diagnostics
57    /// and threw them away**: the caller received `success: false` and
58    /// `errors: 1`, with no way to learn what the 1 was. Every other consumer of
59    /// the type-checker surfaces its messages — `axon check`, `axon compile`,
60    /// `axon run`, `/deploy`, `/execute/dry-run` and `flow_inspect` all do.
61    /// This one path was the exception, and it is the path an `/v1/execute`
62    /// caller, an MCP client and a SCHEDULED DAEMON take.
63    ///
64    /// A count is not a diagnostic. It tells an adopter that their program is
65    /// wrong and refuses to say how — while the flow runs to completion and its
66    /// `persist` / `emit` / `deliver` / `notify` steps take real outward action.
67    ///
68    /// Empty on the clean path, so the wire shape is unchanged for every
69    /// program that type-checks.
70    #[serde(default, skip_serializing_if = "Vec::is_empty")]
71    pub type_errors: Vec<String>,
72    pub step_names: Vec<String>,
73    pub step_results: Vec<String>,
74    pub trace_id: u64,
75    /// v1.24.0 — Per-step stream-effect policies declared in the
76    /// source. Each entry is `(step_name, policy_slug)` where slug is
77    /// one of the closed catalog `{drop_oldest, degrade_quality,
78    /// pause_upstream, fail}`. Empty when no step in the flow declares
79    /// a `<stream:<policy>>` effect. Surfaced on the SSE
80    /// `axon.complete` wire envelope so adopters can observe the
81    /// policy is bound to runtime.
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub effect_policies: Vec<(String, String)>,
84
85    /// v1.24.0 — Per-step `EnforcementSummary` from the
86    /// `StreamPolicyEnforcer` runs. Empty in two cases:
87    ///   1. Legacy synchronous path (deleted in 33.z.e) —
88    ///      the enforcer is not run; the wire stays byte-identical
89    ///      with v1.24.0 (D4 byte-compat).
90    ///   2. Async streaming path where no step in the flow has a
91    ///      declared `<stream:<policy>>` effect — the enforcer is
92    ///      not constructed (no policy to enforce); D2 contract.
93    /// Surfaced on the SSE `axon.complete` wire envelope so adopters
94    /// can observe whether the declared policy actually fired in
95    /// production (a `drop_oldest` policy that never fires under
96    /// sustained load is a configuration smell).
97    #[serde(default, skip_serializing_if = "Vec::is_empty")]
98    pub enforcement_summaries: Vec<(String, EnforcementSummaryWire)>,
99
100    /// v1.24.0 — Closed-catalog runtime warnings. Populated
101    /// only when `server_execute_streaming` falls back to the
102    /// legacy synchronous path; carries one `axon-W002
103    /// streaming-not-supported` warning with the specific
104    /// `FallbackMode` tag identifying WHY. Empty on the happy
105    /// (async-streaming-active) path = D4 byte-compat preserved
106    /// (wire field elided when empty).
107    #[serde(default, skip_serializing_if = "Vec::is_empty")]
108    pub runtime_warnings: Vec<crate::runtime_warnings::RuntimeWarning>,
109
110    /// v2.0.0 — semantic provenance events from the runtime
111    /// walk (`retrieve:<store>`, `shield:<name>`, `mutate:<store>`,
112    /// etc.). Merged into the `FlowEnvelope.provenance_chain` by
113    /// the converter. Empty for flows with no taxonomy-participating
114    /// steps.
115    #[serde(default, skip_serializing_if = "Vec::is_empty")]
116    pub provenance_events: Vec<String>,
117
118    /// v2.0.0 — surfaced blame attribution when the flow
119    /// proceeded on degraded posture (anchor breach / shield
120    /// rejection / store breach / backend soft-fail / type mismatch).
121    /// `None` on clean happy path; the converter writes this slot
122    /// into the wire envelope's `blame_attribution` field verbatim.
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub blame_attribution: Option<crate::wire_envelope::BlameContext>,
125
126    /// v2.7.0 — per-tool epistemic envelopes (`base`, `scope`,
127    /// `confidence`) for every flow-level `use <Tool>` whose tool declares
128    /// an `epistemic:<level>` effect. Propagated from the runner's
129    /// IR-derived capture and written into
130    /// `FlowEnvelope.epistemic_envelopes` by the converter; the streaming
131    /// path derives the identical set via
132    /// `resolve_epistemic_envelopes_for_flow`. Empty (and elided from the
133    /// wire) for flows that dispatch no epistemic-annotated tool.
134    #[serde(default, skip_serializing_if = "Vec::is_empty")]
135    pub epistemic_envelopes: Vec<crate::epistemic_capture::EpistemicEnvelope>,
136
137    /// v2.15.0 — the HONEST hard-failure detail when a node's
138    /// `DispatchError` aborted the non-streaming flow (a failing
139    /// `persist`/`mutate`/`purge` store write, a backend error, etc.):
140    /// `Some("flow 'F' failed at persist into 'S': <cause>")`, naming the
141    /// failing node + the underlying cause. Byte-parity with the streaming
142    /// dispatcher's `FlowError.error` (v1.32.0/D6). `None` on the clean path;
143    /// the converter writes this slot into `FlowEnvelope.error` verbatim and
144    /// counts it as one `errors` so the wire envelope's certainty bounds to
145    /// the derived ceiling. Closes the v2.15.0 silent-abort regression (a
146    /// pre-insert store failure used to present as `success:false` + empty
147    /// result + zero diagnostic). Elided from the wire when `None`.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub error: Option<String>,
150
151    /// v2.46.0 — the run's temporal record when any step rendered a
152    /// declared `now:` (`captured_utc` + `tzdb_version` + `zones` — the
153    /// replayability triple of `time_is_an_explicit_input`, v2.27.0/v2.46.0). The
154    /// converter writes it into `FlowEnvelope.temporal_context` verbatim.
155    /// `None` — and elided from the wire — for every `now:`-less flow, so
156    /// every pre-v2.46.0 envelope stays byte-identical.
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub temporal_context: Option<crate::temporal_context::TemporalRecord>,
159}
160
161/// v1.24.0 — Wire-serializable mirror of
162/// [`crate::stream_effect_dispatcher::EnforcementSummary`] published
163/// on `axon.complete` per the D2 contract.
164///
165/// `policy_slug` is the closed-catalog slug of the policy that the
166/// enforcer ran (`drop_oldest` / `degrade_quality` / `pause_upstream`
167/// / `fail`); `pushed`/`delivered` count chunks the enforcer's input
168/// stream produced + the consumer drained respectively. The four
169/// `*_hits` / `*_blocks` / `*_overflows` counters surface
170/// policy-specific activations so adopters can verify the declared
171/// policy actually fired (D2 contract — declaration ⟺ runtime
172/// behavior).
173///
174/// All counters are `u64` so high-throughput long-running flows
175/// don't risk overflow. `failed` is set only when the enforcer's
176/// internal stream surfaced `BackpressurePolicy::Fail` overflow.
177#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
178pub struct EnforcementSummaryWire {
179    pub policy_slug: String,
180    pub chunks_pushed: u64,
181    pub chunks_delivered: u64,
182    pub drop_oldest_hits: u64,
183    pub degrade_quality_hits: u64,
184    pub pause_upstream_blocks: u64,
185    pub fail_overflows: u64,
186    pub failed: bool,
187}
188
189impl EnforcementSummaryWire {
190    /// Project from the rich internal `EnforcementSummary` (which has
191    /// `policy: Option<&'static str>`) into the wire-stable shape.
192    pub fn from_summary(
193        s: &crate::stream_effect_dispatcher::EnforcementSummary,
194    ) -> Self {
195        Self {
196            policy_slug: s.policy.unwrap_or("").to_string(),
197            chunks_pushed: s.chunks_pushed,
198            chunks_delivered: s.chunks_delivered,
199            drop_oldest_hits: s.drop_oldest_hits,
200            degrade_quality_hits: s.degrade_quality_hits,
201            pause_upstream_blocks: s.pause_upstream_blocks,
202            fail_overflows: s.fail_overflows,
203            failed: s.failed,
204        }
205    }
206}