1#![forbid(unsafe_code)]
4
5use serde::{Deserialize, Deserializer, Serialize};
8use serde_json::{Value, json};
9use std::collections::HashSet;
10use std::fmt;
11use thiserror::Error;
12
13mod catalog_proof;
14mod route_identity;
15
16pub use catalog_proof::*;
17pub use route_identity::*;
18
19pub const M1_WIRE_VERSION: &str = "2026-07-24.m1";
20
21pub const MAX_SPEND_AUTHORIZATION_MICRO_USD: u64 = 10_000_000;
25
26macro_rules! identifier {
27 ($name:ident) => {
28 #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
29 #[serde(transparent)]
30 pub struct $name(String);
31
32 impl $name {
33 pub fn parse(value: impl Into<String>) -> Result<Self, ValidationError> {
34 let value = value.into();
35 if value.is_empty()
36 || value.len() > 128
37 || !value.bytes().all(|byte| {
38 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':')
39 })
40 {
41 return Err(ValidationError::InvalidIdentifier(stringify!($name)));
42 }
43 Ok(Self(value))
44 }
45
46 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49 }
50
51 impl fmt::Display for $name {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 formatter.write_str(&self.0)
54 }
55 }
56
57 impl<'de> Deserialize<'de> for $name {
58 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59 where
60 D: Deserializer<'de>,
61 {
62 let value = String::deserialize(deserializer)?;
63 Self::parse(value).map_err(serde::de::Error::custom)
64 }
65 }
66 };
67}
68
69identifier!(HarnessInstanceId);
70identifier!(SessionId);
71identifier!(TurnId);
72identifier!(RequestId);
73identifier!(AttemptId);
74identifier!(ResponseId);
75identifier!(ItemId);
76identifier!(CallId);
77
78#[derive(Debug, Error, PartialEq, Eq)]
79pub enum ValidationError {
80 #[error("an identifier is invalid")]
81 InvalidIdentifier(&'static str),
82 #[error("the wire version is unsupported")]
83 UnsupportedWireVersion,
84 #[error("streaming is required")]
85 StreamingRequired,
86 #[error("the request contains a non-translatable capability")]
87 NonTranslatableCapability,
88 #[error("the tool definition is invalid or collides with another tool")]
89 InvalidToolDefinition,
90 #[error("the request declares an invalid spend authorization")]
91 InvalidSpendAuthorization,
92}
93
94#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
95#[serde(deny_unknown_fields)]
96pub struct CanonicalRequest {
97 pub wire_version: String,
98 pub model: String,
99 pub stream: bool,
100 pub harness_instance: HarnessInstanceId,
101 pub session_id: SessionId,
102 pub turn_id: TurnId,
103 pub input: Vec<InputItem>,
104 #[serde(default)]
105 pub tools: Vec<FunctionTool>,
106 #[serde(default)]
107 pub tool_choice: ToolChoice,
108 #[serde(default)]
109 pub structured_output: Option<StructuredOutputIntent>,
110 #[serde(default)]
111 pub reasoning: ReasoningIntent,
112 #[serde(default)]
113 pub max_output_tokens: Option<u32>,
114 #[serde(default)]
118 pub max_spend_micro_usd: Option<u64>,
119 #[serde(default)]
120 pub capabilities: CapabilityRequirements,
121}
122
123impl CanonicalRequest {
124 pub fn validate(&self) -> Result<(), ValidationError> {
125 if self.wire_version != M1_WIRE_VERSION {
126 return Err(ValidationError::UnsupportedWireVersion);
127 }
128 if !self.stream {
129 return Err(ValidationError::StreamingRequired);
130 }
131 if let Some(max_spend) = self.max_spend_micro_usd
132 && max_spend > MAX_SPEND_AUTHORIZATION_MICRO_USD
133 {
134 return Err(ValidationError::InvalidSpendAuthorization);
135 }
136 validate_tools(&self.tools)?;
137 if let ToolChoice::Named(name) = &self.tool_choice {
138 validate_tool_name(name)?;
139 if self.tools.iter().filter(|tool| tool.name == *name).count() != 1 {
140 return Err(ValidationError::InvalidToolDefinition);
141 }
142 }
143 for item in &self.input {
144 if let InputItem::FunctionCall {
145 namespace, name, ..
146 } = item
147 {
148 validate_tool_name(name)?;
149 if let Some(namespace) = namespace {
150 validate_tool_namespace(namespace)?;
151 }
152 }
153 }
154 Ok(())
155 }
156
157 pub fn function_output(&self) -> Option<(&CallId, &Value)> {
158 self.input.iter().find_map(|item| match item {
159 InputItem::FunctionCallOutput { call_id, output } => Some((call_id, output)),
160 InputItem::Message { .. } | InputItem::FunctionCall { .. } => None,
161 })
162 }
163
164 pub fn function_call(&self, expected_call_id: &CallId) -> Option<(Option<&str>, &str)> {
165 self.input.iter().find_map(|item| match item {
166 InputItem::FunctionCall {
167 call_id,
168 namespace,
169 name,
170 ..
171 } if call_id == expected_call_id => Some((namespace.as_deref(), name.as_str())),
172 InputItem::Message { .. }
173 | InputItem::FunctionCall { .. }
174 | InputItem::FunctionCallOutput { .. } => None,
175 })
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
181pub enum InputItem {
182 Message {
183 role: MessageRole,
184 content: Vec<ContentBlock>,
185 },
186 FunctionCall {
187 call_id: CallId,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 namespace: Option<String>,
190 name: String,
191 arguments: Value,
192 },
193 FunctionCallOutput {
194 call_id: CallId,
195 output: Value,
196 },
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(rename_all = "snake_case")]
201pub enum MessageRole {
202 Developer,
203 System,
204 User,
205 Assistant,
206}
207
208#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
209#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
210pub enum ContentBlock {
211 Text { text: String },
212 ImageReference { url: String },
213}
214
215#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
216#[serde(deny_unknown_fields)]
217pub struct FunctionTool {
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub namespace: Option<String>,
220 pub name: String,
221 pub description: String,
222 pub parameters: Value,
223 pub strict: bool,
224}
225
226pub const MAX_TOOL_COMPONENT_BYTES: usize = 64;
227
228pub fn validate_tool_name(value: &str) -> Result<(), ValidationError> {
229 validate_tool_component(value)
230}
231
232pub fn validate_tool_namespace(value: &str) -> Result<(), ValidationError> {
233 validate_tool_component(value)?;
234 if matches!(
235 value,
236 "function" | "custom" | "namespace" | "tool_search" | "web_search"
237 ) {
238 return Err(ValidationError::InvalidToolDefinition);
239 }
240 Ok(())
241}
242
243fn validate_tool_component(value: &str) -> Result<(), ValidationError> {
244 if value.is_empty()
245 || value.len() > MAX_TOOL_COMPONENT_BYTES
246 || !value
247 .bytes()
248 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
249 {
250 return Err(ValidationError::InvalidToolDefinition);
251 }
252 Ok(())
253}
254
255fn validate_tools(tools: &[FunctionTool]) -> Result<(), ValidationError> {
256 let mut identities = HashSet::new();
257 let mut namespaces = HashSet::new();
258 let mut direct_names = HashSet::new();
259 for tool in tools {
260 validate_tool_name(&tool.name)?;
261 if let Some(namespace) = &tool.namespace {
262 validate_tool_namespace(namespace)?;
263 namespaces.insert(namespace.as_str());
264 } else {
265 direct_names.insert(tool.name.as_str());
266 }
267 if !identities.insert((tool.namespace.as_deref(), tool.name.as_str())) {
268 return Err(ValidationError::InvalidToolDefinition);
269 }
270 }
271 if namespaces
272 .iter()
273 .any(|namespace| direct_names.contains(namespace))
274 {
275 return Err(ValidationError::InvalidToolDefinition);
276 }
277 Ok(())
278}
279
280#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282pub enum ToolChoice {
283 #[default]
284 Auto,
285 None,
286 Required,
287 Named(String),
288}
289
290#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
291#[serde(deny_unknown_fields)]
292pub struct StructuredOutputIntent {
293 pub name: String,
294 pub schema: Value,
295 pub strict: bool,
296}
297
298#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
299#[serde(rename_all = "snake_case")]
300pub enum ReasoningIntent {
301 #[default]
302 None,
303 PublicSummary,
304}
305
306#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(deny_unknown_fields)]
308pub struct CapabilityRequirements {
309 #[serde(default)]
310 pub images: bool,
311 #[serde(default)]
312 pub structured_output: bool,
313 #[serde(default)]
314 pub public_reasoning: bool,
315 #[serde(default)]
316 pub audio: bool,
317}
318
319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
320pub struct ProviderCapabilities {
321 pub images: bool,
322 pub structured_output: bool,
323 pub public_reasoning: bool,
324 pub audio: bool,
325}
326
327impl CapabilityRequirements {
328 pub fn preflight(&self, supported: ProviderCapabilities) -> Result<(), PublicErrorCode> {
329 if (self.images && !supported.images)
330 || (self.structured_output && !supported.structured_output)
331 || (self.public_reasoning && !supported.public_reasoning)
332 || (self.audio && !supported.audio)
333 {
334 return Err(PublicErrorCode::UnsupportedCapability);
335 }
336 Ok(())
337 }
338}
339
340#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(rename_all = "snake_case")]
342pub enum PublicErrorCode {
343 InvalidRequest,
344 AuthenticationRequired,
345 ModelNotFound,
346 UnsupportedCapability,
347 UnsupportedWireCombination,
348 RouterUnavailable,
349 CorrelationMismatch,
350 ProviderFailure,
351}
352
353impl PublicErrorCode {
354 pub fn as_str(&self) -> &'static str {
355 match self {
356 Self::InvalidRequest => "invalid_request",
357 Self::AuthenticationRequired => "authentication_required",
358 Self::ModelNotFound => "model_not_found",
359 Self::UnsupportedCapability => "unsupported_capability",
360 Self::UnsupportedWireCombination => "unsupported_wire_combination",
361 Self::RouterUnavailable => "router_unavailable",
362 Self::CorrelationMismatch => "correlation_mismatch",
363 Self::ProviderFailure => "provider_failure",
364 }
365 }
366}
367
368#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
369#[serde(deny_unknown_fields)]
370pub struct PublicError {
371 pub code: PublicErrorCode,
372 pub message: String,
373 pub request_id: RequestId,
374 pub retryable: bool,
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub details: Option<Value>,
377}
378
379#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
380#[serde(rename_all = "snake_case")]
381pub enum InterruptionCause {
382 ProviderFailure,
383 MalformedStream,
384 Timeout,
385 Cancelled,
386}
387
388impl InterruptionCause {
389 const fn as_str(self) -> &'static str {
390 match self {
391 Self::ProviderFailure => "provider_failure",
392 Self::MalformedStream => "malformed_stream",
393 Self::Timeout => "timeout",
394 Self::Cancelled => "cancelled",
395 }
396 }
397}
398
399#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
400#[serde(rename_all = "snake_case")]
401pub enum SafeResumeBoundary {
402 PublicOutput,
403 ToolCall,
404 ForwardedToolResult,
405}
406
407impl SafeResumeBoundary {
408 const fn as_str(self) -> &'static str {
409 match self {
410 Self::PublicOutput => "public_output",
411 Self::ToolCall => "tool_call",
412 Self::ForwardedToolResult => "forwarded_tool_result",
413 }
414 }
415}
416
417impl PublicError {
418 pub fn redacted(code: PublicErrorCode, request_id: RequestId, retryable: bool) -> Self {
419 let message = match code {
420 PublicErrorCode::InvalidRequest => "The request is invalid.",
421 PublicErrorCode::AuthenticationRequired => "Local authentication is required.",
422 PublicErrorCode::ModelNotFound => "The requested model alias is unavailable.",
423 PublicErrorCode::UnsupportedCapability => {
424 "The route does not support a required capability."
425 }
426 PublicErrorCode::UnsupportedWireCombination => {
427 "The requested wire combination is unsupported."
428 }
429 PublicErrorCode::RouterUnavailable => "The local router is unavailable.",
430 PublicErrorCode::CorrelationMismatch => "The tool result correlation is invalid.",
431 PublicErrorCode::ProviderFailure => "The transformed provider failed.",
432 };
433 Self {
434 code,
435 message: message.to_owned(),
436 request_id,
437 retryable,
438 details: None,
439 }
440 }
441
442 pub fn interrupted(
443 request_id: RequestId,
444 cause: InterruptionCause,
445 boundary: SafeResumeBoundary,
446 ) -> Self {
447 let mut error = Self::redacted(PublicErrorCode::ProviderFailure, request_id, false);
448 error.message = "The provider stream stopped after a non-replayable boundary.".to_owned();
449 error.details = Some(json!({
450 "interruption": {
451 "cause": cause.as_str(),
452 "boundary": boundary.as_str(),
453 "automatic_replay": false,
454 "safe_resume": {
455 "action": "start_new_turn",
456 "preserve_visible_output": true,
457 "repeat_completed_tool_calls": false
458 }
459 }
460 }));
461 error
462 }
463}
464
465#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
469#[serde(tag = "type", rename_all = "snake_case")]
470pub enum SubagentDisposition {
471 Native,
473 Emulated,
476 Refused { reason: String },
479}
480
481impl SubagentDisposition {
482 pub fn derive(native_selected: bool, emulation_enabled: bool) -> Self {
487 if native_selected {
488 Self::Native
489 } else if emulation_enabled {
490 Self::Emulated
491 } else {
492 Self::Refused {
493 reason: "no_phase_serves_subagents_natively_and_emulation_is_disabled".to_owned(),
494 }
495 }
496 }
497}
498
499#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
500#[serde(tag = "type")]
501pub enum CanonicalEvent {
502 #[serde(rename = "response.created")]
503 ResponseCreated {
504 response_id: ResponseId,
505 request_id: RequestId,
506 sequence: u64,
507 },
508 #[serde(rename = "response.in_progress")]
509 ResponseInProgress {
510 response_id: ResponseId,
511 sequence: u64,
512 },
513 #[serde(rename = "response.output_item.added")]
514 OutputItemAdded {
515 response_id: ResponseId,
516 item_id: ItemId,
517 sequence: u64,
518 item_kind: OutputItemKind,
519 #[serde(skip_serializing_if = "Option::is_none")]
520 call_id: Option<CallId>,
521 #[serde(skip_serializing_if = "Option::is_none")]
522 name: Option<String>,
523 #[serde(skip_serializing_if = "Option::is_none")]
524 namespace: Option<String>,
525 },
526 #[serde(rename = "response.output_item.done")]
527 OutputItemDone {
528 response_id: ResponseId,
529 item_id: ItemId,
530 sequence: u64,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 call_id: Option<CallId>,
533 },
534 #[serde(rename = "response.content_part.added")]
535 ContentPartAdded {
536 response_id: ResponseId,
537 item_id: ItemId,
538 sequence: u64,
539 },
540 #[serde(rename = "response.content_part.done")]
541 ContentPartDone {
542 response_id: ResponseId,
543 item_id: ItemId,
544 sequence: u64,
545 },
546 #[serde(rename = "response.output_text.delta")]
547 OutputTextDelta {
548 response_id: ResponseId,
549 item_id: ItemId,
550 sequence: u64,
551 delta: String,
552 },
553 #[serde(rename = "response.output_text.done")]
554 OutputTextDone {
555 response_id: ResponseId,
556 item_id: ItemId,
557 sequence: u64,
558 text: String,
559 },
560 #[serde(rename = "response.function_call_arguments.delta")]
561 FunctionCallArgumentsDelta {
562 response_id: ResponseId,
563 item_id: ItemId,
564 call_id: CallId,
565 sequence: u64,
566 delta: String,
567 },
568 #[serde(rename = "response.function_call_arguments.done")]
569 FunctionCallArgumentsDone {
570 response_id: ResponseId,
571 item_id: ItemId,
572 call_id: CallId,
573 sequence: u64,
574 arguments: Value,
575 },
576 #[serde(rename = "response.completed")]
577 ResponseCompleted {
578 response_id: ResponseId,
579 sequence: u64,
580 usage: Usage,
581 },
582 #[serde(rename = "response.failed")]
583 ResponseFailed {
584 response_id: ResponseId,
585 sequence: u64,
586 error: PublicError,
587 },
588 #[serde(rename = "error")]
589 Error { sequence: u64, error: PublicError },
590}
591
592impl CanonicalEvent {
593 pub fn event_name(&self) -> &'static str {
594 match self {
595 Self::ResponseCreated { .. } => "response.created",
596 Self::ResponseInProgress { .. } => "response.in_progress",
597 Self::OutputItemAdded { .. } => "response.output_item.added",
598 Self::OutputItemDone { .. } => "response.output_item.done",
599 Self::ContentPartAdded { .. } => "response.content_part.added",
600 Self::ContentPartDone { .. } => "response.content_part.done",
601 Self::OutputTextDelta { .. } => "response.output_text.delta",
602 Self::OutputTextDone { .. } => "response.output_text.done",
603 Self::FunctionCallArgumentsDelta { .. } => "response.function_call_arguments.delta",
604 Self::FunctionCallArgumentsDone { .. } => "response.function_call_arguments.done",
605 Self::ResponseCompleted { .. } => "response.completed",
606 Self::ResponseFailed { .. } => "response.failed",
607 Self::Error { .. } => "error",
608 }
609 }
610
611 pub fn visibility(&self) -> EventVisibility {
612 match self {
613 Self::OutputTextDelta { .. }
614 | Self::OutputTextDone { .. }
615 | Self::ContentPartAdded { .. }
616 | Self::ContentPartDone { .. } => EventVisibility::PublicOutput,
617 Self::OutputItemAdded {
618 item_kind: OutputItemKind::FunctionCall,
619 ..
620 }
621 | Self::FunctionCallArgumentsDelta { .. }
622 | Self::FunctionCallArgumentsDone { .. } => EventVisibility::ToolCall,
623 _ => EventVisibility::None,
624 }
625 }
626}
627
628#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
629#[serde(rename_all = "snake_case")]
630pub enum OutputItemKind {
631 Message,
632 FunctionCall,
633}
634
635#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(deny_unknown_fields)]
637pub struct Usage {
638 pub input_tokens: u64,
639 pub output_tokens: u64,
640}
641
642#[derive(Clone, Copy, Debug, PartialEq, Eq)]
643pub enum EventVisibility {
644 None,
645 PublicOutput,
646 ToolCall,
647}
648
649#[derive(Clone, Copy, Debug, PartialEq, Eq)]
650pub enum VisibilityState {
651 PreVisible,
652 VisibleOutput,
653 ToolVisible,
654 VisibleOutputAndTool,
655 TransportAmbiguous,
656}
657
658#[derive(Debug)]
659pub struct ReplayGuard {
660 state: VisibilityState,
661}
662
663#[derive(Debug)]
664pub struct ReplayAuthorization(());
665
666impl Default for ReplayGuard {
667 fn default() -> Self {
668 Self {
669 state: VisibilityState::PreVisible,
670 }
671 }
672}
673
674impl ReplayGuard {
675 pub fn observe(&mut self, visibility: EventVisibility) {
676 self.state = match (self.state, visibility) {
677 (state, EventVisibility::None) => state,
678 (VisibilityState::PreVisible, EventVisibility::PublicOutput) => {
679 VisibilityState::VisibleOutput
680 }
681 (VisibilityState::PreVisible, EventVisibility::ToolCall) => {
682 VisibilityState::ToolVisible
683 }
684 (VisibilityState::VisibleOutput, EventVisibility::ToolCall)
685 | (VisibilityState::ToolVisible, EventVisibility::PublicOutput) => {
686 VisibilityState::VisibleOutputAndTool
687 }
688 (state, EventVisibility::PublicOutput | EventVisibility::ToolCall) => state,
689 };
690 }
691
692 pub fn mark_transport_ambiguous(&mut self) {
693 self.state = VisibilityState::TransportAmbiguous;
694 }
695
696 pub fn mark_tool_result_forwarded(&mut self) {
697 self.state = VisibilityState::ToolVisible;
698 }
699
700 pub fn authorize_transient_retry(&self) -> Option<ReplayAuthorization> {
701 (self.state == VisibilityState::PreVisible).then_some(ReplayAuthorization(()))
702 }
703
704 pub fn state(&self) -> VisibilityState {
705 self.state
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use serde_json::json;
713
714 #[test]
715 fn strict_request_rejects_hidden_reasoning_and_unknown_fields() {
716 let request = json!({
717 "wire_version": M1_WIRE_VERSION,
718 "model": "router-auto",
719 "stream": true,
720 "harness_instance": "cli",
721 "session_id": "session-1",
722 "turn_id": "turn-1",
723 "input": [],
724 "encrypted_reasoning": "must-not-cross"
725 });
726 assert!(serde_json::from_value::<CanonicalRequest>(request).is_err());
727 }
728
729 #[test]
730 fn identifier_deserialization_enforces_constructor_invariants() {
731 for invalid in [json!(""), json!("line\nbreak"), json!("x".repeat(129))] {
732 assert!(serde_json::from_value::<SessionId>(invalid).is_err());
733 }
734 assert_eq!(
735 serde_json::from_value::<SessionId>(json!("session-safe_1"))
736 .expect("valid identifier")
737 .as_str(),
738 "session-safe_1"
739 );
740 }
741
742 #[test]
743 fn replay_truth_table_is_fail_closed() {
744 let mut pre = ReplayGuard::default();
745 assert!(pre.authorize_transient_retry().is_some());
746
747 pre.observe(EventVisibility::PublicOutput);
748 assert!(pre.authorize_transient_retry().is_none());
749
750 let mut tool = ReplayGuard::default();
751 tool.observe(EventVisibility::ToolCall);
752 assert!(tool.authorize_transient_retry().is_none());
753
754 let mut both = ReplayGuard::default();
755 both.observe(EventVisibility::PublicOutput);
756 both.observe(EventVisibility::ToolCall);
757 assert!(both.authorize_transient_retry().is_none());
758
759 let mut ambiguous = ReplayGuard::default();
760 ambiguous.mark_transport_ambiguous();
761 assert!(ambiguous.authorize_transient_retry().is_none());
762
763 let mut forwarded_result = ReplayGuard::default();
764 forwarded_result.mark_tool_result_forwarded();
765 assert!(forwarded_result.authorize_transient_retry().is_none());
766 }
767
768 #[test]
769 fn structured_interruption_has_one_safe_non_replay_resume_action() {
770 let error = PublicError::interrupted(
771 RequestId::parse("request-interrupted").expect("fixture ID"),
772 InterruptionCause::ProviderFailure,
773 SafeResumeBoundary::ForwardedToolResult,
774 );
775 assert_eq!(error.code, PublicErrorCode::ProviderFailure);
776 assert!(!error.retryable);
777 assert_eq!(
778 error.details,
779 Some(json!({
780 "interruption": {
781 "cause": "provider_failure",
782 "boundary": "forwarded_tool_result",
783 "automatic_replay": false,
784 "safe_resume": {
785 "action": "start_new_turn",
786 "preserve_visible_output": true,
787 "repeat_completed_tool_calls": false
788 }
789 }
790 }))
791 );
792 }
793
794 #[test]
795 fn named_tool_choice_requires_one_unambiguous_declared_tool() {
796 let function = |namespace: Option<&str>| FunctionTool {
797 namespace: namespace.map(str::to_owned),
798 name: "lookup".to_owned(),
799 description: "fixture".to_owned(),
800 parameters: json!({}),
801 strict: true,
802 };
803 let mut request = CanonicalRequest {
804 wire_version: M1_WIRE_VERSION.to_owned(),
805 model: "fixture-transformed".to_owned(),
806 stream: true,
807 harness_instance: HarnessInstanceId::parse("cli").expect("fixture ID"),
808 session_id: SessionId::parse("session").expect("fixture ID"),
809 turn_id: TurnId::parse("turn").expect("fixture ID"),
810 input: Vec::new(),
811 tools: vec![function(None)],
812 tool_choice: ToolChoice::Named("lookup".to_owned()),
813 structured_output: None,
814 reasoning: ReasoningIntent::None,
815 max_output_tokens: None,
816 max_spend_micro_usd: None,
817 capabilities: CapabilityRequirements::default(),
818 };
819 assert_eq!(request.validate(), Ok(()));
820 request.tools = vec![function(Some("one")), function(Some("two"))];
821 assert_eq!(
822 request.validate(),
823 Err(ValidationError::InvalidToolDefinition)
824 );
825 request.tools = vec![function(None)];
826 request.tool_choice = ToolChoice::Named("other".to_owned());
827 assert_eq!(
828 request.validate(),
829 Err(ValidationError::InvalidToolDefinition)
830 );
831 }
832
833 #[test]
834 fn spend_authorization_is_bounded_and_zero_means_metered_spend_forbidden() {
835 let base = CanonicalRequest {
836 wire_version: M1_WIRE_VERSION.to_owned(),
837 model: "router-auto".to_owned(),
838 stream: true,
839 harness_instance: HarnessInstanceId::parse("desktop").expect("fixture ID"),
840 session_id: SessionId::parse("session-spend").expect("fixture ID"),
841 turn_id: TurnId::parse("turn-spend").expect("fixture ID"),
842 input: Vec::new(),
843 tools: Vec::new(),
844 tool_choice: ToolChoice::None,
845 structured_output: None,
846 reasoning: ReasoningIntent::None,
847 max_output_tokens: None,
848 max_spend_micro_usd: None,
849 capabilities: CapabilityRequirements::default(),
850 };
851 assert_eq!(base.validate(), Ok(()));
852
853 let mut zero = base.clone();
854 zero.max_spend_micro_usd = Some(0);
855 assert_eq!(zero.validate(), Ok(()));
856
857 let mut at_bound = base.clone();
858 at_bound.max_spend_micro_usd = Some(MAX_SPEND_AUTHORIZATION_MICRO_USD);
859 assert_eq!(at_bound.validate(), Ok(()));
860
861 let mut absurd = base.clone();
862 absurd.max_spend_micro_usd = Some(MAX_SPEND_AUTHORIZATION_MICRO_USD + 1);
863 assert_eq!(
864 absurd.validate(),
865 Err(ValidationError::InvalidSpendAuthorization)
866 );
867 }
868
869 #[test]
870 fn required_capability_is_never_silently_removed() {
871 let required = CapabilityRequirements {
872 images: true,
873 structured_output: true,
874 public_reasoning: true,
875 audio: false,
876 };
877 let supported = ProviderCapabilities {
878 images: true,
879 structured_output: false,
880 public_reasoning: true,
881 audio: false,
882 };
883 assert_eq!(
884 required.preflight(supported),
885 Err(PublicErrorCode::UnsupportedCapability)
886 );
887 }
888
889 #[test]
890 fn canonical_fixture_round_trip_preserves_supported_semantics() {
891 let request = CanonicalRequest {
892 wire_version: M1_WIRE_VERSION.to_owned(),
893 model: "fixture-transformed".to_owned(),
894 stream: true,
895 harness_instance: HarnessInstanceId::parse("desktop").expect("fixture ID"),
896 session_id: SessionId::parse("session-semantic").expect("fixture ID"),
897 turn_id: TurnId::parse("turn-semantic").expect("fixture ID"),
898 input: vec![
899 InputItem::Message {
900 role: MessageRole::Developer,
901 content: vec![
902 ContentBlock::Text {
903 text: "public fixture".to_owned(),
904 },
905 ContentBlock::ImageReference {
906 url: "fixture://image-1".to_owned(),
907 },
908 ],
909 },
910 InputItem::FunctionCall {
911 call_id: CallId::parse("call-semantic").expect("fixture ID"),
912 namespace: Some("catalog".to_owned()),
913 name: "lookup".to_owned(),
914 arguments: json!({"key": "value"}),
915 },
916 InputItem::FunctionCallOutput {
917 call_id: CallId::parse("call-semantic").expect("fixture ID"),
918 output: json!({"result": 42}),
919 },
920 ],
921 tools: vec![FunctionTool {
922 namespace: Some("catalog".to_owned()),
923 name: "lookup".to_owned(),
924 description: "Synthetic lookup".to_owned(),
925 parameters: json!({"type": "object", "properties": {"key": {"type": "string"}}}),
926 strict: true,
927 }],
928 tool_choice: ToolChoice::Named("lookup".to_owned()),
929 structured_output: Some(StructuredOutputIntent {
930 name: "fixture_result".to_owned(),
931 schema: json!({"type": "object", "properties": {"result": {"type": "integer"}}}),
932 strict: true,
933 }),
934 reasoning: ReasoningIntent::PublicSummary,
935 max_output_tokens: Some(128),
936 max_spend_micro_usd: None,
937 capabilities: CapabilityRequirements {
938 images: true,
939 structured_output: true,
940 public_reasoning: true,
941 audio: false,
942 },
943 };
944 let encoded = serde_json::to_vec(&request).expect("serialize canonical fixture");
945 let decoded: CanonicalRequest =
946 serde_json::from_slice(&encoded).expect("deserialize canonical fixture");
947 assert_eq!(decoded, request);
948 assert_eq!(decoded.tools[0].namespace.as_deref(), Some("catalog"));
949
950 let failed = CanonicalEvent::ResponseFailed {
951 response_id: ResponseId::parse("response-semantic").expect("fixture ID"),
952 sequence: 17,
953 error: PublicError::redacted(
954 PublicErrorCode::ProviderFailure,
955 RequestId::parse("request-semantic").expect("fixture ID"),
956 false,
957 ),
958 };
959 let failed_round_trip: CanonicalEvent = serde_json::from_value(
960 serde_json::to_value(&failed).expect("serialize terminal failure"),
961 )
962 .expect("deserialize terminal failure");
963 assert_eq!(failed_round_trip, failed);
964 }
965
966 #[test]
967 fn canonical_tool_namespaces_reject_duplicates_reserved_names_and_collisions() {
968 let function = |namespace: Option<&str>, name: &str| FunctionTool {
969 namespace: namespace.map(str::to_owned),
970 name: name.to_owned(),
971 description: "fixture".to_owned(),
972 parameters: json!({}),
973 strict: true,
974 };
975 assert_eq!(
976 validate_tools(&[
977 function(Some("collaboration"), "spawn_agent"),
978 function(Some("collaboration"), "spawn_agent"),
979 ]),
980 Err(ValidationError::InvalidToolDefinition)
981 );
982 assert_eq!(
983 validate_tools(&[
984 function(None, "collaboration"),
985 function(Some("collaboration"), "spawn_agent"),
986 ]),
987 Err(ValidationError::InvalidToolDefinition)
988 );
989 assert_eq!(
990 validate_tool_namespace("namespace"),
991 Err(ValidationError::InvalidToolDefinition)
992 );
993 assert_eq!(
994 validate_tool_name(&"x".repeat(MAX_TOOL_COMPONENT_BYTES + 1)),
995 Err(ValidationError::InvalidToolDefinition)
996 );
997 }
998
999 #[test]
1000 fn subagent_disposition_is_deterministic_and_never_silent() {
1001 assert_eq!(
1002 SubagentDisposition::derive(true, false),
1003 SubagentDisposition::Native
1004 );
1005 assert_eq!(
1006 SubagentDisposition::derive(true, true),
1007 SubagentDisposition::Native
1008 );
1009 assert_eq!(
1010 SubagentDisposition::derive(false, true),
1011 SubagentDisposition::Emulated
1012 );
1013 assert_eq!(
1014 SubagentDisposition::derive(false, false),
1015 SubagentDisposition::Refused {
1016 reason: "no_phase_serves_subagents_natively_and_emulation_is_disabled".to_owned()
1017 }
1018 );
1019 }
1020}