axon/execution_result.rs
1//! §Fase 118.b.2 — 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//! (§118.a.1) lived in the flow executor; `IngestProvenance` (§118.b.1) 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`] (§39.b), 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 §39.b and named by
21//! `tests/fase39b_wire_envelope_integration.rs` and
22//! `tests/fase39c_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/// §Fase 39.b — 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 pub step_names: Vec<String>,
54 pub step_results: Vec<String>,
55 pub trace_id: u64,
56 /// §Fase 33.e — Per-step stream-effect policies declared in the
57 /// source. Each entry is `(step_name, policy_slug)` where slug is
58 /// one of the closed catalog `{drop_oldest, degrade_quality,
59 /// pause_upstream, fail}`. Empty when no step in the flow declares
60 /// a `<stream:<policy>>` effect. Surfaced on the SSE
61 /// `axon.complete` wire envelope so adopters can observe the
62 /// policy is bound to runtime.
63 #[serde(default, skip_serializing_if = "Vec::is_empty")]
64 pub effect_policies: Vec<(String, String)>,
65
66 /// §Fase 33.x.d — Per-step `EnforcementSummary` from the
67 /// `StreamPolicyEnforcer` runs. Empty in two cases:
68 /// 1. Legacy synchronous path (deleted in 33.z.e) —
69 /// the enforcer is not run; the wire stays byte-identical
70 /// with v1.24.0 (D4 byte-compat).
71 /// 2. Async streaming path where no step in the flow has a
72 /// declared `<stream:<policy>>` effect — the enforcer is
73 /// not constructed (no policy to enforce); D2 contract.
74 /// Surfaced on the SSE `axon.complete` wire envelope so adopters
75 /// can observe whether the declared policy actually fired in
76 /// production (a `drop_oldest` policy that never fires under
77 /// sustained load is a configuration smell).
78 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 pub enforcement_summaries: Vec<(String, EnforcementSummaryWire)>,
80
81 /// §Fase 33.x.g — Closed-catalog runtime warnings. Populated
82 /// only when `server_execute_streaming` falls back to the
83 /// legacy synchronous path; carries one `axon-W002
84 /// streaming-not-supported` warning with the specific
85 /// `FallbackMode` tag identifying WHY. Empty on the happy
86 /// (async-streaming-active) path = D4 byte-compat preserved
87 /// (wire field elided when empty).
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub runtime_warnings: Vec<crate::runtime_warnings::RuntimeWarning>,
90
91 /// §Fase 39.c.y — semantic provenance events from the runtime
92 /// walk (`retrieve:<store>`, `shield:<name>`, `mutate:<store>`,
93 /// etc.). Merged into the `FlowEnvelope.provenance_chain` by
94 /// the converter. Empty for flows with no taxonomy-participating
95 /// steps.
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub provenance_events: Vec<String>,
98
99 /// §Fase 39.c.z — surfaced blame attribution when the flow
100 /// proceeded on degraded posture (anchor breach / shield
101 /// rejection / store breach / backend soft-fail / type mismatch).
102 /// `None` on clean happy path; the converter writes this slot
103 /// into the wire envelope's `blame_attribution` field verbatim.
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub blame_attribution: Option<crate::wire_envelope::BlameContext>,
106
107 /// §Fase 55.b — per-tool epistemic envelopes (`base`, `scope`,
108 /// `confidence`) for every flow-level `use <Tool>` whose tool declares
109 /// an `epistemic:<level>` effect. Propagated from the runner's
110 /// IR-derived capture and written into
111 /// `FlowEnvelope.epistemic_envelopes` by the converter; the streaming
112 /// path derives the identical set via
113 /// `resolve_epistemic_envelopes_for_flow`. Empty (and elided from the
114 /// wire) for flows that dispatch no epistemic-annotated tool.
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
116 pub epistemic_envelopes: Vec<crate::epistemic_capture::EpistemicEnvelope>,
117
118 /// §Fase 65.F — the HONEST hard-failure detail when a node's
119 /// `DispatchError` aborted the non-streaming flow (a failing
120 /// `persist`/`mutate`/`purge` store write, a backend error, etc.):
121 /// `Some("flow 'F' failed at persist into 'S': <cause>")`, naming the
122 /// failing node + the underlying cause. Byte-parity with the streaming
123 /// dispatcher's `FlowError.error` (§37.e/D6). `None` on the clean path;
124 /// the converter writes this slot into `FlowEnvelope.error` verbatim and
125 /// counts it as one `errors` so the wire envelope's certainty bounds to
126 /// the derived ceiling. Closes the §65.E.2 silent-abort regression (a
127 /// pre-insert store failure used to present as `success:false` + empty
128 /// result + zero diagnostic). Elided from the wire when `None`.
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub error: Option<String>,
131
132 /// §Fase 91.b — the run's temporal record when any step rendered a
133 /// declared `now:` (`captured_utc` + `tzdb_version` + `zones` — the
134 /// replayability triple of `time_is_an_explicit_input`, §71/§91). The
135 /// converter writes it into `FlowEnvelope.temporal_context` verbatim.
136 /// `None` — and elided from the wire — for every `now:`-less flow, so
137 /// every pre-§91 envelope stays byte-identical.
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub temporal_context: Option<crate::temporal_context::TemporalRecord>,
140}
141
142/// §Fase 33.x.d — Wire-serializable mirror of
143/// [`crate::stream_effect_dispatcher::EnforcementSummary`] published
144/// on `axon.complete` per the D2 contract.
145///
146/// `policy_slug` is the closed-catalog slug of the policy that the
147/// enforcer ran (`drop_oldest` / `degrade_quality` / `pause_upstream`
148/// / `fail`); `pushed`/`delivered` count chunks the enforcer's input
149/// stream produced + the consumer drained respectively. The four
150/// `*_hits` / `*_blocks` / `*_overflows` counters surface
151/// policy-specific activations so adopters can verify the declared
152/// policy actually fired (D2 contract — declaration ⟺ runtime
153/// behavior).
154///
155/// All counters are `u64` so high-throughput long-running flows
156/// don't risk overflow. `failed` is set only when the enforcer's
157/// internal stream surfaced `BackpressurePolicy::Fail` overflow.
158#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
159pub struct EnforcementSummaryWire {
160 pub policy_slug: String,
161 pub chunks_pushed: u64,
162 pub chunks_delivered: u64,
163 pub drop_oldest_hits: u64,
164 pub degrade_quality_hits: u64,
165 pub pause_upstream_blocks: u64,
166 pub fail_overflows: u64,
167 pub failed: bool,
168}
169
170impl EnforcementSummaryWire {
171 /// Project from the rich internal `EnforcementSummary` (which has
172 /// `policy: Option<&'static str>`) into the wire-stable shape.
173 pub fn from_summary(
174 s: &crate::stream_effect_dispatcher::EnforcementSummary,
175 ) -> Self {
176 Self {
177 policy_slug: s.policy.unwrap_or("").to_string(),
178 chunks_pushed: s.chunks_pushed,
179 chunks_delivered: s.chunks_delivered,
180 drop_oldest_hits: s.drop_oldest_hits,
181 degrade_quality_hits: s.degrade_quality_hits,
182 pause_upstream_blocks: s.pause_upstream_blocks,
183 fail_overflows: s.fail_overflows,
184 failed: s.failed,
185 }
186 }
187}