返回 CodeWhale
fleet_reasoning.rs
根目录 / crates / workflow / src / fleet_reasoning.rs
1 //! The one requested → effective reasoning resolver, with provider capability
2 //! normalization, preserved provenance, and durable receipts that carry
3 //! **disclosure without content**.
4 //!
5 //! Models never auto-switch inside the exact Fleet experience. A worker's
6 //! provider/model is **frozen and preflighted** before this module runs;
7 //! everything here only decides how hard that already-chosen model thinks.
8 //!
9 //! Resolution order for an exact member:
10 //!
11 //! 1. A concrete requested tier resolves to itself, normalized against the
12 //! route's real capability. **No Router is called** — a manually pinned tier
13 //! costs nothing.
14 //! 2. `reasoning = "auto"` **always** goes to the Fleet's attached Reasoning
15 //! Router (see [`crate::reasoning_router`]). There is no
16 //! provider-native-adaptive bypass: a route that chooses its own depth is a
17 //! fact about how the request is *shaped*, not a reason to skip the service
18 //! the operator configured. A missing or unready Router is an error *before
19 //! work starts*, and exact Fleets never fall back to the local keyword
20 //! heuristic or to legacy model routing.
21 //!
22 //! Legacy (non-exact) callers keep the old behavior through
23 //! [`resolve_legacy_reasoning`], which is allowed to use a local heuristic.
24 //!
25 //! ## What a durable receipt may hold
26 //!
27 //! A receipt is written to journals and events that travel further than the
28 //! machine that produced them, so it holds **no task text and no routing
29 //! summary text** — only bounded counts, a truncation flag, a stable hash of
30 //! the exact transmitted bytes, what redaction removed, and whether the
31 //! inference crossed provider boundaries. Everything else on it is an id, a
32 //! model string, a tier label, or a boolean.
33
34 use serde::{Deserialize, Serialize};
35 use thiserror::Error;
36
37 use crate::fleet_exact::{FrozenRoute, ReasoningTier, RequestedReasoning};
38 use crate::fleet_preflight::{EndpointIdentity, PreflightedRoute};
39 use crate::reasoning_router::{
40 CapturedReasoningRouter, REASONING_ROUTER_SERVICE_KIND, RouterCallReasoning,
41 };
42 use crate::redaction::redact_for_disclosure;
43
44 /// How much reasoning control a provider/model route *actually* expresses on
45 /// the wire.
46 ///
47 /// This is the distinction that keeps a receipt honest. A selector tier and a
48 /// provider-effective control are different things: Z.AI's GLM routes only ever
49 /// emit `thinking = {"type": "enabled"}` or `{"type": "disabled"}`, so
50 /// requesting `high` and requesting `max` produce a byte-identical request.
51 /// Presenting those as two distinct provider-effective tiers would be a claim
52 /// the wire does not support. Routes that genuinely vary a `reasoning_effort`
53 /// value per tier are [`Self::Tiers`].
54 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55 #[serde(rename_all = "snake_case")]
56 pub enum ProviderReasoningControl {
57 /// The route accepts no thinking payload at all.
58 None,
59 /// The route can express only "think" / "do not think". Distinct requested
60 /// tiers above `off` collapse to the same provider-effective control.
61 EnabledDisabled,
62 /// The route expresses distinct tiers on the wire.
63 Tiers,
64 /// The route always chooses its own depth and ignores the requested tier.
65 /// Only set this from a source-backed provider behavior.
66 NativeAdaptive,
67 }
68
69 impl ProviderReasoningControl {
70 #[must_use]
71 pub const fn as_str(self) -> &'static str {
72 match self {
73 Self::None => "none",
74 Self::EnabledDisabled => "enabled_disabled",
75 Self::Tiers => "tiers",
76 Self::NativeAdaptive => "native_adaptive",
77 }
78 }
79 }
80
81 /// What a provider/model route can truthfully do with reasoning.
82 ///
83 /// [`ProviderReasoningControl::NativeAdaptive`] is deliberately opt-in: it must
84 /// only be set for a route that genuinely lets the provider choose its own
85 /// thinking depth, established from the request-shaping source rather than
86 /// asserted here.
87 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88 pub struct ReasoningCapability {
89 /// How much control the route actually expresses.
90 pub control: ProviderReasoningControl,
91 /// Lowest tier the route can actually run (always-thinking routes cannot
92 /// honor `off`).
93 pub min_tier: Option<ReasoningTier>,
94 /// Highest tier the route can actually run.
95 pub max_tier: Option<ReasoningTier>,
96 /// The tier the route *actually* expresses for each requested tier, in
97 /// `[off, low, medium, high, max]` order.
98 ///
99 /// `min_tier`/`max_tier` can only describe a floor and a ceiling. Real
100 /// routes also **collapse interior tiers**: CodeWhale's own route
101 /// normalizer coerces `low` and `medium` to `high` on every non-Codex
102 /// route while leaving `off` alone, which is a hole rather than a clamp and
103 /// is therefore inexpressible as min/max. Recording the map is what keeps
104 /// `effective` and `provider_effective` describing the request that was
105 /// actually made instead of the tier the selector merely named — a receipt
106 /// that says `low` for a request that carried `high` is exactly the
107 /// invisible substitution this type exists to prevent.
108 ///
109 /// `None` means the route expresses every requested tier faithfully.
110 /// `serde(default)` keeps preflights written before this field readable.
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub wire_tiers: Option<[ReasoningTier; 5]>,
113 }
114
115 /// Index of a tier in a [`ReasoningCapability::wire_tiers`] map.
116 const fn tier_index(tier: ReasoningTier) -> usize {
117 match tier {
118 ReasoningTier::Off => 0,
119 ReasoningTier::Low => 1,
120 ReasoningTier::Medium => 2,
121 ReasoningTier::High => 3,
122 ReasoningTier::Max => 4,
123 }
124 }
125
126 /// The identity map: every requested tier reaches the wire unchanged.
127 pub const FAITHFUL_WIRE_TIERS: [ReasoningTier; 5] = [
128 ReasoningTier::Off,
129 ReasoningTier::Low,
130 ReasoningTier::Medium,
131 ReasoningTier::High,
132 ReasoningTier::Max,
133 ];
134
135 impl ReasoningCapability {
136 /// A route with no reasoning support at all.
137 #[must_use]
138 pub const fn none() -> Self {
139 Self {
140 control: ProviderReasoningControl::None,
141 min_tier: None,
142 max_tier: None,
143 wire_tiers: None,
144 }
145 }
146
147 /// A route with ordinary off..max tiers and no native adaptive mode.
148 #[must_use]
149 pub const fn tiered() -> Self {
150 Self {
151 control: ProviderReasoningControl::Tiers,
152 min_tier: None,
153 max_tier: None,
154 wire_tiers: None,
155 }
156 }
157
158 /// A route whose only provider-effective control is thinking on/off — the
159 /// Z.AI GLM shape. Requested tiers are still recorded; they simply do not
160 /// become distinct provider-effective tiers.
161 #[must_use]
162 pub const fn enabled_disabled() -> Self {
163 Self {
164 control: ProviderReasoningControl::EnabledDisabled,
165 min_tier: None,
166 max_tier: None,
167 wire_tiers: None,
168 }
169 }
170
171 /// A route that truthfully performs provider-native adaptive thinking.
172 #[must_use]
173 pub const fn native_adaptive() -> Self {
174 Self {
175 control: ProviderReasoningControl::NativeAdaptive,
176 min_tier: None,
177 max_tier: None,
178 wire_tiers: None,
179 }
180 }
181
182 /// Record what each requested tier actually becomes on the wire.
183 ///
184 /// The identity map is stored as `None`, so a faithful route never carries
185 /// a redundant table and never reports a normalization it did not perform.
186 #[must_use]
187 pub fn with_wire_tiers(mut self, wire_tiers: [ReasoningTier; 5]) -> Self {
188 self.wire_tiers = (wire_tiers != FAITHFUL_WIRE_TIERS).then_some(wire_tiers);
189 self
190 }
191
192 /// What the requested tier becomes on the wire, before floor/ceiling
193 /// clamping. Identity for a route that expresses every tier faithfully.
194 #[must_use]
195 pub fn wire_tier(&self, tier: ReasoningTier) -> ReasoningTier {
196 self.wire_tiers.map_or(tier, |wire| wire[tier_index(tier)])
197 }
198
199 /// Whether the route accepts any thinking payload at all.
200 #[must_use]
201 pub const fn supports_thinking(&self) -> bool {
202 !matches!(self.control, ProviderReasoningControl::None)
203 }
204
205 /// Whether the route performs provider-native adaptive thinking.
206 #[must_use]
207 pub const fn supports_native_adaptive(&self) -> bool {
208 matches!(self.control, ProviderReasoningControl::NativeAdaptive)
209 }
210
211 /// Resolve a requested tier into what the route can actually run. Returns
212 /// the tier and whether normalization changed it.
213 ///
214 /// The wire map is applied **before** the floor/ceiling clamps: a route
215 /// that collapses `low` onto `high` has already decided what leaves the
216 /// host, and a clamp cannot undo that. Any movement is reported, so the
217 /// caller records `capability_normalized` rather than presenting the
218 /// requested tier as the one that ran.
219 #[must_use]
220 pub fn normalize(&self, tier: ReasoningTier) -> (ReasoningTier, bool) {
221 if !self.supports_thinking() {
222 return (ReasoningTier::Off, tier != ReasoningTier::Off);
223 }
224 let mut effective = self.wire_tier(tier);
225 if let Some(min) = self.min_tier
226 && effective < min
227 {
228 effective = min;
229 }
230 if let Some(max) = self.max_tier
231 && effective > max
232 {
233 effective = max;
234 }
235 (effective, effective != tier)
236 }
237
238 /// The control the provider actually receives for a selected tier.
239 #[must_use]
240 pub const fn provider_effective(&self, tier: ReasoningTier) -> ProviderEffectiveReasoning {
241 match self.control {
242 ProviderReasoningControl::None => ProviderEffectiveReasoning::Disabled,
243 ProviderReasoningControl::EnabledDisabled => match tier {
244 ReasoningTier::Off => ProviderEffectiveReasoning::Disabled,
245 _ => ProviderEffectiveReasoning::Enabled,
246 },
247 ProviderReasoningControl::Tiers => ProviderEffectiveReasoning::Tier(tier),
248 ProviderReasoningControl::NativeAdaptive => ProviderEffectiveReasoning::NativeAdaptive,
249 }
250 }
251 }
252
253 /// What the provider actually ends up being asked for, as distinct from the
254 /// tier the selector picked.
255 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256 #[serde(rename_all = "snake_case", tag = "kind", content = "tier")]
257 pub enum ProviderEffectiveReasoning {
258 /// Thinking is off (or unsupported) on the wire.
259 Disabled,
260 /// Thinking is on, and the route cannot express a depth. A receipt must
261 /// not upgrade this to a tier label.
262 Enabled,
263 /// The route expresses this exact tier on the wire.
264 Tier(ReasoningTier),
265 /// The provider chooses its own depth.
266 NativeAdaptive,
267 }
268
269 impl ProviderEffectiveReasoning {
270 #[must_use]
271 pub const fn label(self) -> &'static str {
272 match self {
273 Self::Disabled => "disabled",
274 Self::Enabled => "enabled",
275 Self::Tier(tier) => tier.as_str(),
276 Self::NativeAdaptive => "native_adaptive",
277 }
278 }
279 }
280
281 // ── Router call reasoning: configured, visible, and cheap ───────────────────
282
283 /// The cheapest reasoning a Router call falls back to when nothing else is
284 /// configured. A Router profile may raise this to `low` — and no further.
285 pub const ROUTER_CALL_REASONING: RouterCallReasoning = RouterCallReasoning::Off;
286
287 /// Everything a receipt needs to say about *the Router's own call*.
288 ///
289 /// Four separate facts, because collapsing them is how a receipt starts lying:
290 /// what the operator configured, what the selector landed on after
291 /// normalization, how much control the Router's route actually expresses, and
292 /// what the provider was therefore told. A Router configured `low` on a route
293 /// that supports `low` is called at `low` and says so — this type exists so
294 /// that "forced to `off` while displaying `low`" is not expressible.
295 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296 pub struct RouterCallDisclosure {
297 /// What the Router profile asked for: `off` or `low`.
298 pub requested: String,
299 /// The tier the selector landed on after capability normalization.
300 pub effective: String,
301 /// How much reasoning control the Router's own route expresses.
302 pub provider_control: String,
303 /// What the Router's provider is actually told.
304 pub provider_effective: String,
305 /// Whether the route's real capability moved the requested tier.
306 #[serde(default)]
307 pub capability_normalized: bool,
308 }
309
310 impl RouterCallDisclosure {
311 /// The compact receipt form.
312 #[must_use]
313 pub fn receipt(&self) -> String {
314 format!(
315 "router_call_requested={} router_call_effective={} router_call_provider_control={} \
316 router_call_provider_effective={}",
317 self.requested, self.effective, self.provider_control, self.provider_effective,
318 )
319 }
320 }
321
322 /// The tier a Router call is actually made at, plus the disclosure for it.
323 #[derive(Debug, Clone, PartialEq, Eq)]
324 pub struct RouterCallPlan {
325 /// The concrete tier to place on the Router request.
326 pub tier: ReasoningTier,
327 /// The four-sided story, for the receipt.
328 pub disclosure: RouterCallDisclosure,
329 }
330
331 /// Decide what a Router call runs at, given what the operator configured and
332 /// what the Router's own route can express.
333 ///
334 /// The configured value is honored wherever the route can express it. It is
335 /// only moved by a *capability* fact — an always-thinking route that cannot
336 /// honor `off` gets its own floor — and that move is recorded, never hidden.
337 #[must_use]
338 pub fn router_call_plan(
339 requested: RouterCallReasoning,
340 capability: &ReasoningCapability,
341 ) -> RouterCallPlan {
342 let (tier, capability_normalized) = capability.normalize(requested.tier());
343 RouterCallPlan {
344 tier,
345 disclosure: RouterCallDisclosure {
346 requested: requested.as_str().to_string(),
347 effective: tier.as_str().to_string(),
348 provider_control: capability.control.as_str().to_string(),
349 provider_effective: capability.provider_effective(tier).label().to_string(),
350 capability_normalized,
351 },
352 }
353 }
354
355 /// The exact identity of the Reasoning Router service that decided a tier.
356 ///
357 /// A receipt carries this so "who chose this tier, and what did that cost"
358 /// is answerable without re-reading any file. It is explicitly labelled as a
359 /// **service**, not a Fleet member: `service_kind` is always
360 /// [`REASONING_ROUTER_SERVICE_KIND`] and `dispatchable` is always false.
361 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362 pub struct RouterIdentity {
363 /// The Router's id: a saved profile name, or a legacy inline member id.
364 pub id: String,
365 /// Origin the definition came from, or `legacy_inline`.
366 #[serde(default = "legacy_origin")]
367 pub origin: String,
368 /// Always `reasoning_router`. Present so a receipt states what kind of
369 /// thing chose the tier rather than leaving a reader to infer it.
370 #[serde(default = "service_kind", alias = "role")]
371 pub service_kind: String,
372 /// True when this Router was written inline in the Fleet file.
373 #[serde(default)]
374 pub legacy_inline: bool,
375 /// The Router's exact configured provider id.
376 pub provider: String,
377 /// The Router's canonical wire model.
378 pub model: String,
379 /// Where the Router's own request goes.
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub endpoint: Option<EndpointIdentity>,
382 /// What the Router's own call was configured to, and actually ran at.
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub call: Option<RouterCallDisclosure>,
385 }
386
387 fn service_kind() -> String {
388 REASONING_ROUTER_SERVICE_KIND.to_string()
389 }
390
391 fn legacy_origin() -> String {
392 crate::reasoning_router::LEGACY_INLINE_ROUTER_ORIGIN.to_string()
393 }
394
395 impl RouterIdentity {
396 /// Build an identity from the captured service and its preflighted route.
397 #[must_use]
398 pub fn from_captured(
399 captured: &CapturedReasoningRouter,
400 route: Option<&PreflightedRoute>,
401 call: Option<RouterCallDisclosure>,
402 ) -> Self {
403 Self {
404 id: captured.id.clone(),
405 origin: captured.origin.clone(),
406 service_kind: captured.service_kind.clone(),
407 legacy_inline: captured.legacy_inline,
408 provider: route.map_or_else(
409 || captured.route.provider.clone(),
410 |route| route.provider_id.clone(),
411 ),
412 model: route.map_or_else(
413 || captured.route.model.clone(),
414 |route| route.wire_model.clone(),
415 ),
416 endpoint: route.map(|route| route.endpoint.clone()),
417 call,
418 }
419 }
420
421 /// A minimal identity for a Router whose route was supplied directly.
422 #[must_use]
423 pub fn new(provider: impl Into<String>, model: impl Into<String>) -> Self {
424 Self {
425 id: "router".to_string(),
426 origin: legacy_origin(),
427 service_kind: service_kind(),
428 legacy_inline: true,
429 provider: provider.into(),
430 model: model.into(),
431 endpoint: None,
432 call: None,
433 }
434 }
435
436 /// `origin/id` — the stable qualified form.
437 #[must_use]
438 pub fn qualified(&self) -> String {
439 format!("{}/{}", self.origin, self.id)
440 }
441
442 /// The compact receipt form, which names the service kind explicitly so a
443 /// reader is never left guessing whether a Fleet member did this.
444 #[must_use]
445 pub fn label(&self) -> String {
446 format!(
447 "{}:{} {}/{}",
448 self.service_kind,
449 self.qualified(),
450 self.provider,
451 self.model
452 )
453 }
454 }
455
456 /// Whether an exact Fleet actually has a Router it can call right now.
457 #[derive(Debug, Clone, PartialEq, Eq)]
458 pub enum RouterAvailability {
459 /// The Fleet references no Reasoning Router.
460 Absent,
461 /// A Router is referenced but cannot be called (profile not found, no
462 /// credentials, route does not resolve, …). Decided locally.
463 Unavailable { reason: String },
464 /// A Router is referenced and ready.
465 Ready,
466 }
467
468 /// The reasoning a request actually runs with.
469 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
470 #[serde(rename_all = "snake_case", tag = "kind", content = "tier")]
471 pub enum EffectiveReasoning {
472 /// A concrete tier placed on the request.
473 Tier(ReasoningTier),
474 /// The provider chooses its own depth; no tier is placed on the request.
475 NativeAdaptive,
476 }
477
478 impl EffectiveReasoning {
479 #[must_use]
480 pub fn label(self) -> &'static str {
481 match self {
482 Self::Tier(tier) => tier.as_str(),
483 Self::NativeAdaptive => "native_adaptive",
484 }
485 }
486
487 /// The concrete tier, if one was chosen.
488 #[must_use]
489 pub const fn tier(self) -> Option<ReasoningTier> {
490 match self {
491 Self::Tier(tier) => Some(tier),
492 Self::NativeAdaptive => None,
493 }
494 }
495 }
496
497 /// Where the effective reasoning came from. Provenance is preserved alongside
498 /// the request so a receipt can show both.
499 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
500 #[serde(rename_all = "snake_case")]
501 pub enum EffectiveReasoningSource {
502 /// The member named a concrete tier. No Router was called.
503 MemberExplicit,
504 /// The route performs its own adaptive thinking and no Router was called.
505 ///
506 /// **No longer produced.** The native-adaptive bypass was removed: `auto`
507 /// in an exact Fleet always asks the Fleet's Router. The variant is kept so
508 /// journals and events written before that change still deserialize.
509 ProviderNativeAdaptive,
510 /// The attached Reasoning Router decided the tier for a frozen route.
511 FleetRouter,
512 /// Legacy `reasoning_effort = "auto"` outside exact Fleets.
513 LegacyHeuristic,
514 /// Inherited from the session/parent.
515 SessionInherited,
516 }
517
518 impl EffectiveReasoningSource {
519 #[must_use]
520 pub const fn as_str(self) -> &'static str {
521 match self {
522 Self::MemberExplicit => "member_explicit",
523 Self::ProviderNativeAdaptive => "provider_native_adaptive",
524 Self::FleetRouter => "fleet_router",
525 Self::LegacyHeuristic => "legacy_heuristic",
526 Self::SessionInherited => "session_inherited",
527 }
528 }
529 }
530
531 /// A resolved reasoning decision that keeps every side of the story: what the
532 /// member asked for, which tier the selector landed on, what the provider is
533 /// actually able to be told, and where the decision came from.
534 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
535 pub struct ResolvedReasoning {
536 requested: RequestedReasoning,
537 effective: EffectiveReasoning,
538 provider_control: ProviderReasoningControl,
539 provider_effective: ProviderEffectiveReasoning,
540 source: EffectiveReasoningSource,
541 capability_normalized: bool,
542 /// The Router that decided this tier, when one did. `default` keeps older
543 /// serialized decisions (which had no such field) readable.
544 #[serde(default, skip_serializing_if = "Option::is_none")]
545 router: Option<RouterIdentity>,
546 }
547
548 impl ResolvedReasoning {
549 fn new(
550 requested: RequestedReasoning,
551 effective: EffectiveReasoning,
552 capability: &ReasoningCapability,
553 source: EffectiveReasoningSource,
554 capability_normalized: bool,
555 ) -> Self {
556 let provider_effective = match effective {
557 EffectiveReasoning::Tier(tier) => capability.provider_effective(tier),
558 EffectiveReasoning::NativeAdaptive => ProviderEffectiveReasoning::NativeAdaptive,
559 };
560 Self {
561 requested,
562 effective,
563 provider_control: capability.control,
564 provider_effective,
565 source,
566 capability_normalized,
567 router: None,
568 }
569 }
570
571 fn with_router(mut self, router: RouterIdentity) -> Self {
572 self.router = Some(router);
573 self
574 }
575
576 /// The Router that chose this tier, if the decision came from one.
577 #[must_use]
578 pub fn router(&self) -> Option<&RouterIdentity> {
579 self.router.as_ref()
580 }
581
582 #[must_use]
583 pub const fn requested(&self) -> RequestedReasoning {
584 self.requested
585 }
586
587 /// The tier the selector landed on. This is a CodeWhale-side selector
588 /// value; it is not automatically what the provider is told.
589 #[must_use]
590 pub const fn effective(&self) -> EffectiveReasoning {
591 self.effective
592 }
593
594 /// How much reasoning control the route actually expresses.
595 #[must_use]
596 pub const fn provider_control(&self) -> ProviderReasoningControl {
597 self.provider_control
598 }
599
600 /// What the provider is actually asked for. On an enabled/disabled route
601 /// (Z.AI GLM) both `high` and `max` land here as `enabled` — a receipt must
602 /// report this, not the selector tier, as the provider-effective control.
603 #[must_use]
604 pub const fn provider_effective(&self) -> ProviderEffectiveReasoning {
605 self.provider_effective
606 }
607
608 #[must_use]
609 pub const fn source(&self) -> EffectiveReasoningSource {
610 self.source
611 }
612
613 /// Whether the route's real capability changed the requested tier.
614 #[must_use]
615 pub const fn capability_normalized(&self) -> bool {
616 self.capability_normalized
617 }
618
619 /// A truthful one-line receipt: requested → selected → what the provider
620 /// can actually be told, plus the Router that decided it when one did.
621 #[must_use]
622 pub fn receipt(&self) -> String {
623 let mut line = format!(
624 "requested={} selected={} provider_control={} provider_effective={} source={}",
625 self.requested.as_str(),
626 self.effective.label(),
627 self.provider_control.as_str(),
628 self.provider_effective.label(),
629 self.source.as_str(),
630 );
631 if let Some(router) = &self.router {
632 line.push_str(&format!(" router={}", router.label()));
633 if let Some(call) = &router.call {
634 line.push(' ');
635 line.push_str(&call.receipt());
636 }
637 }
638 line
639 }
640 }
641
642 // ── Routing summary: transmitted once, disclosed without content ────────────
643
644 /// Character ceiling on the task text handed to a Router.
645 ///
646 /// A Router decides one thing — how hard to think — and a few hundred
647 /// characters of task shape is enough for that. Bounding it keeps the routing
648 /// call cheap and bounds how much of a task's content leaves for the Router's
649 /// provider, which may be a different provider than the worker's.
650 pub const ROUTER_SUMMARY_MAX_CHARS: usize = 600;
651
652 /// Scope label recorded on a disclosure: what class of content was sent.
653 pub const ROUTING_SCOPE: &str = "bounded_redacted_task_shape";
654
655 /// A coarse, host-derived shape label for a task.
656 ///
657 /// This is the "minimal task classification" a routing payload may carry. It is
658 /// computed from the already-redacted summary and is deliberately crude: the
659 /// Router needs to know roughly what kind of work this is, not what the work
660 /// says.
661 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
662 #[serde(rename_all = "snake_case")]
663 pub enum TaskShape {
664 /// Reading, inspecting, summarizing.
665 Read,
666 /// Editing, implementing, fixing.
667 Edit,
668 /// Debugging, diagnosing, root-causing.
669 Diagnose,
670 /// Nothing distinctive.
671 Unclassified,
672 }
673
674 impl TaskShape {
675 #[must_use]
676 pub const fn as_str(self) -> &'static str {
677 match self {
678 Self::Read => "read",
679 Self::Edit => "edit",
680 Self::Diagnose => "diagnose",
681 Self::Unclassified => "unclassified",
682 }
683 }
684
685 /// Classify from bounded, already-redacted text.
686 #[must_use]
687 pub fn classify(text: &str) -> Self {
688 let lowered = text.to_ascii_lowercase();
689 let has = |needles: &[&str]| needles.iter().any(|needle| lowered.contains(needle));
690 if has(&[
691 "debug",
692 "why does",
693 "root cause",
694 "failing",
695 "flake",
696 "crash",
697 ]) {
698 Self::Diagnose
699 } else if has(&[
700 "edit",
701 "implement",
702 "refactor",
703 "fix",
704 "add ",
705 "rewrite",
706 "migrate",
707 ]) {
708 Self::Edit
709 } else if has(&["read", "review", "summarize", "audit", "inspect", "explain"]) {
710 Self::Read
711 } else {
712 Self::Unclassified
713 }
714 }
715 }
716
717 /// Everything a **durable** record may say about what was sent to a Router.
718 ///
719 /// Note what is absent: the text. A receipt states how much left, whether it
720 /// was cut, what it hashes to, what redaction removed, and whether it crossed a
721 /// provider boundary — never the content itself.
722 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
723 pub struct RoutingDisclosure {
724 /// Bytes actually transmitted.
725 #[serde(default)]
726 pub transmitted_bytes: usize,
727 /// Characters actually transmitted.
728 #[serde(default)]
729 pub transmitted_chars: usize,
730 /// Characters the sanitized, redacted text had before truncation.
731 #[serde(default)]
732 pub original_chars: usize,
733 /// Whether the text was cut to fit [`ROUTER_SUMMARY_MAX_CHARS`].
734 #[serde(default)]
735 pub truncated: bool,
736 /// `sha256:<hex>` over the exact transmitted bytes. Stable, and reveals
737 /// nothing about the content.
738 #[serde(default)]
739 pub content_hash: String,
740 /// Whether redaction removed anything.
741 #[serde(default)]
742 pub redacted: bool,
743 /// Which classes of content redaction removed — never the content.
744 #[serde(default)]
745 pub redactions: Vec<String>,
746 /// What class of content was in scope to send at all.
747 #[serde(default)]
748 pub scope: String,
749 /// The coarse task shape that was included.
750 #[serde(default)]
751 pub task_shape: String,
752 /// Whether this summary went to a provider other than the worker's.
753 #[serde(default)]
754 pub cross_provider_inference: bool,
755 }
756
757 impl RoutingDisclosure {
758 /// One-line disclosure for a receipt.
759 #[must_use]
760 pub fn receipt(&self) -> String {
761 format!(
762 "routing_summary_bytes={} chars={} truncated={} hash={} redacted={} \
763 cross_provider={}",
764 self.transmitted_bytes,
765 self.transmitted_chars,
766 self.truncated,
767 self.content_hash,
768 self.redacted,
769 self.cross_provider_inference,
770 )
771 }
772 }
773
774 /// The bounded payload actually handed to a Router, plus its disclosure.
775 ///
776 /// The text is **private and transient**: [`Self::text`] hands it to the
777 /// transport, [`Self::disclosure`] is what may be persisted. The type makes it
778 /// awkward to accidentally durable-write the content, which is the point.
779 #[derive(Debug, Clone, PartialEq, Eq)]
780 pub struct RoutingPayload {
781 text: String,
782 disclosure: RoutingDisclosure,
783 }
784
785 impl RoutingPayload {
786 /// The exact bytes to transmit. Sent **once** — see
787 /// [`router_user_message`].
788 #[must_use]
789 pub fn text(&self) -> &str {
790 &self.text
791 }
792
793 /// The durable, content-free disclosure.
794 #[must_use]
795 pub fn disclosure(&self) -> &RoutingDisclosure {
796 &self.disclosure
797 }
798
799 /// Consume the payload, keeping only what may be persisted.
800 #[must_use]
801 pub fn into_disclosure(self) -> RoutingDisclosure {
802 self.disclosure
803 }
804
805 /// Stamp whether this payload crossed a provider boundary. Known by the
806 /// caller (which holds the preflight), not by this module.
807 #[must_use]
808 pub fn with_cross_provider(mut self, cross_provider: bool) -> Self {
809 self.disclosure.cross_provider_inference = cross_provider;
810 self
811 }
812 }
813
814 /// Bound, sanitize, and redact task text into the payload a Router receives.
815 ///
816 /// Four things happen, in order:
817 ///
818 /// 1. Control characters (including newlines) collapse to spaces and runs of
819 /// whitespace collapse to one, so the task cannot restructure the prompt it
820 /// is embedded in.
821 /// 2. Wrapper/fence sequences a router prompt uses structurally — backtick
822 /// fences and brace-JSON — are neutralized, so task text cannot close the
823 /// prompt's own framing or present itself as the answer object.
824 /// 3. **Absolute paths and secret-shaped tokens are removed**, and the fact is
825 /// recorded. Neither has any business reaching a routing service, and
826 /// neither may be persisted next to one.
827 /// 4. The result is cut to [`ROUTER_SUMMARY_MAX_CHARS`] characters, and the cut
828 /// is recorded rather than hidden.
829 #[must_use]
830 pub fn bounded_routing_payload(task: &str) -> RoutingPayload {
831 let mut sanitized = String::with_capacity(task.len().min(ROUTER_SUMMARY_MAX_CHARS * 2));
832 let mut pending_space = false;
833 for ch in task.chars() {
834 let mapped = match ch {
835 ch if ch.is_control() || ch.is_whitespace() => {
836 pending_space = !sanitized.is_empty();
837 continue;
838 }
839 // Fences and braces are the router prompt's own structure. Replace
840 // rather than drop, so the text stays readable and its length stays
841 // honest.
842 '`' => '\'',
843 '{' => '(',
844 '}' => ')',
845 other => other,
846 };
847 if pending_space {
848 sanitized.push(' ');
849 pending_space = false;
850 }
851 sanitized.push(mapped);
852 }
853
854 let redaction = redact_for_disclosure(&sanitized);
855 let redacted = redaction.redacted();
856 let redactions = redaction.kinds();
857 let cleaned = redaction.into_text();
858
859 let original_chars = cleaned.chars().count();
860 let truncated = original_chars > ROUTER_SUMMARY_MAX_CHARS;
861 let text = if truncated {
862 cleaned
863 .chars()
864 .take(ROUTER_SUMMARY_MAX_CHARS)
865 .collect::<String>()
866 .trim_end()
867 .to_string()
868 } else {
869 cleaned
870 };
871
872 let task_shape = TaskShape::classify(&text);
873
874 RoutingPayload {
875 disclosure: RoutingDisclosure {
876 transmitted_bytes: text.len(),
877 transmitted_chars: text.chars().count(),
878 original_chars,
879 truncated,
880 content_hash: crate::named_fleet::sha256_label(text.as_bytes()),
881 redacted,
882 redactions,
883 scope: ROUTING_SCOPE.to_string(),
884 task_shape: task_shape.as_str().to_string(),
885 cross_provider_inference: false,
886 },
887 text,
888 }
889 }
890
891 // ── Router call contract ────────────────────────────────────────────────────
892
893 /// The only thing a Reasoning Router is asked. Provider/model are inputs, not
894 /// questions: they are already frozen and are shown to the router purely as
895 /// context for how hard to think.
896 #[derive(Debug, Clone, PartialEq, Eq)]
897 pub struct RouterCallInput {
898 pub fleet: String,
899 pub member_id: String,
900 pub frozen: FrozenRoute,
901 /// The bounded, redacted payload. Constructed once by the caller and
902 /// transmitted once — the system prompt does not repeat it.
903 pub payload: RoutingPayload,
904 }
905
906 /// Output-token ceiling for a Router call. The Router answers with one small
907 /// JSON object; nothing it could legitimately say needs more room, and a tight
908 /// bound is what keeps a per-task Router call cheap.
909 pub const ROUTER_MAX_OUTPUT_TOKENS: u32 = 32;
910
911 /// System prompt for a Reasoning Router call.
912 ///
913 /// **Carries no task content.** The bounded summary is transmitted exactly once,
914 /// in the user turn ([`router_user_message`]). Duplicating it here would double
915 /// what leaves for the Router's provider while the receipt counted it once,
916 /// making the disclosed byte count a understatement of what was actually sent.
917 #[must_use]
918 pub fn router_system_prompt(input: &RouterCallInput) -> String {
919 format!(
920 "You are the reasoning router for the `{fleet}` fleet. You are a reasoning-only service, \
921 not a fleet member: the worker's provider and model are already frozen and you cannot change \
922 them, choose a different member, or alter tools or permissions.\n\
923 Worker member: {member}\n\
924 Frozen provider: {provider}\n\
925 Frozen model: {model}\n\
926 The next message is a bounded, redacted description of the task's shape. Judge only how hard the \
927 already-chosen model should think about it.\n\n\
928 Reply with exactly this JSON object and nothing else: \
929 {{\"reasoning\":\"off|low|medium|high|max\"}}. \
930 Emit one object only — no second object, no repeated key, no text before or after it. \
931 No other key is permitted — not a rationale, not an explanation, and above all not a \
932 provider, model, route, member, or fleet field. Any extra key rejects your answer and \
933 fails the run. Do not answer \"auto\".",
934 fleet = input.fleet,
935 member = input.member_id,
936 provider = input.frozen.provider,
937 model = input.frozen.model,
938 )
939 }
940
941 /// The user turn for a Router call: the bounded summary, transmitted once.
942 ///
943 /// The bytes returned here are exactly the bytes the disclosure's count and
944 /// hash describe.
945 #[must_use]
946 pub fn router_user_message(input: &RouterCallInput) -> String {
947 input.payload.text().to_string()
948 }
949
950 /// A Reasoning Router's entire output. One job, one field.
951 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
952 pub struct RouterDecision {
953 pub reasoning: ReasoningTier,
954 }
955
956 /// The one and only key a Reasoning Router may emit.
957 pub const ROUTER_REASONING_FIELD: &str = "reasoning";
958
959 /// Fields a router is never allowed to emit. Seeing any of them means the
960 /// router tried to move a frozen route, which fails the run.
961 const ROUTER_FORBIDDEN_FIELDS: &[&str] = &[
962 "provider",
963 "provider_id",
964 "provider_kind",
965 "model",
966 "model_id",
967 "wire_model",
968 "wire_model_id",
969 "route",
970 "model_route",
971 "endpoint",
972 "fleet",
973 "member",
974 "member_id",
975 "role",
976 "tools",
977 "allowed_tools",
978 "permissions",
979 ];
980
981 /// Parse a router response, rejecting anything that is not purely a reasoning
982 /// decision for the already frozen route.
983 pub fn parse_router_decision(raw: &str) -> Result<RouterDecision, RouterDecisionError> {
984 // Deliberately NOT `model_policy::repair_json_text_once`: that helper
985 // *extracts* the first valid JSON payload out of surrounding prose, which
986 // is the right behavior for a chatty content model and exactly the wrong
987 // behavior here. Silently discarding whatever followed the object is how a
988 // router that answered twice — or answered and then argued — gets read as
989 // if it had answered once. A router's contract is one object and nothing
990 // else, so only a code fence is stripped.
991 let repaired = strip_router_code_fence(raw);
992
993 // Exactly one JSON object and nothing else. `from_str` alone would accept a
994 // valid object followed by prose or by a second object, which is precisely
995 // how a chatty or self-correcting router smuggles a second answer past a
996 // strict key check. A streaming deserializer that must reach EOF is what
997 // makes "one object, nothing else" literal.
998 //
999 // The entries are collected as an ordered `Vec`, not a `Map`: `serde_json`'s
1000 // object representation silently keeps the *last* value for a duplicated
1001 // key, so `{"reasoning":"off","reasoning":"max"}` would otherwise parse as
1002 // a clean single-key answer. A router that names its one key twice has not
1003 // made one concrete choice, and this is where that is caught.
1004 let mut stream = serde_json::Deserializer::from_str(repaired).into_iter::<RouterObject>();
1005 let object = match stream.next() {
1006 Some(Ok(object)) => object,
1007 Some(Err(error)) => return Err(RouterDecisionError::Parse(error.to_string())),
1008 None => return Err(RouterDecisionError::Parse("router output was empty".into())),
1009 };
1010 let consumed = stream.byte_offset();
1011 if !repaired[consumed..].trim().is_empty() {
1012 return Err(RouterDecisionError::TrailingContent {
1013 trailing: trailing_excerpt(&repaired[consumed..]),
1014 });
1015 }
1016
1017 let entries = &object.0;
1018
1019 // Duplicate keys first: a repeated key is not one concrete choice, and the
1020 // checks below would otherwise judge only whichever copy they reached.
1021 for (index, (field, _)) in entries.iter().enumerate() {
1022 if entries[..index]
1023 .iter()
1024 .any(|(earlier, _)| earlier.eq_ignore_ascii_case(field))
1025 {
1026 return Err(RouterDecisionError::DuplicateField {
1027 field: field.clone(),
1028 });
1029 }
1030 }
1031
1032 // Strict: `reasoning` is the only key a router may emit. Route-shaped keys
1033 // are checked across the whole object first and keep their own distinct
1034 // error — "the router tried to move a frozen route" is a different failure
1035 // from "the router was chatty", and a chatty key sorting first must not
1036 // mask an attempted route mutation.
1037 if let Some((field, _)) = entries.iter().find(|(field, _)| {
1038 ROUTER_FORBIDDEN_FIELDS
1039 .iter()
1040 .any(|forbidden| field.as_str().eq_ignore_ascii_case(forbidden))
1041 }) {
1042 return Err(RouterDecisionError::RouteMutationAttempt {
1043 field: field.clone(),
1044 });
1045 }
1046 if let Some((field, _)) = entries
1047 .iter()
1048 .find(|(field, _)| field.as_str() != ROUTER_REASONING_FIELD)
1049 {
1050 return Err(RouterDecisionError::UnknownField {
1051 field: field.clone(),
1052 });
1053 }
1054
1055 let reasoning = entries
1056 .iter()
1057 .find(|(field, _)| field == ROUTER_REASONING_FIELD)
1058 .and_then(|(_, value)| value.as_str())
1059 .ok_or(RouterDecisionError::MissingReasoning)?;
1060
1061 if reasoning.trim().eq_ignore_ascii_case("auto") {
1062 return Err(RouterDecisionError::AutoReasoning);
1063 }
1064
1065 let reasoning =
1066 ReasoningTier::parse(reasoning).ok_or_else(|| RouterDecisionError::InvalidReasoning {
1067 value: reasoning.trim().to_string(),
1068 })?;
1069
1070 Ok(RouterDecision { reasoning })
1071 }
1072
1073 /// A JSON object preserved as ordered key/value pairs, duplicates included.
1074 ///
1075 /// `serde_json::Map` would collapse `{"a":1,"a":2}` to a single entry, which is
1076 /// exactly the smuggling route [`parse_router_decision`] must close.
1077 struct RouterObject(Vec<(String, serde_json::Value)>);
1078
1079 impl<'de> Deserialize<'de> for RouterObject {
1080 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1081 where
1082 D: serde::Deserializer<'de>,
1083 {
1084 struct ObjectVisitor;
1085
1086 impl<'de> serde::de::Visitor<'de> for ObjectVisitor {
1087 type Value = RouterObject;
1088
1089 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1090 formatter.write_str("a JSON object")
1091 }
1092
1093 fn visit_map<A>(self, mut map: A) -> Result<RouterObject, A::Error>
1094 where
1095 A: serde::de::MapAccess<'de>,
1096 {
1097 let mut entries = Vec::new();
1098 while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
1099 entries.push((key, value));
1100 }
1101 Ok(RouterObject(entries))
1102 }
1103 }
1104
1105 deserializer.deserialize_map(ObjectVisitor)
1106 }
1107 }
1108
1109 // ── Resolution ──────────────────────────────────────────────────────────────
1110
1111 /// Resolve reasoning for one exact Fleet member against its **frozen** route.
1112 ///
1113 /// `frozen` is taken by reference purely so the caller has to have frozen the
1114 /// route first; this function never reads or rewrites provider/model.
1115 pub fn resolve_exact_member_reasoning(
1116 member_id: &str,
1117 frozen: &FrozenRoute,
1118 requested: RequestedReasoning,
1119 capability: &ReasoningCapability,
1120 router: &RouterAvailability,
1121 decision: Option<&RouterDecision>,
1122 router_identity: Option<&RouterIdentity>,
1123 ) -> Result<ResolvedReasoning, ReasoningResolveError> {
1124 let _ = frozen;
1125
1126 if let Some(tier) = requested.tier() {
1127 // Manual reasoning uses no Router at all. Not "a Router that returns
1128 // the same answer" — no call, no cost, no cross-provider disclosure.
1129 let (effective, capability_normalized) = capability.normalize(tier);
1130 return Ok(ResolvedReasoning::new(
1131 requested,
1132 EffectiveReasoning::Tier(effective),
1133 capability,
1134 EffectiveReasoningSource::MemberExplicit,
1135 capability_normalized,
1136 ));
1137 }
1138
1139 // Auto, explicitly requested by this member. It ALWAYS goes to the Fleet's
1140 // attached Reasoning Router — there is no provider-native-adaptive bypass
1141 // and no local heuristic. A route that shapes its own thinking depth is
1142 // recorded on the receipt as a provider-effective control; it is not a
1143 // reason to skip the service the operator configured.
1144 match router {
1145 RouterAvailability::Absent => Err(ReasoningResolveError::RouterRequired {
1146 member: member_id.to_string(),
1147 reason: "this fleet references no reasoning router".to_string(),
1148 }),
1149 RouterAvailability::Unavailable { reason } => {
1150 Err(ReasoningResolveError::RouterUnavailable {
1151 member: member_id.to_string(),
1152 reason: reason.clone(),
1153 })
1154 }
1155 RouterAvailability::Ready => {
1156 let decision =
1157 decision.ok_or_else(|| ReasoningResolveError::RouterDecisionMissing {
1158 member: member_id.to_string(),
1159 })?;
1160 let identity =
1161 router_identity.ok_or_else(|| ReasoningResolveError::RouterIdentityMissing {
1162 member: member_id.to_string(),
1163 })?;
1164 let (effective, capability_normalized) = capability.normalize(decision.reasoning);
1165 Ok(ResolvedReasoning::new(
1166 requested,
1167 EffectiveReasoning::Tier(effective),
1168 capability,
1169 EffectiveReasoningSource::FleetRouter,
1170 capability_normalized,
1171 )
1172 .with_router(identity.clone()))
1173 }
1174 }
1175 }
1176
1177 /// Legacy path: `reasoning_effort = "auto"` outside an exact Fleet keeps its
1178 /// compatibility behavior and may use the caller's local heuristic.
1179 ///
1180 /// The heuristic tier is supplied by the caller (the TUI owns the keyword
1181 /// table) so this crate stays free of prompt-classification policy.
1182 #[must_use]
1183 pub fn resolve_legacy_reasoning(
1184 requested: RequestedReasoning,
1185 capability: &ReasoningCapability,
1186 heuristic_tier: ReasoningTier,
1187 ) -> ResolvedReasoning {
1188 let (tier, source) = match requested.tier() {
1189 Some(tier) => (tier, EffectiveReasoningSource::MemberExplicit),
1190 None => (heuristic_tier, EffectiveReasoningSource::LegacyHeuristic),
1191 };
1192 let (effective, capability_normalized) = capability.normalize(tier);
1193 ResolvedReasoning::new(
1194 requested,
1195 EffectiveReasoning::Tier(effective),
1196 capability,
1197 source,
1198 capability_normalized,
1199 )
1200 }
1201
1202 // ── The durable receipt ─────────────────────────────────────────────────────
1203
1204 /// The durable, visible receipt for one exact-Fleet task launch.
1205 ///
1206 /// This is the artifact that makes an exact Fleet auditable: it names the Fleet
1207 /// and the member that ran, the exact provider and **canonical wire model** they
1208 /// were frozen to, every side of the reasoning decision, and — when a Reasoning
1209 /// Router chose the tier — that service's exact identity, route, and configured
1210 /// requested-to-provider-effective call reasoning.
1211 ///
1212 /// **No task text, no summary text, no secrets, no absolute paths.** Every field
1213 /// is a non-sensitive id, model string, tier label, count, hash, or boolean. The
1214 /// Fleet is identified by qualified `origin/name` plus content hash rather than
1215 /// by where it lives on disk.
1216 ///
1217 /// Every field added after the first shipped shape carries `serde(default)`, so
1218 /// journals and events written by an older build stay readable.
1219 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1220 pub struct FleetTaskReceipt {
1221 /// Qualified Fleet identity, e.g. `workspace/glm-pair`.
1222 pub fleet: String,
1223 /// `exact` or `legacy`.
1224 #[serde(default)]
1225 pub schema_kind: String,
1226 #[serde(default)]
1227 pub schema_revision: u32,
1228 /// Content hash of the frozen snapshot this launch resolved against.
1229 #[serde(default)]
1230 pub content_hash: String,
1231 /// Fixed member id — what addresses the roster profile.
1232 pub member_id: String,
1233 /// Fixed **semantic** member role — what gates, handoffs, and records use.
1234 pub member_role: String,
1235 /// The **Runtime permission posture** selected after member resolution,
1236 /// when it is not the same string as the semantic role.
1237 ///
1238 /// These are two different facts and a receipt must not collapse them. The
1239 /// semantic role (`auditor`, `implementer`) is what an operator named and
1240 /// what gates key on; the posture (`scout`, `builder`, `verifier`,
1241 /// `custom`) is the Runtime role whose baseline policy was requested after
1242 /// selection. The live parent may narrow that baseline further, so the
1243 /// posture is not a claim about the final individual capabilities; the
1244 /// separately checked authority fingerprint records those. Displaying the
1245 /// posture where the role belongs renames the operator's member; enforcing
1246 /// an arbitrary semantic role as a Runtime policy would grant a surface
1247 /// nobody selected.
1248 ///
1249 /// `None` means the two coincide, so an unchanged receipt stays unchanged.
1250 #[serde(default, skip_serializing_if = "Option::is_none")]
1251 pub posture_role: Option<String>,
1252 /// Fingerprint of the permission envelope this launch installs on the
1253 /// child.
1254 ///
1255 /// Separate from `posture_role` on purpose, and the separation is the
1256 /// point: the posture is the *semantic* answer to "which built-in surface
1257 /// does this member run on", while the fingerprint is the *effective*
1258 /// answer to "exactly which allowlist, deny list, write authority, and
1259 /// delegation budget were installed". Two members can share a posture and
1260 /// carry different envelopes, so a receipt that recorded only the posture
1261 /// could not be checked against the child that actually ran.
1262 ///
1263 /// The spawn boundary compares this against the envelope it is about to
1264 /// construct and refuses the launch when they differ, which is what stops
1265 /// the value from being a label nobody verifies. `None` means the launch
1266 /// carried no host-derived ceiling.
1267 #[serde(default, skip_serializing_if = "Option::is_none")]
1268 pub authority_fingerprint: Option<String>,
1269 /// Exact provider the member is frozen to.
1270 pub provider: String,
1271 /// Canonical wire model. The same value the child actually spawns with.
1272 pub model: String,
1273 /// The model string as written in the saved Fleet, when it differed from
1274 /// the canonical wire form.
1275 #[serde(default, skip_serializing_if = "Option::is_none")]
1276 pub declared_model: Option<String>,
1277 /// Non-secret identity of the endpoint the worker's request goes to.
1278 #[serde(default, skip_serializing_if = "Option::is_none")]
1279 pub endpoint: Option<EndpointIdentity>,
1280 /// What the saved Fleet asked for (`auto` included).
1281 pub requested_reasoning: String,
1282 /// The tier the selector landed on.
1283 pub effective_reasoning: String,
1284 /// How much reasoning control the route actually expresses.
1285 #[serde(default)]
1286 pub provider_control: String,
1287 /// What the provider is actually told — not always the selector tier.
1288 pub provider_effective_reasoning: String,
1289 /// Where the decision came from.
1290 pub selection_source: String,
1291 /// Whether the route's real capability moved the requested tier.
1292 #[serde(default)]
1293 pub capability_normalized: bool,
1294 /// The Reasoning Router service that chose the tier, when one did.
1295 #[serde(default, skip_serializing_if = "Option::is_none")]
1296 pub router: Option<RouterIdentity>,
1297 /// Content-free disclosure of the bounded routing summary that left for
1298 /// the Router's provider. `None` when no Router was called.
1299 #[serde(default, skip_serializing_if = "Option::is_none")]
1300 pub routing_summary: Option<RoutingDisclosure>,
1301 /// Whether the member holds a model-visible network tool. This is a tool
1302 /// statement, not a transport one — see [`transport_disclosure`].
1303 #[serde(default)]
1304 pub member_network_tool: bool,
1305 /// Whether a Router on a different provider than the worker saw the
1306 /// bounded summary.
1307 #[serde(default)]
1308 pub cross_provider_inference: bool,
1309 /// Plain-language statement of what actually crosses the network.
1310 #[serde(default)]
1311 pub transport: String,
1312 }
1313
1314 /// The one honest sentence about transport that every exact-Fleet receipt
1315 /// carries, so a tool-surface fact is never read as an air-gap claim.
1316 ///
1317 /// It states three separable things and never conflates them:
1318 ///
1319 /// 1. Host-owned provider inference always crosses the network. Always.
1320 /// 2. Whether the *member* holds a model-visible network tool — which is what
1321 /// `network_tool` actually governs. A member that holds one is described as
1322 /// holding one; the previous wording asserted the negative unconditionally.
1323 /// 3. Whether a bounded routing summary additionally left for a Router's
1324 /// provider, and whether that was a *different* provider.
1325 #[must_use]
1326 pub fn transport_disclosure(
1327 router_called: bool,
1328 member_network_tool: bool,
1329 cross_provider: bool,
1330 ) -> String {
1331 let tool_clause = if member_network_tool {
1332 "the member also holds a model-visible network tool"
1333 } else {
1334 "the member holds no model-visible network tool"
1335 };
1336 let mut line = format!("Host-owned provider inference over the network; {tool_clause}.");
1337 if router_called {
1338 line.push_str(" A bounded, redacted routing summary was also sent to the fleet's ");
1339 if cross_provider {
1340 line.push_str("reasoning router, which runs on a different provider than this member.");
1341 } else {
1342 line.push_str("reasoning router, which runs on the same provider as this member.");
1343 }
1344 }
1345 line
1346 }
1347
1348 impl FleetTaskReceipt {
1349 /// Build a receipt from a resolved decision plus the preflighted identity
1350 /// it was resolved for.
1351 #[must_use]
1352 #[allow(clippy::too_many_arguments)]
1353 pub fn new(
1354 fleet: impl Into<String>,
1355 schema_kind: impl Into<String>,
1356 schema_revision: u32,
1357 content_hash: impl Into<String>,
1358 member_id: impl Into<String>,
1359 member_role: impl Into<String>,
1360 route: &PreflightedRoute,
1361 resolved: &ResolvedReasoning,
1362 routing_summary: Option<RoutingDisclosure>,
1363 member_network_tool: bool,
1364 ) -> Self {
1365 let router = resolved.router().cloned();
1366 let router_called = router.is_some();
1367 let cross_provider = routing_summary
1368 .as_ref()
1369 .is_some_and(|summary| summary.cross_provider_inference);
1370 Self {
1371 fleet: fleet.into(),
1372 schema_kind: schema_kind.into(),
1373 schema_revision,
1374 content_hash: content_hash.into(),
1375 member_id: member_id.into(),
1376 member_role: member_role.into(),
1377 posture_role: None,
1378 authority_fingerprint: None,
1379 provider: route.provider_id.clone(),
1380 model: route.wire_model.clone(),
1381 declared_model: route
1382 .model_canonicalized()
1383 .then(|| route.declared_model.clone()),
1384 endpoint: Some(route.endpoint.clone()),
1385 requested_reasoning: resolved.requested().as_str().to_string(),
1386 effective_reasoning: resolved.effective().label().to_string(),
1387 provider_control: resolved.provider_control().as_str().to_string(),
1388 provider_effective_reasoning: resolved.provider_effective().label().to_string(),
1389 selection_source: resolved.source().as_str().to_string(),
1390 capability_normalized: resolved.capability_normalized(),
1391 router,
1392 routing_summary,
1393 member_network_tool,
1394 cross_provider_inference: cross_provider,
1395 transport: transport_disclosure(router_called, member_network_tool, cross_provider),
1396 }
1397 }
1398
1399 /// Record the Runtime permission posture chosen after member resolution,
1400 /// alongside — never instead of — its semantic role.
1401 ///
1402 /// A posture equal to the role is dropped: there is nothing to disclose
1403 /// when the two coincide, and storing it would make the field noise.
1404 #[must_use]
1405 pub fn with_posture_role(mut self, posture_role: impl Into<String>) -> Self {
1406 let posture_role = posture_role.into();
1407 self.posture_role = (posture_role != self.member_role).then_some(posture_role);
1408 self
1409 }
1410
1411 /// Record the fingerprint of the permission envelope this launch installs.
1412 ///
1413 /// Unlike [`Self::with_posture_role`] nothing is dropped for coinciding
1414 /// with something else: the fingerprint is the value the spawn boundary
1415 /// checks, and an absent one means "no ceiling to enforce", not "the
1416 /// obvious ceiling".
1417 #[must_use]
1418 pub fn with_authority_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
1419 self.authority_fingerprint = Some(fingerprint.into());
1420 self
1421 }
1422
1423 /// A single visible line summarizing the whole decision.
1424 #[must_use]
1425 pub fn line(&self) -> String {
1426 let mut line = format!(
1427 "fleet={} member={} (role {}) route={}/{} requested={} effective={} \
1428 provider_control={} provider_effective={} source={}",
1429 self.fleet,
1430 self.member_id,
1431 self.member_role,
1432 self.provider,
1433 self.model,
1434 self.requested_reasoning,
1435 self.effective_reasoning,
1436 self.provider_control,
1437 self.provider_effective_reasoning,
1438 self.selection_source,
1439 );
1440 if let Some(posture) = &self.posture_role {
1441 line.push_str(&format!(" posture={posture}"));
1442 }
1443 if let Some(router) = &self.router {
1444 line.push_str(&format!(" router={}", router.label()));
1445 if let Some(call) = &router.call {
1446 line.push(' ');
1447 line.push_str(&call.receipt());
1448 }
1449 }
1450 if let Some(summary) = &self.routing_summary {
1451 line.push_str(&format!(" {}", summary.receipt()));
1452 }
1453 line
1454 }
1455 }
1456
1457 #[derive(Debug, Clone, PartialEq, Eq, Error)]
1458 pub enum ReasoningResolveError {
1459 #[error(
1460 "fleet member `{member}` requests reasoning `auto`, and {reason}. Attach a reasoning \
1461 router to this fleet (`reasoning_router = \"<name>\"`) or pin an explicit reasoning tier."
1462 )]
1463 RouterRequired { member: String, reason: String },
1464 #[error(
1465 "fleet member `{member}` requests reasoning `auto` but the fleet's reasoning router is \
1466 unavailable: {reason}. Fix the router profile or pin an explicit reasoning tier."
1467 )]
1468 RouterUnavailable { member: String, reason: String },
1469 #[error("fleet member `{member}` requires a router decision that was not supplied")]
1470 RouterDecisionMissing { member: String },
1471 #[error(
1472 "fleet member `{member}` took a router decision with no router identity; a receipt must \
1473 be able to name which reasoning router chose the tier"
1474 )]
1475 RouterIdentityMissing { member: String },
1476 }
1477
1478 #[derive(Debug, Clone, PartialEq, Eq, Error)]
1479 pub enum RouterDecisionError {
1480 #[error("router output was not parseable JSON: {0}")]
1481 Parse(String),
1482 #[error(
1483 "router output contains `{field}`; a reasoning router may only choose a reasoning tier \
1484 and can never move an already frozen provider/model route, member, role, or permission"
1485 )]
1486 RouteMutationAttempt { field: String },
1487 #[error(
1488 "router output contains `{field}`; a reasoning router has exactly one job and may emit \
1489 only `reasoning`"
1490 )]
1491 UnknownField { field: String },
1492 #[error(
1493 "router output names `{field}` more than once; a reasoning router must make exactly one \
1494 concrete choice, and a repeated key is two answers wearing one name"
1495 )]
1496 DuplicateField { field: String },
1497 #[error("router output has no `reasoning` field")]
1498 MissingReasoning,
1499 #[error("router chose `auto`, which is not a concrete reasoning tier")]
1500 AutoReasoning,
1501 #[error("router chose invalid reasoning `{value}`")]
1502 InvalidReasoning { value: String },
1503 #[error(
1504 "router output has content after its JSON object (`{trailing}`); a router must emit \
1505 exactly one object and nothing else"
1506 )]
1507 TrailingContent { trailing: String },
1508 }
1509
1510 /// Strip one surrounding markdown code fence, and nothing else.
1511 ///
1512 /// A fence is formatting, not content: a router that wrapped its object in a
1513 /// json code fence still emitted exactly one object. Anything *inside* the
1514 /// fence is returned verbatim so the one-object rule can judge it.
1515 fn strip_router_code_fence(raw: &str) -> &str {
1516 let trimmed = raw.trim();
1517 trimmed
1518 .strip_prefix("```json")
1519 .or_else(|| trimmed.strip_prefix("```"))
1520 .and_then(|value| value.strip_suffix("```"))
1521 .map_or(trimmed, str::trim)
1522 }
1523
1524 /// A short, sanitized excerpt of whatever followed the router's object, for the
1525 /// error message. Bounded so a runaway response cannot become the error.
1526 fn trailing_excerpt(rest: &str) -> String {
1527 let cleaned: String = rest
1528 .trim()
1529 .chars()
1530 .map(|ch| if ch.is_control() { ' ' } else { ch })
1531 .take(60)
1532 .collect();
1533 cleaned.trim().to_string()
1534 }
1535
1536 #[cfg(test)]
1537 mod tests {
1538 use super::*;
1539 use crate::fleet_preflight::CredentialReadiness;
1540
1541 fn frozen() -> FrozenRoute {
1542 FrozenRoute {
1543 provider: "zai".to_string(),
1544 model: "glm-5".to_string(),
1545 }
1546 }
1547
1548 fn preflighted() -> PreflightedRoute {
1549 PreflightedRoute {
1550 member_id: "implementer".to_string(),
1551 provider_id: "zai".to_string(),
1552 provider_config_id: None,
1553 provider_kind: "zai".to_string(),
1554 declared_model: "glm-5".to_string(),
1555 wire_model: "glm-5".to_string(),
1556 endpoint: EndpointIdentity::from_base_url("https://api.z.ai/api/paas/v4"),
1557 credential: CredentialReadiness::Configured,
1558 capability: ReasoningCapability::tiered(),
1559 }
1560 }
1561
1562 fn router_identity() -> RouterIdentity {
1563 RouterIdentity {
1564 id: "luna-low".to_string(),
1565 origin: "workspace".to_string(),
1566 service_kind: REASONING_ROUTER_SERVICE_KIND.to_string(),
1567 legacy_inline: false,
1568 provider: "openai".to_string(),
1569 model: "gpt-5.6-luna".to_string(),
1570 endpoint: Some(EndpointIdentity::from_base_url("https://api.openai.com/v1")),
1571 call: Some(
1572 router_call_plan(RouterCallReasoning::Low, &ReasoningCapability::tiered())
1573 .disclosure,
1574 ),
1575 }
1576 }
1577
1578 #[test]
1579 fn explicit_tier_resolves_without_a_router() {
1580 let resolved = resolve_exact_member_reasoning(
1581 "implementer",
1582 &frozen(),
1583 RequestedReasoning::High,
1584 &ReasoningCapability::tiered(),
1585 &RouterAvailability::Absent,
1586 None,
1587 None,
1588 )
1589 .expect("explicit tiers never need a router");
1590
1591 assert_eq!(resolved.requested(), RequestedReasoning::High);
1592 assert_eq!(
1593 resolved.effective(),
1594 EffectiveReasoning::Tier(ReasoningTier::High)
1595 );
1596 assert_eq!(resolved.source(), EffectiveReasoningSource::MemberExplicit);
1597 assert!(
1598 resolved.router().is_none(),
1599 "manual reasoning uses no router"
1600 );
1601 assert!(!resolved.capability_normalized());
1602 }
1603
1604 #[test]
1605 fn auto_without_a_router_fails_before_work_starts() {
1606 let err = resolve_exact_member_reasoning(
1607 "implementer",
1608 &frozen(),
1609 RequestedReasoning::Auto,
1610 &ReasoningCapability::tiered(),
1611 &RouterAvailability::Absent,
1612 None,
1613 None,
1614 )
1615 .expect_err("auto must fail closed without a router");
1616
1617 assert!(matches!(err, ReasoningResolveError::RouterRequired { .. }));
1618 let message = err.to_string();
1619 assert!(message.contains("implementer"), "{message}");
1620 assert!(message.contains("reasoning_router"), "{message}");
1621 }
1622
1623 #[test]
1624 fn auto_with_an_unavailable_router_fails_closed_too() {
1625 let err = resolve_exact_member_reasoning(
1626 "implementer",
1627 &frozen(),
1628 RequestedReasoning::Auto,
1629 &ReasoningCapability::tiered(),
1630 &RouterAvailability::Unavailable {
1631 reason: "no credentials for provider `openai`".to_string(),
1632 },
1633 None,
1634 None,
1635 )
1636 .expect_err("unavailable router must fail closed");
1637
1638 assert!(matches!(
1639 err,
1640 ReasoningResolveError::RouterUnavailable { .. }
1641 ));
1642 }
1643
1644 #[test]
1645 fn a_ready_router_decides_only_reasoning_on_a_frozen_route() {
1646 let decision =
1647 parse_router_decision(r#"{"reasoning":"max"}"#).expect("valid router decision");
1648
1649 let worker = frozen();
1650 let resolved = resolve_exact_member_reasoning(
1651 "implementer",
1652 &worker,
1653 RequestedReasoning::Auto,
1654 &ReasoningCapability::tiered(),
1655 &RouterAvailability::Ready,
1656 Some(&decision),
1657 Some(&router_identity()),
1658 )
1659 .expect("ready router resolves auto");
1660
1661 assert_eq!(resolved.requested(), RequestedReasoning::Auto);
1662 assert_eq!(
1663 resolved.effective(),
1664 EffectiveReasoning::Tier(ReasoningTier::Max)
1665 );
1666 assert_eq!(resolved.source(), EffectiveReasoningSource::FleetRouter);
1667 // No model mutation: the frozen route is byte-identical afterwards.
1668 assert_eq!(worker.provider, "zai");
1669 assert_eq!(worker.model, "glm-5");
1670 }
1671
1672 #[test]
1673 fn router_output_that_names_a_route_member_or_permission_is_rejected() {
1674 for raw in [
1675 r#"{"reasoning":"high","provider":"deepseek"}"#,
1676 r#"{"reasoning":"high","model":"glm-5-turbo"}"#,
1677 r#"{"reasoning":"high","model_route":"faster"}"#,
1678 r#"{"reasoning":"high","member_id":"someone-else"}"#,
1679 r#"{"reasoning":"high","role":"builder"}"#,
1680 r#"{"reasoning":"high","allowed_tools":["shell"]}"#,
1681 r#"{"reasoning":"high","permissions":"full"}"#,
1682 ] {
1683 let err = parse_router_decision(raw).expect_err("route fields must be rejected");
1684 assert!(
1685 matches!(err, RouterDecisionError::RouteMutationAttempt { .. }),
1686 "raw={raw} err={err:?}"
1687 );
1688 }
1689 }
1690
1691 #[test]
1692 fn router_may_not_answer_auto_or_garbage() {
1693 assert!(matches!(
1694 parse_router_decision(r#"{"reasoning":"auto"}"#).expect_err("auto"),
1695 RouterDecisionError::AutoReasoning
1696 ));
1697 assert!(matches!(
1698 parse_router_decision(r#"{"reasoning":"turbo"}"#).expect_err("garbage"),
1699 RouterDecisionError::InvalidReasoning { .. }
1700 ));
1701 assert!(matches!(
1702 parse_router_decision("{}").expect_err("missing"),
1703 RouterDecisionError::MissingReasoning
1704 ));
1705 }
1706
1707 /// A router has one job. Anything beyond `reasoning` — including the
1708 /// rationale the old contract tolerated — is rejected outright.
1709 #[test]
1710 fn router_output_rejects_every_unknown_field_including_rationale() {
1711 for raw in [
1712 r#"{"reasoning":"high","rationale":"multi-file refactor"}"#,
1713 r#"{"reasoning":"high","confidence":0.9}"#,
1714 r#"{"reasoning":"high","notes":"just in case"}"#,
1715 r#"{"thinking":"high"}"#,
1716 ] {
1717 let err = parse_router_decision(raw).expect_err("strict output");
1718 assert!(
1719 matches!(err, RouterDecisionError::UnknownField { .. }),
1720 "raw={raw} err={err:?}"
1721 );
1722 }
1723
1724 let only = parse_router_decision(r#"{"reasoning":"low"}"#).expect("sole field accepted");
1725 assert_eq!(only.reasoning, ReasoningTier::Low);
1726 }
1727
1728 /// `serde_json`'s object type keeps only the last value for a repeated key,
1729 /// so a duplicate would otherwise parse as a clean single-key answer. One
1730 /// reasoning key, one concrete choice — a repeat is two answers.
1731 #[test]
1732 fn a_duplicated_reasoning_key_is_rejected_not_last_write_wins() {
1733 for raw in [
1734 r#"{"reasoning":"off","reasoning":"max"}"#,
1735 r#"{"reasoning":"max","reasoning":"max"}"#,
1736 r#"{"reasoning":"low","Reasoning":"max"}"#,
1737 ] {
1738 let err = parse_router_decision(raw).expect_err("duplicate key");
1739 assert!(
1740 matches!(err, RouterDecisionError::DuplicateField { .. }),
1741 "raw={raw} err={err:?}"
1742 );
1743 }
1744
1745 // Sanity: the same parser still accepts the single-key form.
1746 assert_eq!(
1747 parse_router_decision(r#"{"reasoning":"off"}"#)
1748 .expect("single key")
1749 .reasoning,
1750 ReasoningTier::Off
1751 );
1752 }
1753
1754 /// A duplicate must be caught before the unknown-field and route-mutation
1755 /// checks judge whichever copy they happened to reach.
1756 #[test]
1757 fn a_duplicate_is_reported_even_next_to_other_violations() {
1758 let err = parse_router_decision(r#"{"reasoning":"off","reasoning":"max","provider":"x"}"#)
1759 .expect_err("duplicate first");
1760 assert!(
1761 matches!(err, RouterDecisionError::DuplicateField { .. }),
1762 "{err:?}"
1763 );
1764 }
1765
1766 /// A chatty key must not mask an attempted route mutation, whichever way
1767 /// the object's keys happen to be ordered.
1768 #[test]
1769 fn a_route_mutation_keeps_its_distinct_error_next_to_chatty_keys() {
1770 for raw in [
1771 r#"{"aaa_note":"x","reasoning":"high","provider":"deepseek"}"#,
1772 r#"{"provider":"deepseek","zzz_note":"x","reasoning":"high"}"#,
1773 ] {
1774 let err = parse_router_decision(raw).expect_err("route mutation");
1775 assert!(
1776 matches!(
1777 err,
1778 RouterDecisionError::RouteMutationAttempt { ref field } if field == "provider"
1779 ),
1780 "raw={raw} err={err:?}"
1781 );
1782 }
1783 }
1784
1785 /// There is no native-adaptive bypass. `auto` in an exact Fleet means "ask
1786 /// the fleet's reasoning router", full stop.
1787 #[test]
1788 fn a_native_adaptive_route_still_requires_the_router_for_auto() {
1789 let err = resolve_exact_member_reasoning(
1790 "implementer",
1791 &frozen(),
1792 RequestedReasoning::Auto,
1793 &ReasoningCapability::native_adaptive(),
1794 &RouterAvailability::Absent,
1795 None,
1796 None,
1797 )
1798 .expect_err("auto must reach the router even on a native-adaptive route");
1799 assert!(matches!(err, ReasoningResolveError::RouterRequired { .. }));
1800
1801 let decision = parse_router_decision(r#"{"reasoning":"low"}"#).expect("decision");
1802 let resolved = resolve_exact_member_reasoning(
1803 "implementer",
1804 &frozen(),
1805 RequestedReasoning::Auto,
1806 &ReasoningCapability::native_adaptive(),
1807 &RouterAvailability::Ready,
1808 Some(&decision),
1809 Some(&router_identity()),
1810 )
1811 .expect("router decides");
1812 assert_eq!(resolved.source(), EffectiveReasoningSource::FleetRouter);
1813 assert_eq!(
1814 resolved.provider_effective(),
1815 ProviderEffectiveReasoning::NativeAdaptive,
1816 "the route's real control is still reported, just not used as a bypass"
1817 );
1818 }
1819
1820 /// A valid first object followed by anything else is not a valid answer.
1821 #[test]
1822 fn a_valid_object_followed_by_trailing_content_is_rejected() {
1823 for raw in [
1824 r#"{"reasoning":"high"} and I'd also suggest switching models"#,
1825 r#"{"reasoning":"high"}{"reasoning":"off"}"#,
1826 "{\"reasoning\":\"high\"}\n{\"reasoning\":\"max\"}",
1827 r#"{"reasoning":"high"} {"provider":"deepseek"}"#,
1828 ] {
1829 let err = parse_router_decision(raw).expect_err("one object and nothing else");
1830 assert!(
1831 matches!(err, RouterDecisionError::TrailingContent { .. }),
1832 "raw={raw} err={err:?}"
1833 );
1834 }
1835
1836 // Surrounding whitespace is not trailing content, and a code fence is
1837 // formatting rather than a second answer.
1838 assert_eq!(
1839 parse_router_decision(" {\"reasoning\":\"low\"}\n\n")
1840 .expect("whitespace is fine")
1841 .reasoning,
1842 ReasoningTier::Low
1843 );
1844 assert_eq!(
1845 parse_router_decision("```json\n{\"reasoning\":\"max\"}\n```")
1846 .expect("a fence is formatting")
1847 .reasoning,
1848 ReasoningTier::Max
1849 );
1850 }
1851
1852 /// The user's example: GPT-5.6 Luna configured at `low` is *called* at low
1853 /// and says so. Nothing forces `off` behind a `low` label.
1854 #[test]
1855 fn a_router_configured_low_is_called_at_low_and_receipts_it() {
1856 let plan = router_call_plan(RouterCallReasoning::Low, &ReasoningCapability::tiered());
1857
1858 assert_eq!(plan.tier, ReasoningTier::Low);
1859 assert_eq!(plan.disclosure.requested, "low");
1860 assert_eq!(plan.disclosure.effective, "low");
1861 assert_eq!(plan.disclosure.provider_control, "tiers");
1862 assert_eq!(plan.disclosure.provider_effective, "low");
1863 assert!(!plan.disclosure.capability_normalized);
1864
1865 let receipt = plan.disclosure.receipt();
1866 assert!(receipt.contains("router_call_requested=low"), "{receipt}");
1867 assert!(receipt.contains("router_call_effective=low"), "{receipt}");
1868 assert!(
1869 receipt.contains("router_call_provider_effective=low"),
1870 "{receipt}"
1871 );
1872 }
1873
1874 #[test]
1875 fn a_router_configured_off_stays_off() {
1876 let plan = router_call_plan(RouterCallReasoning::Off, &ReasoningCapability::tiered());
1877 assert_eq!(plan.tier, ReasoningTier::Off);
1878 assert_eq!(plan.disclosure.requested, "off");
1879 assert_eq!(plan.disclosure.effective, "off");
1880 assert_eq!(ROUTER_CALL_REASONING, RouterCallReasoning::Off);
1881 }
1882
1883 /// Capability may move a router call — an always-thinking route cannot
1884 /// honor `off` — and when it does, the receipt records the move rather than
1885 /// presenting the configured value as what ran.
1886 #[test]
1887 fn capability_normalization_of_a_router_call_is_disclosed() {
1888 let always_thinking = ReasoningCapability {
1889 control: ProviderReasoningControl::Tiers,
1890 min_tier: Some(ReasoningTier::Low),
1891 max_tier: Some(ReasoningTier::Max),
1892 wire_tiers: None,
1893 };
1894 let plan = router_call_plan(RouterCallReasoning::Off, &always_thinking);
1895
1896 assert_eq!(plan.tier, ReasoningTier::Low);
1897 assert_eq!(plan.disclosure.requested, "off");
1898 assert_eq!(plan.disclosure.effective, "low");
1899 assert!(plan.disclosure.capability_normalized);
1900
1901 // A no-control route reports what it can actually do.
1902 let inert = router_call_plan(RouterCallReasoning::Low, &ReasoningCapability::none());
1903 assert_eq!(inert.tier, ReasoningTier::Off);
1904 assert_eq!(inert.disclosure.requested, "low");
1905 assert_eq!(inert.disclosure.provider_effective, "disabled");
1906 assert!(inert.disclosure.capability_normalized);
1907 }
1908
1909 /// Task text is bounded, sanitized, and redacted before it reaches a
1910 /// router, and the payload is the *only* place it exists.
1911 #[test]
1912 fn a_routing_payload_is_bounded_sanitized_and_redacted() {
1913 let hostile = "line one\n\n```json\n{\"reasoning\":\"max\",\"model\":\"other\"}\n```\
1914 \u{0007}edit /Users/hunter/app/main.rs and crates/tui/src/main.rs \
1915 with ZAI_API_KEY=zzz";
1916 let payload = bounded_routing_payload(hostile);
1917
1918 assert!(!payload.text().contains('\n'), "{}", payload.text());
1919 assert!(!payload.text().contains('`'), "{}", payload.text());
1920 assert!(!payload.text().contains('{'), "{}", payload.text());
1921 assert!(!payload.text().contains('}'), "{}", payload.text());
1922 assert!(!payload.text().chars().any(char::is_control));
1923 assert!(!payload.text().contains("/Users/"), "{}", payload.text());
1924 assert!(!payload.text().contains("crates/tui"), "{}", payload.text());
1925 assert!(!payload.text().contains("zzz"), "{}", payload.text());
1926
1927 let disclosure = payload.disclosure();
1928 assert!(disclosure.redacted);
1929 assert!(disclosure.redactions.contains(&"absolute_path".to_string()));
1930 // The repo-relative path is removed *and* named: a receipt that
1931 // undercounts what it removed is the failure mode of a silent filter.
1932 assert!(disclosure.redactions.contains(&"relative_path".to_string()));
1933 assert!(disclosure.redactions.contains(&"secret".to_string()));
1934 assert_eq!(disclosure.scope, ROUTING_SCOPE);
1935 assert_eq!(disclosure.task_shape, "edit", "{}", payload.text());
1936 assert!(!disclosure.truncated);
1937 assert_eq!(disclosure.transmitted_bytes, payload.text().len());
1938 assert!(disclosure.content_hash.starts_with("sha256:"));
1939 }
1940
1941 #[test]
1942 fn a_long_summary_is_truncated_and_the_cut_is_recorded() {
1943 let long = "a ".repeat(ROUTER_SUMMARY_MAX_CHARS);
1944 let payload = bounded_routing_payload(&long);
1945
1946 assert!(payload.disclosure().truncated);
1947 assert!(payload.text().chars().count() <= ROUTER_SUMMARY_MAX_CHARS);
1948 assert!(payload.disclosure().original_chars > ROUTER_SUMMARY_MAX_CHARS);
1949 assert!(payload.disclosure().receipt().contains("truncated=true"));
1950 }
1951
1952 /// The bounded summary is transmitted exactly once. Repeating it in the
1953 /// system prompt would double what leaves for the router's provider while
1954 /// the receipt's byte count described only one copy.
1955 #[test]
1956 fn the_routing_summary_is_transmitted_once_and_the_hash_matches_those_bytes() {
1957 let payload = bounded_routing_payload("refactor the parser across three crates");
1958 let disclosure = payload.disclosure().clone();
1959 let input = RouterCallInput {
1960 fleet: "workspace/glm-pair".to_string(),
1961 member_id: "implementer".to_string(),
1962 frozen: frozen(),
1963 payload,
1964 };
1965
1966 let system = router_system_prompt(&input);
1967 let user = router_user_message(&input);
1968
1969 assert!(
1970 !system.contains("refactor the parser"),
1971 "the system prompt must carry no task content: {system}"
1972 );
1973 assert!(system.contains("The next message is a bounded"), "{system}");
1974 assert_eq!(user, "refactor the parser across three crates");
1975
1976 // The disclosed count and hash describe exactly the transmitted bytes.
1977 assert_eq!(disclosure.transmitted_bytes, user.len());
1978 assert_eq!(disclosure.transmitted_chars, user.chars().count());
1979 assert_eq!(
1980 disclosure.content_hash,
1981 crate::named_fleet::sha256_label(user.as_bytes())
1982 );
1983
1984 // Exactly one copy across both messages.
1985 let combined = format!("{system}\n{user}");
1986 assert_eq!(
1987 combined
1988 .matches("refactor the parser across three crates")
1989 .count(),
1990 1,
1991 "the summary must appear once across the whole request: {combined}"
1992 );
1993 }
1994
1995 #[test]
1996 fn the_router_prompt_states_the_frozen_route_and_forbids_moving_it() {
1997 let input = RouterCallInput {
1998 fleet: "workspace/glm-pair".to_string(),
1999 member_id: "implementer".to_string(),
2000 frozen: frozen(),
2001 payload: bounded_routing_payload("land a fix"),
2002 };
2003 let prompt = router_system_prompt(&input);
2004
2005 assert!(prompt.contains("already frozen"), "{prompt}");
2006 assert!(prompt.contains("glm-5"), "{prompt}");
2007 assert!(prompt.contains("fails the run"), "{prompt}");
2008 assert!(prompt.contains("reasoning-only service"), "{prompt}");
2009 assert!(prompt.contains("no repeated key"), "{prompt}");
2010 assert!(
2011 !prompt.to_ascii_lowercase().contains("rationale")
2012 || prompt.contains("not a rationale"),
2013 "the prompt must not invite a rationale: {prompt}"
2014 );
2015 }
2016
2017 /// The receipt must answer every question the operator can ask about a
2018 /// launch — including who chose the tier and what that service cost — while
2019 /// storing **no task or summary text**.
2020 #[test]
2021 fn a_receipt_discloses_everything_and_stores_no_content() {
2022 let decision = parse_router_decision(r#"{"reasoning":"max"}"#).expect("decision");
2023 let identity = router_identity();
2024 assert_eq!(identity.service_kind, "reasoning_router");
2025
2026 let resolved = resolve_exact_member_reasoning(
2027 "implementer",
2028 &frozen(),
2029 RequestedReasoning::Auto,
2030 &ReasoningCapability::enabled_disabled(),
2031 &RouterAvailability::Ready,
2032 Some(&decision),
2033 Some(&identity),
2034 )
2035 .expect("resolve");
2036
2037 let summary = bounded_routing_payload("land a fix in /Users/hunter/app")
2038 .with_cross_provider(true)
2039 .into_disclosure();
2040
2041 let receipt = FleetTaskReceipt::new(
2042 "workspace/glm-pair",
2043 "exact",
2044 1,
2045 "sha256:abc",
2046 "implementer",
2047 "builder",
2048 &preflighted(),
2049 &resolved,
2050 Some(summary),
2051 false,
2052 );
2053
2054 assert_eq!(receipt.member_id, "implementer");
2055 assert_eq!(receipt.member_role, "builder");
2056 assert_eq!(receipt.provider, "zai");
2057 assert_eq!(receipt.model, "glm-5");
2058 assert_eq!(receipt.requested_reasoning, "auto");
2059 assert_eq!(receipt.effective_reasoning, "max");
2060 // The GLM route cannot express `max` distinctly; the receipt says so.
2061 assert_eq!(receipt.provider_effective_reasoning, "enabled");
2062 assert_eq!(receipt.provider_control, "enabled_disabled");
2063 assert_eq!(receipt.selection_source, "fleet_router");
2064 assert!(receipt.cross_provider_inference);
2065
2066 let router = receipt.router.as_ref().expect("router identity");
2067 assert_eq!(router.service_kind, "reasoning_router");
2068 assert_eq!(router.qualified(), "workspace/luna-low");
2069 assert_eq!(router.provider, "openai");
2070 assert_eq!(router.model, "gpt-5.6-luna");
2071 let call = router.call.as_ref().expect("call disclosure");
2072 assert_eq!(call.requested, "low");
2073 assert_eq!(call.effective, "low");
2074 assert_eq!(call.provider_effective, "low");
2075
2076 // Disclosure without content: counts, hash, redaction — no text.
2077 let disclosure = receipt.routing_summary.as_ref().expect("disclosure");
2078 assert!(disclosure.transmitted_bytes > 0);
2079 assert!(disclosure.content_hash.starts_with("sha256:"));
2080 assert!(disclosure.redacted);
2081
2082 let json = serde_json::to_string(&receipt).expect("serialize");
2083 assert!(
2084 !json.contains("land a fix"),
2085 "a receipt must never store task text: {json}"
2086 );
2087 assert!(!json.contains("/Users/"), "{json}");
2088 assert!(
2089 !json.contains("\"text\""),
2090 "a receipt must have no text field at all: {json}"
2091 );
2092 let lowered = json.to_ascii_lowercase();
2093 for forbidden in ["api_key", "secret\"", "bearer", "base_url"] {
2094 assert!(!lowered.contains(forbidden), "{forbidden} in {json}");
2095 }
2096
2097 let line = receipt.line();
2098 for expected in [
2099 "requested=auto",
2100 "effective=max",
2101 "provider_effective=enabled",
2102 "source=fleet_router",
2103 "router=reasoning_router:workspace/luna-low openai/gpt-5.6-luna",
2104 "router_call_requested=low",
2105 "cross_provider=true",
2106 ] {
2107 assert!(line.contains(expected), "{expected} missing from {line}");
2108 }
2109 assert!(
2110 !line.contains("land a fix"),
2111 "the visible line must not echo task text: {line}"
2112 );
2113
2114 let back: FleetTaskReceipt = serde_json::from_str(&json).expect("round-trip");
2115 assert_eq!(back, receipt);
2116 }
2117
2118 /// `network_tool` is a statement about the member's *tool surface*. The
2119 /// transport sentence must reflect whichever way it actually points, and
2120 /// must never be read as "nothing left the host".
2121 #[test]
2122 fn transport_disclosure_follows_the_member_network_tool_truth() {
2123 let without = transport_disclosure(false, false, false);
2124 assert!(
2125 without.contains("holds no model-visible network tool"),
2126 "{without}"
2127 );
2128 assert!(
2129 without.contains("Host-owned provider inference"),
2130 "{without}"
2131 );
2132
2133 let with = transport_disclosure(false, true, false);
2134 assert!(
2135 with.contains("also holds a model-visible network tool"),
2136 "a member that holds one must not be described as holding none: {with}"
2137 );
2138 assert!(
2139 !with.contains("holds no model-visible network tool"),
2140 "{with}"
2141 );
2142
2143 let cross = transport_disclosure(true, true, true);
2144 assert!(cross.contains("different provider"), "{cross}");
2145 let same = transport_disclosure(true, false, false);
2146 assert!(same.contains("same provider"), "{same}");
2147 }
2148
2149 /// A receipt built for a member that *does* hold a network tool says so.
2150 #[test]
2151 fn a_network_capable_members_receipt_does_not_claim_it_has_no_network_tool() {
2152 let resolved = resolve_exact_member_reasoning(
2153 "implementer",
2154 &frozen(),
2155 RequestedReasoning::High,
2156 &ReasoningCapability::tiered(),
2157 &RouterAvailability::Absent,
2158 None,
2159 None,
2160 )
2161 .expect("resolve");
2162
2163 let receipt = FleetTaskReceipt::new(
2164 "workspace/glm-pair",
2165 "exact",
2166 1,
2167 "sha256:abc",
2168 "implementer",
2169 "builder",
2170 &preflighted(),
2171 &resolved,
2172 None,
2173 true,
2174 );
2175
2176 assert!(receipt.member_network_tool);
2177 assert!(
2178 receipt
2179 .transport
2180 .contains("also holds a model-visible network tool"),
2181 "{}",
2182 receipt.transport
2183 );
2184 assert!(!receipt.cross_provider_inference);
2185 assert!(receipt.routing_summary.is_none());
2186 }
2187
2188 /// The semantic role and the runtime permission posture are two facts, and
2189 /// a receipt has to keep both. A member the operator named `auditor` that
2190 /// runs under the `scout` posture must not be *displayed* as a scout, and
2191 /// must not be *enforced* as an auditor.
2192 #[test]
2193 fn a_receipt_keeps_the_semantic_role_and_the_permission_posture_apart() {
2194 let resolved = resolve_exact_member_reasoning(
2195 "auditor",
2196 &frozen(),
2197 RequestedReasoning::High,
2198 &ReasoningCapability::tiered(),
2199 &RouterAvailability::Absent,
2200 None,
2201 None,
2202 )
2203 .expect("resolve");
2204
2205 let receipt = FleetTaskReceipt::new(
2206 "workspace/glm-pair",
2207 "exact",
2208 1,
2209 "sha256:abc",
2210 "auditor",
2211 "auditor",
2212 &preflighted(),
2213 &resolved,
2214 None,
2215 false,
2216 )
2217 .with_posture_role("scout");
2218
2219 assert_eq!(receipt.member_role, "auditor");
2220 assert_eq!(receipt.posture_role.as_deref(), Some("scout"));
2221 let line = receipt.line();
2222 assert!(line.contains("(role auditor)"), "{line}");
2223 assert!(line.contains("posture=scout"), "{line}");
2224
2225 let json = serde_json::to_string(&receipt).expect("serialize");
2226 let back: FleetTaskReceipt = serde_json::from_str(&json).expect("round-trip");
2227 assert_eq!(back, receipt);
2228
2229 // When the two coincide there is nothing to disclose, so the field
2230 // stays absent and older receipts stay byte-identical.
2231 let same = FleetTaskReceipt::new(
2232 "workspace/glm-pair",
2233 "exact",
2234 1,
2235 "sha256:abc",
2236 "implementer",
2237 "builder",
2238 &preflighted(),
2239 &resolved,
2240 None,
2241 false,
2242 )
2243 .with_posture_role("builder");
2244 assert_eq!(same.posture_role, None);
2245 assert!(!same.line().contains("posture="), "{}", same.line());
2246 assert!(
2247 !serde_json::to_string(&same)
2248 .expect("serialize")
2249 .contains("posture_role")
2250 );
2251 }
2252
2253 /// A receipt records the canonical wire model — the same string the child
2254 /// spawns with — and keeps the declared spelling when they differ.
2255 #[test]
2256 fn a_receipt_records_the_canonical_wire_model_and_the_declared_one() {
2257 let mut route = preflighted();
2258 route.wire_model = "glm-5-20260101".to_string();
2259
2260 let resolved = resolve_exact_member_reasoning(
2261 "implementer",
2262 &route.frozen(),
2263 RequestedReasoning::Low,
2264 &ReasoningCapability::tiered(),
2265 &RouterAvailability::Absent,
2266 None,
2267 None,
2268 )
2269 .expect("resolve");
2270
2271 let receipt = FleetTaskReceipt::new(
2272 "workspace/glm-pair",
2273 "exact",
2274 1,
2275 "sha256:abc",
2276 "implementer",
2277 "builder",
2278 &route,
2279 &resolved,
2280 None,
2281 false,
2282 );
2283
2284 assert_eq!(receipt.model, "glm-5-20260101");
2285 assert_eq!(receipt.declared_model.as_deref(), Some("glm-5"));
2286 assert_eq!(
2287 receipt.endpoint.as_ref().expect("endpoint").host,
2288 "api.z.ai"
2289 );
2290 }
2291
2292 /// A receipt written by an older build (no router/summary/transport fields,
2293 /// and a routing summary that still carried `text`) must still deserialize.
2294 #[test]
2295 fn older_receipts_and_journals_still_deserialize() {
2296 let legacy = r#"{
2297 "fleet": "workspace/glm-pair",
2298 "member_id": "implementer",
2299 "member_role": "builder",
2300 "provider": "zai",
2301 "model": "glm-5",
2302 "requested_reasoning": "high",
2303 "effective_reasoning": "high",
2304 "provider_effective_reasoning": "enabled",
2305 "selection_source": "member_explicit"
2306 }"#;
2307 let receipt: FleetTaskReceipt = serde_json::from_str(legacy).expect("serde defaults");
2308 assert!(receipt.router.is_none());
2309 assert!(receipt.routing_summary.is_none());
2310 assert_eq!(receipt.schema_revision, 0);
2311 assert!(!receipt.cross_provider_inference);
2312
2313 // A journal written when the summary still carried its text: the text
2314 // field is simply ignored, and the counts survive.
2315 let with_text = r#"{
2316 "fleet": "workspace/glm-pair",
2317 "member_id": "implementer",
2318 "member_role": "builder",
2319 "provider": "zai",
2320 "model": "glm-5",
2321 "requested_reasoning": "auto",
2322 "effective_reasoning": "max",
2323 "provider_effective_reasoning": "enabled",
2324 "selection_source": "fleet_router",
2325 "router": {"id":"router","role":"router","provider":"zai","model":"glm-5-turbo"},
2326 "routing_summary": {"text":"land a fix","original_chars":10,"truncated":false}
2327 }"#;
2328 let older: FleetTaskReceipt = serde_json::from_str(with_text).expect("serde defaults");
2329 let summary = older.routing_summary.as_ref().expect("summary");
2330 assert_eq!(summary.original_chars, 10);
2331 assert!(!summary.truncated);
2332 assert_eq!(summary.transmitted_bytes, 0, "unknown in an old journal");
2333 let router = older.router.as_ref().expect("router");
2334 assert_eq!(
2335 router.service_kind, "router",
2336 "the old `role` field aliases in"
2337 );
2338 assert_eq!(router.origin, "legacy_inline");
2339 }
2340
2341 #[test]
2342 fn capability_normalization_is_recorded_not_hidden() {
2343 let capped = ReasoningCapability {
2344 control: ProviderReasoningControl::Tiers,
2345 min_tier: Some(ReasoningTier::Low),
2346 max_tier: Some(ReasoningTier::High),
2347 wire_tiers: None,
2348 };
2349
2350 let raised = resolve_exact_member_reasoning(
2351 "w",
2352 &frozen(),
2353 RequestedReasoning::Off,
2354 &capped,
2355 &RouterAvailability::Absent,
2356 None,
2357 None,
2358 )
2359 .expect("resolve");
2360 assert_eq!(
2361 raised.effective(),
2362 EffectiveReasoning::Tier(ReasoningTier::Low)
2363 );
2364 assert!(raised.capability_normalized());
2365 assert_eq!(
2366 raised.requested(),
2367 RequestedReasoning::Off,
2368 "requested is preserved"
2369 );
2370
2371 let lowered = resolve_exact_member_reasoning(
2372 "w",
2373 &frozen(),
2374 RequestedReasoning::Max,
2375 &capped,
2376 &RouterAvailability::Absent,
2377 None,
2378 None,
2379 )
2380 .expect("resolve");
2381 assert_eq!(
2382 lowered.effective(),
2383 EffectiveReasoning::Tier(ReasoningTier::High)
2384 );
2385 assert!(lowered.capability_normalized());
2386
2387 let thinkless = resolve_exact_member_reasoning(
2388 "w",
2389 &frozen(),
2390 RequestedReasoning::Max,
2391 &ReasoningCapability::none(),
2392 &RouterAvailability::Absent,
2393 None,
2394 None,
2395 )
2396 .expect("resolve");
2397 assert_eq!(
2398 thinkless.effective(),
2399 EffectiveReasoning::Tier(ReasoningTier::Off)
2400 );
2401 }
2402
2403 #[test]
2404 fn legacy_auto_keeps_its_local_heuristic() {
2405 let resolved = resolve_legacy_reasoning(
2406 RequestedReasoning::Auto,
2407 &ReasoningCapability::tiered(),
2408 ReasoningTier::High,
2409 );
2410
2411 assert_eq!(resolved.requested(), RequestedReasoning::Auto);
2412 assert_eq!(
2413 resolved.effective(),
2414 EffectiveReasoning::Tier(ReasoningTier::High)
2415 );
2416 assert_eq!(resolved.source(), EffectiveReasoningSource::LegacyHeuristic);
2417 }
2418
2419 /// Z.AI's GLM routes place `thinking = {"type": "enabled"}` on the wire for
2420 /// every tier above off. `high` and `max` are therefore the same request,
2421 /// and a receipt must say so instead of inventing two provider-effective
2422 /// tiers.
2423 #[test]
2424 fn glm_style_routes_report_enabled_control_not_distinct_high_and_max() {
2425 let glm = ReasoningCapability::enabled_disabled();
2426
2427 let high = resolve_exact_member_reasoning(
2428 "implementer",
2429 &frozen(),
2430 RequestedReasoning::High,
2431 &glm,
2432 &RouterAvailability::Absent,
2433 None,
2434 None,
2435 )
2436 .expect("resolve");
2437 let max = resolve_exact_member_reasoning(
2438 "implementer",
2439 &frozen(),
2440 RequestedReasoning::Max,
2441 &glm,
2442 &RouterAvailability::Absent,
2443 None,
2444 None,
2445 )
2446 .expect("resolve");
2447
2448 assert_eq!(
2449 high.effective(),
2450 EffectiveReasoning::Tier(ReasoningTier::High)
2451 );
2452 assert_eq!(
2453 max.effective(),
2454 EffectiveReasoning::Tier(ReasoningTier::Max)
2455 );
2456 assert_eq!(
2457 high.provider_effective(),
2458 ProviderEffectiveReasoning::Enabled
2459 );
2460 assert_eq!(max.provider_effective(), high.provider_effective());
2461 assert_eq!(
2462 high.provider_control(),
2463 ProviderReasoningControl::EnabledDisabled
2464 );
2465
2466 let off = resolve_exact_member_reasoning(
2467 "implementer",
2468 &frozen(),
2469 RequestedReasoning::Off,
2470 &glm,
2471 &RouterAvailability::Absent,
2472 None,
2473 None,
2474 )
2475 .expect("resolve");
2476 assert_eq!(
2477 off.provider_effective(),
2478 ProviderEffectiveReasoning::Disabled,
2479 "off is the one distinction a GLM route can actually express"
2480 );
2481 }
2482
2483 /// A tiered route (Kimi K3's low/high/max shape) keeps its tiers distinct.
2484 #[test]
2485 fn a_tiered_route_reports_each_tier_as_its_own_provider_effective_control() {
2486 let tiered = ReasoningCapability::tiered();
2487 let mut seen = Vec::new();
2488 for requested in [
2489 RequestedReasoning::Low,
2490 RequestedReasoning::High,
2491 RequestedReasoning::Max,
2492 ] {
2493 let resolved = resolve_exact_member_reasoning(
2494 "w",
2495 &frozen(),
2496 requested,
2497 &tiered,
2498 &RouterAvailability::Absent,
2499 None,
2500 None,
2501 )
2502 .expect("resolve");
2503 seen.push(resolved.provider_effective());
2504 }
2505 assert_eq!(
2506 seen,
2507 vec![
2508 ProviderEffectiveReasoning::Tier(ReasoningTier::Low),
2509 ProviderEffectiveReasoning::Tier(ReasoningTier::High),
2510 ProviderEffectiveReasoning::Tier(ReasoningTier::Max),
2511 ]
2512 );
2513 }
2514
2515 /// Nothing in this crate may assert native adaptive on a route's behalf.
2516 #[test]
2517 fn no_default_capability_claims_provider_native_adaptive() {
2518 for capability in [
2519 ReasoningCapability::none(),
2520 ReasoningCapability::tiered(),
2521 ReasoningCapability::enabled_disabled(),
2522 ] {
2523 assert!(
2524 !capability.supports_native_adaptive(),
2525 "{capability:?} must not claim native adaptive"
2526 );
2527 }
2528 assert!(ReasoningCapability::native_adaptive().supports_native_adaptive());
2529 }
2530
2531 /// A route that *collapses* interior tiers cannot be described by a floor
2532 /// and a ceiling. CodeWhale's own route normalizer coerces `low` and
2533 /// `medium` to `high` on every non-Codex route while leaving `off` alone,
2534 /// so a receipt that reported the requested `low` would name a request
2535 /// nobody made.
2536 #[test]
2537 fn a_route_that_collapses_interior_tiers_receipts_the_tier_that_was_sent() {
2538 let collapsing = ReasoningCapability::tiered().with_wire_tiers([
2539 ReasoningTier::Off,
2540 ReasoningTier::High,
2541 ReasoningTier::High,
2542 ReasoningTier::High,
2543 ReasoningTier::Max,
2544 ]);
2545
2546 let low = resolve_exact_member_reasoning(
2547 "implementer",
2548 &frozen(),
2549 RequestedReasoning::Low,
2550 &collapsing,
2551 &RouterAvailability::Absent,
2552 None,
2553 None,
2554 )
2555 .expect("resolve");
2556
2557 assert_eq!(
2558 low.requested(),
2559 RequestedReasoning::Low,
2560 "requested survives"
2561 );
2562 assert_eq!(
2563 low.effective(),
2564 EffectiveReasoning::Tier(ReasoningTier::High),
2565 "the route sends high, so the receipt must say high"
2566 );
2567 assert!(low.capability_normalized(), "the move is recorded");
2568 assert_eq!(
2569 low.provider_effective(),
2570 ProviderEffectiveReasoning::Tier(ReasoningTier::High)
2571 );
2572 assert!(
2573 !low.receipt().contains("selected=low"),
2574 "a receipt must not name a tier the wire never carried: {}",
2575 low.receipt()
2576 );
2577
2578 // `off` is untouched, which is exactly why a min_tier floor cannot
2579 // express this route.
2580 let off = resolve_exact_member_reasoning(
2581 "implementer",
2582 &frozen(),
2583 RequestedReasoning::Off,
2584 &collapsing,
2585 &RouterAvailability::Absent,
2586 None,
2587 None,
2588 )
2589 .expect("resolve");
2590 assert_eq!(
2591 off.effective(),
2592 EffectiveReasoning::Tier(ReasoningTier::Off)
2593 );
2594 assert!(!off.capability_normalized());
2595 }
2596
2597 /// The identity map is stored as absent, so a faithful route never reports
2598 /// a normalization it did not perform — and older serialized preflights,
2599 /// which have no such field, still read.
2600 #[test]
2601 fn a_faithful_wire_map_is_not_recorded_and_older_capabilities_deserialize() {
2602 let faithful = ReasoningCapability::tiered().with_wire_tiers(FAITHFUL_WIRE_TIERS);
2603 assert_eq!(faithful.wire_tiers, None);
2604 assert_eq!(
2605 faithful.normalize(ReasoningTier::Low),
2606 (ReasoningTier::Low, false)
2607 );
2608 assert_eq!(
2609 faithful.wire_tier(ReasoningTier::Medium),
2610 ReasoningTier::Medium
2611 );
2612
2613 let older: ReasoningCapability =
2614 serde_json::from_str(r#"{"control":"tiers","min_tier":null,"max_tier":null}"#)
2615 .expect("serde default");
2616 assert_eq!(older, ReasoningCapability::tiered());
2617
2618 let collapsing = ReasoningCapability::tiered().with_wire_tiers([
2619 ReasoningTier::Off,
2620 ReasoningTier::High,
2621 ReasoningTier::High,
2622 ReasoningTier::High,
2623 ReasoningTier::Max,
2624 ]);
2625 let json = serde_json::to_string(&collapsing).expect("serialize");
2626 let back: ReasoningCapability = serde_json::from_str(&json).expect("round-trip");
2627 assert_eq!(back, collapsing);
2628 }
2629
2630 /// The Router's own call is normalized by the same authority, so a Router
2631 /// configured `low` on a route that cannot send `low` discloses what it
2632 /// actually cost instead of the label the operator wrote.
2633 #[test]
2634 fn a_router_call_on_a_collapsing_route_discloses_the_tier_it_actually_ran_at() {
2635 let collapsing = ReasoningCapability::tiered().with_wire_tiers([
2636 ReasoningTier::Off,
2637 ReasoningTier::High,
2638 ReasoningTier::High,
2639 ReasoningTier::High,
2640 ReasoningTier::Max,
2641 ]);
2642 let plan = router_call_plan(RouterCallReasoning::Low, &collapsing);
2643
2644 assert_eq!(plan.tier, ReasoningTier::High);
2645 assert_eq!(plan.disclosure.requested, "low");
2646 assert_eq!(plan.disclosure.effective, "high");
2647 assert_eq!(plan.disclosure.provider_effective, "high");
2648 assert!(plan.disclosure.capability_normalized);
2649
2650 // `off` still costs nothing on the same route.
2651 let off = router_call_plan(RouterCallReasoning::Off, &collapsing);
2652 assert_eq!(off.tier, ReasoningTier::Off);
2653 assert!(!off.disclosure.capability_normalized);
2654 }
2655
2656 #[test]
2657 fn task_shape_classification_is_coarse_and_content_free() {
2658 assert_eq!(
2659 TaskShape::classify("debug the flaky test"),
2660 TaskShape::Diagnose
2661 );
2662 assert_eq!(TaskShape::classify("refactor the parser"), TaskShape::Edit);
2663 assert_eq!(TaskShape::classify("review this diff"), TaskShape::Read);
2664 assert_eq!(TaskShape::classify("qqq"), TaskShape::Unclassified);
2665 }
2666 }
2667
2667 lines RUST