1#![forbid(unsafe_code)]
4
5pub mod anthropic_chat;
8pub mod catalog;
9pub mod catalog_authority;
10pub mod catalog_authority_payload;
11pub mod catalog_discovery;
12mod continuity;
13pub mod gemini_chat;
14pub mod http_transport;
15#[cfg(any(target_os = "linux", target_os = "macos"))]
16pub mod local_credential_store;
17#[cfg(not(any(target_os = "linux", target_os = "macos")))]
18#[path = "local_credential_store_stub.rs"]
19pub mod local_credential_store;
20pub mod local_credential_types;
21pub mod ollama_chat;
22pub mod openai_compat_chat;
23pub mod openrouter_chat;
24pub mod usage_ledger;
25
26pub use continuity::{
27 AdapterBoundaryError, AdapterCapabilityDispositions, AdapterDisposition,
28 BoundFixtureContinuityAdapter, FailureClass, FixtureAdapterKind, FixtureContinuityAdapter,
29 PreparedProviderDispatch, ProviderAdapter, ProviderFailureEvidence, ProviderRoute,
30 RawProviderResponse, classify_failure, classify_raw_provider_response,
31};
32
33use router_protocol::{
34 AttemptId, CallId, CanonicalEvent, CanonicalRequest, FunctionTool, HarnessInstanceId,
35 InterruptionCause, ItemId, OutputItemKind, ProviderCapabilities, PublicError, PublicErrorCode,
36 ReplayGuard, RequestId, ResponseId, SafeResumeBoundary, SessionId, ToolChoice, TurnId, Usage,
37};
38use serde_json::{Value, json};
39use std::collections::HashMap;
40use std::sync::{Arc, Mutex, Weak};
41
42const MAX_PENDING_CALLS: usize = 128;
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub enum FixtureBehavior {
46 #[default]
47 Normal,
48 PreVisibleTransientFailure,
49 MalformedStreamBeforeVisibility,
50 MalformedStreamAfterVisibility,
51 TimeoutBeforeVisibility,
52 TimeoutAfterVisibility,
53 CancelledBeforeVisibility,
54 CancelledAfterVisibility,
55 PostVisibleFailure,
56 PostToolFailure,
57}
58
59#[derive(Clone, Debug, PartialEq)]
60pub struct TransformedFixtureRequest {
61 pub fixture_wire: &'static str,
62 pub correlation: String,
63 pub messages: Vec<Value>,
64 pub tool_names: Vec<String>,
65 pub tool_selection: Option<ToolSelectionDiagnostics>,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum ToolSelectionStage {
70 ToolChoiceNone,
71 SafeZeroRequired,
72 ReviewedRequiredPrimitive,
73 NoReviewedSafeTool,
74 Ineligible,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum ReviewedSafeCategory {
79 CollaborationListAgents,
80 ListMcpResources,
81 ListMcpResourceTemplates,
82 GetGoal,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub struct ToolSelectionDiagnostics {
87 pub stage: ToolSelectionStage,
88 pub total_tools: usize,
89 pub zero_required_candidates: usize,
90 pub reviewed_safe_matches: usize,
91 pub reviewed_identity_matches: usize,
92 pub collaboration_wait_agent_matches: usize,
93 pub matched_category: Option<ReviewedSafeCategory>,
94}
95
96#[derive(Clone, Debug)]
97struct CorrelationKey {
98 harness_instance: HarnessInstanceId,
99 session_id: SessionId,
100 turn_id: TurnId,
101 namespace: Option<String>,
102 name: String,
103}
104
105#[derive(Clone, Debug, PartialEq)]
106struct SelectedFixtureTool {
107 namespace: Option<String>,
108 name: String,
109 arguments: Value,
110}
111
112#[derive(Default)]
113struct ProviderState {
114 pending: HashMap<CallId, CorrelationKey>,
115}
116
117#[derive(Clone, Default)]
118pub struct FixtureProvider {
119 state: Arc<Mutex<ProviderState>>,
120}
121
122pub struct PreparedAttempt {
123 pub attempt_id: AttemptId,
124 pub transformed: TransformedFixtureRequest,
125 pub events: Vec<CanonicalEvent>,
126 pending_guard: Option<PendingCallGuard>,
127}
128
129impl PreparedAttempt {
130 pub fn commit(mut self) {
131 if let Some(mut guard) = self.pending_guard.take() {
132 guard.committed = true;
133 }
134 }
135}
136
137struct PendingCallGuard {
138 state: Weak<Mutex<ProviderState>>,
139 call_id: CallId,
140 committed: bool,
141}
142
143impl Drop for PendingCallGuard {
144 fn drop(&mut self) {
145 if self.committed {
146 return;
147 }
148 if let Some(state) = self.state.upgrade()
149 && let Ok(mut state) = state.lock()
150 {
151 state.pending.remove(&self.call_id);
152 }
153 }
154}
155
156impl FixtureProvider {
157 pub const CAPABILITIES: ProviderCapabilities = ProviderCapabilities {
158 images: true,
159 structured_output: true,
160 public_reasoning: true,
161 audio: false,
162 };
163
164 pub fn prepare(
165 &self,
166 request: &CanonicalRequest,
167 request_id: RequestId,
168 attempt_id: AttemptId,
169 behavior: FixtureBehavior,
170 ) -> Result<PreparedAttempt, PublicError> {
171 request.validate().map_err(|error| {
172 let code = match error {
173 router_protocol::ValidationError::StreamingRequired => {
174 PublicErrorCode::UnsupportedWireCombination
175 }
176 router_protocol::ValidationError::NonTranslatableCapability => {
177 PublicErrorCode::UnsupportedCapability
178 }
179 router_protocol::ValidationError::InvalidIdentifier(_)
180 | router_protocol::ValidationError::UnsupportedWireVersion
181 | router_protocol::ValidationError::InvalidToolDefinition
182 | router_protocol::ValidationError::InvalidSpendAuthorization => {
183 PublicErrorCode::InvalidRequest
184 }
185 };
186 PublicError::redacted(code, request_id.clone(), false)
187 })?;
188 request
189 .capabilities
190 .preflight(Self::CAPABILITIES)
191 .map_err(|code| PublicError::redacted(code, request_id.clone(), false))?;
192
193 let transformed = Self::transform(request);
194 let response_id = ResponseId::parse(format!("resp-{attempt_id}")).map_err(|_| {
195 PublicError::redacted(PublicErrorCode::ProviderFailure, request_id.clone(), false)
196 })?;
197
198 if let Some(failure_kind) = pre_visible_failure_kind(behavior) {
199 let mut replay = ReplayGuard::default();
200 let forwarded_tool_result = request.function_output().is_some();
201 if forwarded_tool_result {
202 replay.mark_tool_result_forwarded();
203 }
204 let retryable =
205 failure_kind.retryable() && replay.authorize_transient_retry().is_some();
206 let error = if forwarded_tool_result {
207 PublicError::interrupted(
208 request_id,
209 failure_kind.interruption_cause(),
210 SafeResumeBoundary::ForwardedToolResult,
211 )
212 } else {
213 fixture_failure_error(request_id, failure_kind, retryable)
214 };
215 return Ok(PreparedAttempt {
216 attempt_id,
217 transformed,
218 events: vec![CanonicalEvent::ResponseFailed {
219 response_id,
220 sequence: 0,
221 error,
222 }],
223 pending_guard: None,
224 });
225 }
226
227 if let Some((call_id, _output)) = request.function_output() {
228 return self.prepare_final(
229 request,
230 request_id,
231 attempt_id,
232 response_id,
233 transformed,
234 call_id,
235 );
236 }
237
238 self.prepare_tool_call(
239 request,
240 request_id,
241 attempt_id,
242 response_id,
243 transformed,
244 behavior,
245 )
246 }
247
248 fn prepare_tool_call(
249 &self,
250 request: &CanonicalRequest,
251 request_id: RequestId,
252 attempt_id: AttemptId,
253 response_id: ResponseId,
254 mut transformed: TransformedFixtureRequest,
255 behavior: FixtureBehavior,
256 ) -> Result<PreparedAttempt, PublicError> {
257 if matches!(request.tool_choice, ToolChoice::None) {
258 transformed.tool_selection = Some(ToolSelectionDiagnostics {
259 stage: ToolSelectionStage::ToolChoiceNone,
260 ..tool_selection_diagnostics(&request.tools)
261 });
262 return prepare_no_tool_completion(
263 request_id,
264 attempt_id,
265 response_id,
266 transformed,
267 behavior,
268 );
269 }
270 let selectable_tools = match &request.tool_choice {
271 ToolChoice::Named(name) => request
272 .tools
273 .iter()
274 .find(|tool| tool.name == *name)
275 .map(std::slice::from_ref)
276 .unwrap_or_default(),
277 ToolChoice::Auto | ToolChoice::None | ToolChoice::Required => request.tools.as_slice(),
278 };
279 let selected_tool = match select_fixture_tool(selectable_tools) {
280 Ok((selected, diagnostics)) => {
281 transformed.tool_selection = Some(diagnostics);
282 selected
283 }
284 Err(diagnostics) => {
285 if matches!(request.tool_choice, ToolChoice::Auto | ToolChoice::None) {
286 transformed.tool_selection = Some(ToolSelectionDiagnostics {
287 stage: ToolSelectionStage::NoReviewedSafeTool,
288 ..diagnostics
289 });
290 return prepare_no_tool_completion(
291 request_id,
292 attempt_id,
293 response_id,
294 transformed,
295 behavior,
296 );
297 }
298 let mut error = PublicError::redacted(
299 PublicErrorCode::UnsupportedCapability,
300 request_id,
301 false,
302 );
303 error.details = Some(json!({
304 "failure_stage": "fixture_tool_selector.ineligible",
305 "total_tools": diagnostics.total_tools,
306 "zero_required_candidates": diagnostics.zero_required_candidates,
307 "reviewed_safe_matches": diagnostics.reviewed_safe_matches,
308 "reviewed_identity_matches": diagnostics.reviewed_identity_matches,
309 "collaboration_wait_agent_matches": diagnostics.collaboration_wait_agent_matches
310 }));
311 return Err(error);
312 }
313 };
314 let tool_namespace = selected_tool.namespace;
315 let tool_name = selected_tool.name;
316 let tool_arguments = selected_tool.arguments;
317 let encoded_arguments = serde_json::to_string(&tool_arguments).map_err(|_| {
318 PublicError::redacted(PublicErrorCode::ProviderFailure, request_id.clone(), false)
319 })?;
320 let argument_boundary = encoded_arguments
321 .char_indices()
322 .map(|(index, _)| index)
323 .rfind(|index| *index <= encoded_arguments.len() / 2)
324 .unwrap_or(0);
325 let (argument_prefix, argument_suffix) = encoded_arguments.split_at(argument_boundary);
326 let call_id = CallId::parse(format!("call-{attempt_id}")).map_err(|_| {
327 PublicError::redacted(PublicErrorCode::ProviderFailure, request_id.clone(), false)
328 })?;
329 let item_text = ItemId::parse(format!("text-{attempt_id}")).map_err(|_| {
330 PublicError::redacted(PublicErrorCode::ProviderFailure, request_id.clone(), false)
331 })?;
332 let item_call = ItemId::parse(format!("tool-{attempt_id}")).map_err(|_| {
333 PublicError::redacted(PublicErrorCode::ProviderFailure, request_id.clone(), false)
334 })?;
335
336 let mut events = vec![
337 CanonicalEvent::ResponseCreated {
338 response_id: response_id.clone(),
339 request_id: request_id.clone(),
340 sequence: 0,
341 },
342 CanonicalEvent::ResponseInProgress {
343 response_id: response_id.clone(),
344 sequence: 1,
345 },
346 CanonicalEvent::OutputItemAdded {
347 response_id: response_id.clone(),
348 item_id: item_text.clone(),
349 sequence: 2,
350 item_kind: OutputItemKind::Message,
351 call_id: None,
352 name: None,
353 namespace: None,
354 },
355 CanonicalEvent::ContentPartAdded {
356 response_id: response_id.clone(),
357 item_id: item_text.clone(),
358 sequence: 3,
359 },
360 CanonicalEvent::OutputTextDelta {
361 response_id: response_id.clone(),
362 item_id: item_text.clone(),
363 sequence: 4,
364 delta: "Checking the fixture. ".to_owned(),
365 },
366 CanonicalEvent::OutputTextDone {
367 response_id: response_id.clone(),
368 item_id: item_text.clone(),
369 sequence: 5,
370 text: "Checking the fixture. ".to_owned(),
371 },
372 CanonicalEvent::ContentPartDone {
373 response_id: response_id.clone(),
374 item_id: item_text.clone(),
375 sequence: 6,
376 },
377 CanonicalEvent::OutputItemDone {
378 response_id: response_id.clone(),
379 item_id: item_text,
380 sequence: 7,
381 call_id: None,
382 },
383 ];
384
385 let mut pending_guard = None;
386 if let Some(cause) = post_visible_failure_cause(behavior) {
387 events.push(CanonicalEvent::ResponseFailed {
388 response_id,
389 sequence: 8,
390 error: PublicError::interrupted(
391 request_id,
392 cause,
393 SafeResumeBoundary::PublicOutput,
394 ),
395 });
396 } else {
397 {
398 let mut state = self.state.lock().map_err(|_| {
399 PublicError::redacted(
400 PublicErrorCode::RouterUnavailable,
401 request_id.clone(),
402 true,
403 )
404 })?;
405 if state.pending.len() >= MAX_PENDING_CALLS {
406 return Err(PublicError::redacted(
407 PublicErrorCode::RouterUnavailable,
408 request_id,
409 true,
410 ));
411 }
412 state.pending.insert(
413 call_id.clone(),
414 CorrelationKey {
415 harness_instance: request.harness_instance.clone(),
416 session_id: request.session_id.clone(),
417 turn_id: request.turn_id.clone(),
418 namespace: tool_namespace.clone(),
419 name: tool_name.clone(),
420 },
421 );
422 }
423 pending_guard = Some(PendingCallGuard {
424 state: Arc::downgrade(&self.state),
425 call_id: call_id.clone(),
426 committed: false,
427 });
428 events.extend([
429 CanonicalEvent::OutputItemAdded {
430 response_id: response_id.clone(),
431 item_id: item_call.clone(),
432 sequence: 8,
433 item_kind: OutputItemKind::FunctionCall,
434 call_id: Some(call_id.clone()),
435 name: Some(tool_name),
436 namespace: tool_namespace,
437 },
438 CanonicalEvent::FunctionCallArgumentsDelta {
439 response_id: response_id.clone(),
440 item_id: item_call.clone(),
441 call_id: call_id.clone(),
442 sequence: 9,
443 delta: argument_prefix.to_owned(),
444 },
445 CanonicalEvent::FunctionCallArgumentsDelta {
446 response_id: response_id.clone(),
447 item_id: item_call.clone(),
448 call_id: call_id.clone(),
449 sequence: 10,
450 delta: argument_suffix.to_owned(),
451 },
452 CanonicalEvent::FunctionCallArgumentsDone {
453 response_id: response_id.clone(),
454 item_id: item_call.clone(),
455 call_id: call_id.clone(),
456 sequence: 11,
457 arguments: tool_arguments,
458 },
459 CanonicalEvent::OutputItemDone {
460 response_id: response_id.clone(),
461 item_id: item_call,
462 sequence: 12,
463 call_id: Some(call_id.clone()),
464 },
465 ]);
466 if behavior == FixtureBehavior::PostToolFailure {
467 events.push(CanonicalEvent::ResponseFailed {
468 response_id,
469 sequence: 13,
470 error: PublicError::interrupted(
471 request_id,
472 InterruptionCause::ProviderFailure,
473 SafeResumeBoundary::ToolCall,
474 ),
475 });
476 } else {
477 events.push(CanonicalEvent::ResponseCompleted {
478 response_id,
479 sequence: 13,
480 usage: Usage {
481 input_tokens: 17,
482 output_tokens: 11,
483 },
484 });
485 }
486 }
487
488 Ok(PreparedAttempt {
489 attempt_id,
490 transformed,
491 events,
492 pending_guard,
493 })
494 }
495
496 fn prepare_final(
497 &self,
498 request: &CanonicalRequest,
499 request_id: RequestId,
500 attempt_id: AttemptId,
501 response_id: ResponseId,
502 transformed: TransformedFixtureRequest,
503 call_id: &CallId,
504 ) -> Result<PreparedAttempt, PublicError> {
505 let mut state = self.state.lock().map_err(|_| {
506 PublicError::redacted(PublicErrorCode::RouterUnavailable, request_id.clone(), true)
507 })?;
508 let expected = state.pending.get(call_id).ok_or_else(|| {
509 PublicError::redacted(
510 PublicErrorCode::CorrelationMismatch,
511 request_id.clone(),
512 false,
513 )
514 })?;
515 if expected.harness_instance != request.harness_instance
516 || expected.session_id != request.session_id
517 || expected.turn_id != request.turn_id
518 {
519 return Err(PublicError::redacted(
520 PublicErrorCode::CorrelationMismatch,
521 request_id,
522 false,
523 ));
524 }
525 if let Some((namespace, name)) = request.function_call(call_id)
526 && (namespace != expected.namespace.as_deref() || name != expected.name)
527 {
528 return Err(PublicError::redacted(
529 PublicErrorCode::CorrelationMismatch,
530 request_id,
531 false,
532 ));
533 }
534 state.pending.remove(call_id);
535 drop(state);
536
537 let item_id = ItemId::parse(format!("final-{attempt_id}")).map_err(|_| {
538 PublicError::redacted(PublicErrorCode::ProviderFailure, request_id.clone(), false)
539 })?;
540 let events = vec![
541 CanonicalEvent::ResponseCreated {
542 response_id: response_id.clone(),
543 request_id,
544 sequence: 0,
545 },
546 CanonicalEvent::ResponseInProgress {
547 response_id: response_id.clone(),
548 sequence: 1,
549 },
550 CanonicalEvent::OutputItemAdded {
551 response_id: response_id.clone(),
552 item_id: item_id.clone(),
553 sequence: 2,
554 item_kind: OutputItemKind::Message,
555 call_id: None,
556 name: None,
557 namespace: None,
558 },
559 CanonicalEvent::ContentPartAdded {
560 response_id: response_id.clone(),
561 item_id: item_id.clone(),
562 sequence: 3,
563 },
564 CanonicalEvent::OutputTextDelta {
565 response_id: response_id.clone(),
566 item_id: item_id.clone(),
567 sequence: 4,
568 delta: "Fixture result accepted.".to_owned(),
569 },
570 CanonicalEvent::OutputTextDone {
571 response_id: response_id.clone(),
572 item_id: item_id.clone(),
573 sequence: 5,
574 text: "Fixture result accepted.".to_owned(),
575 },
576 CanonicalEvent::ContentPartDone {
577 response_id: response_id.clone(),
578 item_id: item_id.clone(),
579 sequence: 6,
580 },
581 CanonicalEvent::OutputItemDone {
582 response_id: response_id.clone(),
583 item_id,
584 sequence: 7,
585 call_id: None,
586 },
587 CanonicalEvent::ResponseCompleted {
588 response_id,
589 sequence: 8,
590 usage: Usage {
591 input_tokens: 9,
592 output_tokens: 4,
593 },
594 },
595 ];
596 Ok(PreparedAttempt {
597 attempt_id,
598 transformed,
599 events,
600 pending_guard: None,
601 })
602 }
603
604 fn transform(request: &CanonicalRequest) -> TransformedFixtureRequest {
605 TransformedFixtureRequest {
606 fixture_wire: "fixture.vendor.v2",
607 correlation: format!("{}:{}", request.session_id, request.turn_id),
608 messages: request
609 .input
610 .iter()
611 .map(|item| serde_json::to_value(item).unwrap_or(Value::Null))
612 .collect(),
613 tool_names: request
614 .tools
615 .iter()
616 .map(|tool| match &tool.namespace {
617 Some(namespace) => format!("{namespace}.{}", tool.name),
618 None => tool.name.clone(),
619 })
620 .collect(),
621 tool_selection: None,
622 }
623 }
624
625 pub fn pending_calls(&self) -> usize {
626 self.state.lock().map_or(0, |state| state.pending.len())
627 }
628}
629
630#[derive(Clone, Copy, Debug, PartialEq, Eq)]
631enum FixtureFailureKind {
632 TransientProvider,
633 MalformedStream,
634 Timeout,
635 Cancelled,
636}
637
638impl FixtureFailureKind {
639 const fn as_str(self) -> &'static str {
640 match self {
641 Self::TransientProvider => "transient_provider_failure",
642 Self::MalformedStream => "malformed_stream",
643 Self::Timeout => "timeout",
644 Self::Cancelled => "cancelled",
645 }
646 }
647
648 const fn retryable(self) -> bool {
649 matches!(self, Self::TransientProvider | Self::Timeout)
650 }
651
652 const fn interruption_cause(self) -> InterruptionCause {
653 match self {
654 Self::TransientProvider => InterruptionCause::ProviderFailure,
655 Self::MalformedStream => InterruptionCause::MalformedStream,
656 Self::Timeout => InterruptionCause::Timeout,
657 Self::Cancelled => InterruptionCause::Cancelled,
658 }
659 }
660}
661
662fn pre_visible_failure_kind(behavior: FixtureBehavior) -> Option<FixtureFailureKind> {
663 match behavior {
664 FixtureBehavior::PreVisibleTransientFailure => Some(FixtureFailureKind::TransientProvider),
665 FixtureBehavior::MalformedStreamBeforeVisibility => {
666 Some(FixtureFailureKind::MalformedStream)
667 }
668 FixtureBehavior::TimeoutBeforeVisibility => Some(FixtureFailureKind::Timeout),
669 FixtureBehavior::CancelledBeforeVisibility => Some(FixtureFailureKind::Cancelled),
670 FixtureBehavior::Normal
671 | FixtureBehavior::MalformedStreamAfterVisibility
672 | FixtureBehavior::TimeoutAfterVisibility
673 | FixtureBehavior::CancelledAfterVisibility
674 | FixtureBehavior::PostVisibleFailure
675 | FixtureBehavior::PostToolFailure => None,
676 }
677}
678
679fn post_visible_failure_cause(behavior: FixtureBehavior) -> Option<InterruptionCause> {
680 match behavior {
681 FixtureBehavior::PostVisibleFailure => Some(InterruptionCause::ProviderFailure),
682 FixtureBehavior::MalformedStreamAfterVisibility => Some(InterruptionCause::MalformedStream),
683 FixtureBehavior::TimeoutAfterVisibility => Some(InterruptionCause::Timeout),
684 FixtureBehavior::CancelledAfterVisibility => Some(InterruptionCause::Cancelled),
685 FixtureBehavior::Normal
686 | FixtureBehavior::PreVisibleTransientFailure
687 | FixtureBehavior::MalformedStreamBeforeVisibility
688 | FixtureBehavior::TimeoutBeforeVisibility
689 | FixtureBehavior::CancelledBeforeVisibility
690 | FixtureBehavior::PostToolFailure => None,
691 }
692}
693
694fn fixture_failure_error(
695 request_id: RequestId,
696 kind: FixtureFailureKind,
697 retryable: bool,
698) -> PublicError {
699 let mut error = PublicError::redacted(PublicErrorCode::ProviderFailure, request_id, retryable);
700 error.details = Some(json!({
701 "fixture_failure": {
702 "kind": kind.as_str(),
703 "visibility": "pre_visible",
704 "retry_eligible": retryable
705 }
706 }));
707 error
708}
709
710fn prepare_no_tool_completion(
711 request_id: RequestId,
712 attempt_id: AttemptId,
713 response_id: ResponseId,
714 transformed: TransformedFixtureRequest,
715 behavior: FixtureBehavior,
716) -> Result<PreparedAttempt, PublicError> {
717 let item_id = ItemId::parse(format!("no-tool-{attempt_id}")).map_err(|_| {
718 PublicError::redacted(PublicErrorCode::ProviderFailure, request_id.clone(), false)
719 })?;
720 let text = "Bounded fixture completed without a tool.".to_owned();
721 let mut events = vec![
722 CanonicalEvent::ResponseCreated {
723 response_id: response_id.clone(),
724 request_id: request_id.clone(),
725 sequence: 0,
726 },
727 CanonicalEvent::ResponseInProgress {
728 response_id: response_id.clone(),
729 sequence: 1,
730 },
731 CanonicalEvent::OutputItemAdded {
732 response_id: response_id.clone(),
733 item_id: item_id.clone(),
734 sequence: 2,
735 item_kind: OutputItemKind::Message,
736 call_id: None,
737 name: None,
738 namespace: None,
739 },
740 CanonicalEvent::ContentPartAdded {
741 response_id: response_id.clone(),
742 item_id: item_id.clone(),
743 sequence: 3,
744 },
745 CanonicalEvent::OutputTextDelta {
746 response_id: response_id.clone(),
747 item_id: item_id.clone(),
748 sequence: 4,
749 delta: text.clone(),
750 },
751 CanonicalEvent::OutputTextDone {
752 response_id: response_id.clone(),
753 item_id: item_id.clone(),
754 sequence: 5,
755 text,
756 },
757 CanonicalEvent::ContentPartDone {
758 response_id: response_id.clone(),
759 item_id: item_id.clone(),
760 sequence: 6,
761 },
762 CanonicalEvent::OutputItemDone {
763 response_id: response_id.clone(),
764 item_id,
765 sequence: 7,
766 call_id: None,
767 },
768 ];
769 if let Some(cause) = post_visible_failure_cause(behavior) {
770 events.push(CanonicalEvent::ResponseFailed {
771 response_id,
772 sequence: 8,
773 error: PublicError::interrupted(request_id, cause, SafeResumeBoundary::PublicOutput),
774 });
775 } else {
776 events.push(CanonicalEvent::ResponseCompleted {
777 response_id,
778 sequence: 8,
779 usage: Usage {
780 input_tokens: 7,
781 output_tokens: 6,
782 },
783 });
784 }
785 Ok(PreparedAttempt {
786 attempt_id,
787 transformed,
788 events,
789 pending_guard: None,
790 })
791}
792
793fn select_fixture_tool(
794 tools: &[FunctionTool],
795) -> Result<(SelectedFixtureTool, ToolSelectionDiagnostics), ToolSelectionDiagnostics> {
796 let base = tool_selection_diagnostics(tools);
797
798 for category in [
799 ReviewedSafeCategory::CollaborationListAgents,
800 ReviewedSafeCategory::ListMcpResources,
801 ReviewedSafeCategory::ListMcpResourceTemplates,
802 ReviewedSafeCategory::GetGoal,
803 ] {
804 if let Some(tool) = tools.iter().find(|tool| {
805 reviewed_safe_category(tool) == Some(category)
806 && schema_accepts_empty_object(&tool.parameters)
807 }) {
808 return Ok((
809 SelectedFixtureTool {
810 namespace: tool.namespace.clone(),
811 name: tool.name.clone(),
812 arguments: json!({}),
813 },
814 ToolSelectionDiagnostics {
815 stage: ToolSelectionStage::SafeZeroRequired,
816 matched_category: Some(category),
817 ..base
818 },
819 ));
820 }
821 }
822
823 if let Some((tool, arguments, category)) = tools.iter().find_map(|tool| {
824 reviewed_required_arguments(tool).map(|(arguments, category)| (tool, arguments, category))
825 }) {
826 return Ok((
827 SelectedFixtureTool {
828 namespace: tool.namespace.clone(),
829 name: tool.name.clone(),
830 arguments,
831 },
832 ToolSelectionDiagnostics {
833 stage: ToolSelectionStage::ReviewedRequiredPrimitive,
834 matched_category: category,
835 ..base
836 },
837 ));
838 }
839
840 Err(base)
841}
842
843fn tool_selection_diagnostics(tools: &[FunctionTool]) -> ToolSelectionDiagnostics {
844 let zero_required_candidates = tools
845 .iter()
846 .filter(|tool| schema_accepts_empty_object(&tool.parameters))
847 .count();
848 let reviewed_safe_matches = tools
849 .iter()
850 .filter(|tool| {
851 reviewed_safe_category(tool).is_some() && schema_accepts_empty_object(&tool.parameters)
852 })
853 .count();
854 let reviewed_identity_matches = tools
855 .iter()
856 .filter(|tool| reviewed_safe_category(tool).is_some())
857 .count();
858 let collaboration_wait_agent_matches = tools
859 .iter()
860 .filter(|tool| {
861 tool.namespace.as_deref() == Some("collaboration")
862 && tool.name == "wait_agent"
863 && schema_accepts_empty_object(&tool.parameters)
864 })
865 .count();
866 ToolSelectionDiagnostics {
867 stage: ToolSelectionStage::Ineligible,
868 total_tools: tools.len(),
869 zero_required_candidates,
870 reviewed_safe_matches,
871 reviewed_identity_matches,
872 collaboration_wait_agent_matches,
873 matched_category: None,
874 }
875}
876
877fn reviewed_safe_category(tool: &FunctionTool) -> Option<ReviewedSafeCategory> {
878 match (tool.namespace.as_deref(), tool.name.as_str()) {
879 (Some("collaboration"), "list_agents") => {
880 Some(ReviewedSafeCategory::CollaborationListAgents)
881 }
882 (None, "list_mcp_resources") => Some(ReviewedSafeCategory::ListMcpResources),
883 (None, "list_mcp_resource_templates") => {
884 Some(ReviewedSafeCategory::ListMcpResourceTemplates)
885 }
886 (None, "get_goal") => Some(ReviewedSafeCategory::GetGoal),
887 _ => None,
888 }
889}
890
891fn schema_accepts_empty_object(schema: &Value) -> bool {
892 if let Some(accepted) = schema.as_bool() {
893 return accepted;
894 }
895 let Some(schema) = schema.as_object() else {
896 return false;
897 };
898 if schema.contains_key("$ref") || schema.contains_key("$dynamicRef") {
899 return false;
900 }
901 if let Some(schema_type) = schema.get("type") {
902 let accepts_object = schema_type.as_str() == Some("object")
903 || schema_type
904 .as_array()
905 .is_some_and(|types| types.iter().any(|schema_type| schema_type == "object"));
906 if !accepts_object {
907 return false;
908 }
909 }
910 if schema
911 .get("const")
912 .is_some_and(|constant| constant != &json!({}))
913 {
914 return false;
915 }
916 if schema
917 .get("enum")
918 .and_then(Value::as_array)
919 .is_some_and(|values| !values.iter().any(|value| value == &json!({})))
920 {
921 return false;
922 }
923 if schema
924 .get("required")
925 .and_then(Value::as_array)
926 .is_some_and(|required| !required.is_empty())
927 {
928 return false;
929 }
930 if schema
931 .get("minProperties")
932 .and_then(Value::as_u64)
933 .is_some_and(|minimum| minimum > 0)
934 {
935 return false;
936 }
937 if schema
938 .get("allOf")
939 .and_then(Value::as_array)
940 .is_some_and(|schemas| !schemas.iter().all(schema_accepts_empty_object))
941 {
942 return false;
943 }
944 if schema
945 .get("anyOf")
946 .and_then(Value::as_array)
947 .is_some_and(|schemas| !schemas.iter().any(schema_accepts_empty_object))
948 {
949 return false;
950 }
951 if schema
952 .get("oneOf")
953 .and_then(Value::as_array)
954 .is_some_and(|schemas| {
955 schemas
956 .iter()
957 .filter(|schema| schema_accepts_empty_object(schema))
958 .count()
959 != 1
960 })
961 {
962 return false;
963 }
964 if schema.get("not").is_some_and(schema_accepts_empty_object) {
965 return false;
966 }
967 if let Some(condition) = schema.get("if") {
968 let branch = if schema_accepts_empty_object(condition) {
969 schema.get("then")
970 } else {
971 schema.get("else")
972 };
973 if branch.is_some_and(|branch| !schema_accepts_empty_object(branch)) {
974 return false;
975 }
976 }
977 true
978}
979
980fn reviewed_required_arguments(
981 tool: &FunctionTool,
982) -> Option<(Value, Option<ReviewedSafeCategory>)> {
983 if tool.namespace.as_deref() == Some("collaboration") && tool.name == "list_agents" {
984 return reviewed_single_string_argument(tool, "path_prefix", "").map(|arguments| {
985 (
986 arguments,
987 Some(ReviewedSafeCategory::CollaborationListAgents),
988 )
989 });
990 }
991 if tool.namespace.is_none() && tool.name == "get_weather" {
992 return reviewed_single_string_argument(tool, "city", "San Francisco")
993 .map(|arguments| (arguments, None));
994 }
995 None
996}
997
998fn reviewed_single_string_argument(
999 tool: &FunctionTool,
1000 property_name: &str,
1001 value: &str,
1002) -> Option<Value> {
1003 let arguments = Value::Object(serde_json::Map::from_iter([(
1004 property_name.to_owned(),
1005 Value::String(value.to_owned()),
1006 )]));
1007 arguments_validate_against_reviewed_schema(&arguments, &tool.parameters, property_name)
1008 .then_some(arguments)
1009}
1010
1011fn arguments_validate_against_reviewed_schema(
1012 arguments: &Value,
1013 schema: &Value,
1014 property_name: &str,
1015) -> bool {
1016 let (Some(arguments), Some(schema)) = (arguments.as_object(), schema.as_object()) else {
1017 return false;
1018 };
1019 if schema.get("type").and_then(Value::as_str) != Some("object") {
1020 return false;
1021 }
1022 let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
1023 return false;
1024 };
1025 let Some(required) = schema.get("required").and_then(Value::as_array) else {
1026 return false;
1027 };
1028 if required.len() != 1 || required[0].as_str() != Some(property_name) {
1029 return false;
1030 }
1031 let Some(property) = properties.get(property_name).and_then(Value::as_object) else {
1032 return false;
1033 };
1034 if property.get("type").and_then(Value::as_str) != Some("string")
1035 || property.contains_key("pattern")
1036 || property.contains_key("enum")
1037 || property.contains_key("const")
1038 || property
1039 .get("minLength")
1040 .and_then(Value::as_u64)
1041 .is_some_and(|minimum| minimum > 0)
1042 {
1043 return false;
1044 }
1045 arguments.len() == 1 && arguments.get(property_name).is_some_and(Value::is_string)
1046}
1047
1048pub fn event_stream_is_retryable(events: &[CanonicalEvent]) -> bool {
1049 let mut guard = ReplayGuard::default();
1050 for event in events {
1051 guard.observe(event.visibility());
1052 if matches!(
1053 event,
1054 CanonicalEvent::ResponseFailed { error, .. } | CanonicalEvent::Error { error, .. }
1055 if !error.retryable
1056 ) {
1057 return false;
1058 }
1059 }
1060 guard.authorize_transient_retry().is_some()
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065 use super::*;
1066 use router_protocol::{
1067 CapabilityRequirements, ContentBlock, FunctionTool, HarnessInstanceId, InputItem,
1068 M1_WIRE_VERSION, MessageRole, ReasoningIntent, ToolChoice,
1069 };
1070
1071 fn request(session: &str, turn: &str, output: Option<CallId>) -> CanonicalRequest {
1072 let input = if let Some(call_id) = output {
1073 vec![InputItem::FunctionCallOutput {
1074 call_id,
1075 output: json!({"temperature": 61}),
1076 }]
1077 } else {
1078 vec![InputItem::Message {
1079 role: MessageRole::User,
1080 content: vec![ContentBlock::Text {
1081 text: "synthetic weather request".to_owned(),
1082 }],
1083 }]
1084 };
1085 CanonicalRequest {
1086 wire_version: M1_WIRE_VERSION.to_owned(),
1087 model: "fixture-transformed".to_owned(),
1088 stream: true,
1089 harness_instance: HarnessInstanceId::parse("synthetic-cli").expect("fixture id"),
1090 session_id: SessionId::parse(session).expect("fixture id"),
1091 turn_id: TurnId::parse(turn).expect("fixture id"),
1092 input,
1093 tools: vec![FunctionTool {
1094 namespace: None,
1095 name: "get_weather".to_owned(),
1096 description: "reviewed fixture tool".to_owned(),
1097 parameters: json!({
1098 "type": "object",
1099 "properties": {"city": {"type": "string"}},
1100 "required": ["city"],
1101 "additionalProperties": false
1102 }),
1103 strict: true,
1104 }],
1105 tool_choice: ToolChoice::Auto,
1106 structured_output: None,
1107 reasoning: ReasoningIntent::PublicSummary,
1108 max_output_tokens: Some(64),
1109 max_spend_micro_usd: None,
1110 capabilities: CapabilityRequirements {
1111 images: false,
1112 structured_output: false,
1113 public_reasoning: true,
1114 audio: false,
1115 },
1116 }
1117 }
1118
1119 fn ids(number: u64) -> (RequestId, AttemptId) {
1120 (
1121 RequestId::parse(format!("request-{number}")).expect("fixture id"),
1122 AttemptId::parse(format!("attempt-{number}")).expect("fixture id"),
1123 )
1124 }
1125
1126 #[test]
1127 fn transformed_two_request_tool_loop_preserves_call_id() {
1128 let provider = FixtureProvider::default();
1129 let (request_id, attempt_id) = ids(1);
1130 let first = provider
1131 .prepare(
1132 &request("session-1", "turn-1", None),
1133 request_id,
1134 attempt_id,
1135 FixtureBehavior::Normal,
1136 )
1137 .expect("first request should prepare");
1138 assert_eq!(first.transformed.fixture_wire, "fixture.vendor.v2");
1139 let call_id = first
1140 .events
1141 .iter()
1142 .find_map(|event| match event {
1143 CanonicalEvent::FunctionCallArgumentsDone { call_id, .. } => Some(call_id.clone()),
1144 _ => None,
1145 })
1146 .expect("fixture must emit function call");
1147 first.commit();
1148
1149 let (request_id, attempt_id) = ids(2);
1150 let second = provider
1151 .prepare(
1152 &request("session-1", "turn-1", Some(call_id.clone())),
1153 request_id,
1154 attempt_id,
1155 FixtureBehavior::Normal,
1156 )
1157 .expect("correlated output should prepare");
1158 assert!(matches!(
1159 second.events.last(),
1160 Some(CanonicalEvent::ResponseCompleted { .. })
1161 ));
1162 assert_eq!(provider.pending_calls(), 0);
1163
1164 let (request_id, attempt_id) = ids(3);
1165 let mismatch = provider.prepare(
1166 &request("session-2", "turn-1", Some(call_id)),
1167 request_id,
1168 attempt_id,
1169 FixtureBehavior::Normal,
1170 );
1171 match mismatch {
1172 Err(error) => assert_eq!(error.code, PublicErrorCode::CorrelationMismatch),
1173 Ok(_) => panic!("call id reuse must fail"),
1174 }
1175 }
1176
1177 #[test]
1178 fn failures_obey_visibility_replay_boundary() {
1179 let provider = FixtureProvider::default();
1180 for (number, behavior, expected_retryable) in [
1181 (10, FixtureBehavior::PreVisibleTransientFailure, true),
1182 (11, FixtureBehavior::MalformedStreamBeforeVisibility, false),
1183 (12, FixtureBehavior::TimeoutBeforeVisibility, true),
1184 (13, FixtureBehavior::CancelledBeforeVisibility, false),
1185 (14, FixtureBehavior::PostVisibleFailure, false),
1186 (15, FixtureBehavior::MalformedStreamAfterVisibility, false),
1187 (16, FixtureBehavior::TimeoutAfterVisibility, false),
1188 (17, FixtureBehavior::PostToolFailure, false),
1189 (18, FixtureBehavior::CancelledAfterVisibility, false),
1190 ] {
1191 let (request_id, attempt_id) = ids(number);
1192 let attempt = provider
1193 .prepare(
1194 &request("session-failure", &format!("turn-{number}"), None),
1195 request_id,
1196 attempt_id,
1197 behavior,
1198 )
1199 .expect("failure fixture should prepare");
1200 assert_eq!(
1201 event_stream_is_retryable(&attempt.events),
1202 expected_retryable
1203 );
1204 }
1205 }
1206
1207 #[test]
1208 fn malformed_timeout_and_cancellation_fixtures_are_terminal_and_explicit() {
1209 let provider = FixtureProvider::default();
1210 for (number, behavior, expected_kind, expected_retryable) in [
1211 (
1212 60,
1213 FixtureBehavior::MalformedStreamBeforeVisibility,
1214 "malformed_stream",
1215 false,
1216 ),
1217 (
1218 61,
1219 FixtureBehavior::TimeoutBeforeVisibility,
1220 "timeout",
1221 true,
1222 ),
1223 (
1224 62,
1225 FixtureBehavior::CancelledBeforeVisibility,
1226 "cancelled",
1227 false,
1228 ),
1229 ] {
1230 let (request_id, attempt_id) = ids(number);
1231 let attempt = provider
1232 .prepare(
1233 &request("session-fixture", &format!("turn-{number}"), None),
1234 request_id,
1235 attempt_id,
1236 behavior,
1237 )
1238 .expect("terminal fixture");
1239 let error = match attempt.events.as_slice() {
1240 [CanonicalEvent::ResponseFailed { error, .. }] => error,
1241 _ => panic!("pre-visible fixture must emit one terminal failure"),
1242 };
1243 assert_eq!(
1244 error
1245 .details
1246 .as_ref()
1247 .and_then(|details| { details["fixture_failure"]["kind"].as_str() }),
1248 Some(expected_kind)
1249 );
1250 assert_eq!(error.retryable, expected_retryable);
1251 assert_eq!(
1252 event_stream_is_retryable(&attempt.events),
1253 expected_retryable
1254 );
1255 }
1256
1257 for (number, behavior, expected_cause) in [
1258 (
1259 63,
1260 FixtureBehavior::MalformedStreamAfterVisibility,
1261 "malformed_stream",
1262 ),
1263 (64, FixtureBehavior::TimeoutAfterVisibility, "timeout"),
1264 (65, FixtureBehavior::CancelledAfterVisibility, "cancelled"),
1265 ] {
1266 let (request_id, attempt_id) = ids(number);
1267 let attempt = provider
1268 .prepare(
1269 &request("session-fixture", &format!("turn-{number}"), None),
1270 request_id,
1271 attempt_id,
1272 behavior,
1273 )
1274 .expect("post-visible terminal fixture");
1275 let error = match attempt.events.last() {
1276 Some(CanonicalEvent::ResponseFailed { error, .. }) => error,
1277 _ => panic!("post-visible fixture must end in failure"),
1278 };
1279 assert_eq!(
1280 error
1281 .details
1282 .as_ref()
1283 .and_then(|details| { details["interruption"]["cause"].as_str() }),
1284 Some(expected_cause)
1285 );
1286 assert!(!error.retryable);
1287 assert!(!event_stream_is_retryable(&attempt.events));
1288 attempt.commit();
1289 assert_eq!(provider.pending_calls(), 0);
1290 }
1291 }
1292
1293 #[test]
1294 fn post_boundary_failures_return_fixed_safe_resume_without_replay() {
1295 let provider = FixtureProvider::default();
1296 for (number, behavior, boundary) in [
1297 (70, FixtureBehavior::PostVisibleFailure, "public_output"),
1298 (71, FixtureBehavior::PostToolFailure, "tool_call"),
1299 ] {
1300 let (request_id, attempt_id) = ids(number);
1301 let attempt = provider
1302 .prepare(
1303 &request("session-resume", &format!("turn-{number}"), None),
1304 request_id,
1305 attempt_id,
1306 behavior,
1307 )
1308 .expect("safe-resume fixture");
1309 assert_safe_resume(attempt.events.last(), boundary);
1310 assert!(!event_stream_is_retryable(&attempt.events));
1311 }
1312
1313 let (request_id, attempt_id) = ids(72);
1314 let attempt = provider
1315 .prepare(
1316 &request(
1317 "session-resume",
1318 "turn-forwarded-result",
1319 Some(CallId::parse("call-prior").expect("fixture id")),
1320 ),
1321 request_id,
1322 attempt_id,
1323 FixtureBehavior::PreVisibleTransientFailure,
1324 )
1325 .expect("forwarded-result failure fixture");
1326 assert_safe_resume(attempt.events.last(), "forwarded_tool_result");
1327 assert!(!event_stream_is_retryable(&attempt.events));
1328
1329 let (request_id, attempt_id) = ids(73);
1330 let cancelled = provider
1331 .prepare(
1332 &request(
1333 "session-resume",
1334 "turn-forwarded-result-cancelled",
1335 Some(CallId::parse("call-prior-cancelled").expect("fixture id")),
1336 ),
1337 request_id,
1338 attempt_id,
1339 FixtureBehavior::CancelledBeforeVisibility,
1340 )
1341 .expect("forwarded-result cancellation fixture");
1342 assert_safe_resume(cancelled.events.last(), "forwarded_tool_result");
1343 assert!(!event_stream_is_retryable(&cancelled.events));
1344 }
1345
1346 fn assert_safe_resume(event: Option<&CanonicalEvent>, expected_boundary: &str) {
1347 let error = match event {
1348 Some(CanonicalEvent::ResponseFailed { error, .. }) => error,
1349 _ => panic!("expected terminal response failure"),
1350 };
1351 let interruption =
1352 &error.details.as_ref().expect("structured interruption")["interruption"];
1353 assert_eq!(interruption["boundary"], expected_boundary);
1354 assert_eq!(interruption["automatic_replay"], false);
1355 assert_eq!(interruption["safe_resume"]["action"], "start_new_turn");
1356 assert_eq!(
1357 interruption["safe_resume"]["repeat_completed_tool_calls"],
1358 false
1359 );
1360 assert!(!error.retryable);
1361 }
1362
1363 #[test]
1364 fn forwarded_tool_result_is_never_marked_retryable() {
1365 let provider = FixtureProvider::default();
1366 let (request_id, attempt_id) = ids(13);
1367 let attempt = provider
1368 .prepare(
1369 &request(
1370 "session-result",
1371 "turn-result",
1372 Some(CallId::parse("call-prior").expect("fixture id")),
1373 ),
1374 request_id,
1375 attempt_id,
1376 FixtureBehavior::PreVisibleTransientFailure,
1377 )
1378 .expect("pre-visible failure fixture");
1379 assert!(!event_stream_is_retryable(&attempt.events));
1380 assert!(matches!(
1381 attempt.events.last(),
1382 Some(CanonicalEvent::ResponseFailed { error, .. }) if !error.retryable
1383 ));
1384 }
1385
1386 #[test]
1387 fn uncommitted_attempt_releases_pending_correlation() {
1388 let provider = FixtureProvider::default();
1389 let (request_id, attempt_id) = ids(20);
1390 let attempt = provider
1391 .prepare(
1392 &request("session-cancel", "turn-cancel", None),
1393 request_id,
1394 attempt_id,
1395 FixtureBehavior::Normal,
1396 )
1397 .expect("request should prepare");
1398 assert_eq!(provider.pending_calls(), 1);
1399 drop(attempt);
1400 assert_eq!(provider.pending_calls(), 0);
1401 }
1402
1403 #[test]
1404 fn pending_correlation_is_bounded() {
1405 let provider = FixtureProvider::default();
1406 for number in 0..MAX_PENDING_CALLS as u64 {
1407 let (request_id, attempt_id) = ids(1000 + number);
1408 provider
1409 .prepare(
1410 &request("session-cap", &format!("turn-{number}"), None),
1411 request_id,
1412 attempt_id,
1413 FixtureBehavior::Normal,
1414 )
1415 .expect("pending fixture within cap")
1416 .commit();
1417 }
1418 assert_eq!(provider.pending_calls(), MAX_PENDING_CALLS);
1419 let (request_id, attempt_id) = ids(2000);
1420 let error = match provider.prepare(
1421 &request("session-cap", "turn-overflow", None),
1422 request_id,
1423 attempt_id,
1424 FixtureBehavior::Normal,
1425 ) {
1426 Err(error) => error,
1427 Ok(_) => panic!("pending correlation cap must fail closed"),
1428 };
1429 assert_eq!(error.code, PublicErrorCode::RouterUnavailable);
1430 assert_eq!(provider.pending_calls(), MAX_PENDING_CALLS);
1431 }
1432
1433 #[test]
1434 fn correlation_is_bound_to_harness_instance() {
1435 let provider = FixtureProvider::default();
1436 let (request_id, attempt_id) = ids(21);
1437 let first = provider
1438 .prepare(
1439 &request("shared-session", "shared-turn", None),
1440 request_id,
1441 attempt_id,
1442 FixtureBehavior::Normal,
1443 )
1444 .expect("initial call");
1445 let call_id = first
1446 .events
1447 .iter()
1448 .find_map(|event| match event {
1449 CanonicalEvent::FunctionCallArgumentsDone { call_id, .. } => Some(call_id.clone()),
1450 _ => None,
1451 })
1452 .expect("fixture call ID");
1453 first.commit();
1454
1455 let mut other_harness = request("shared-session", "shared-turn", Some(call_id));
1456 other_harness.harness_instance =
1457 HarnessInstanceId::parse("synthetic-desktop").expect("fixture id");
1458 let (request_id, attempt_id) = ids(22);
1459 let error = match provider.prepare(
1460 &other_harness,
1461 request_id,
1462 attempt_id,
1463 FixtureBehavior::Normal,
1464 ) {
1465 Err(error) => error,
1466 Ok(_) => panic!("cross-harness correlation must fail"),
1467 };
1468 assert_eq!(error.code, PublicErrorCode::CorrelationMismatch);
1469 }
1470
1471 #[test]
1472 fn namespaced_tool_identity_is_emitted_and_checked_during_correlation() {
1473 let provider = FixtureProvider::default();
1474 let mut initial = request("session-namespace", "turn-namespace", None);
1475 initial.tools = vec![FunctionTool {
1476 namespace: Some("collaboration".to_owned()),
1477 name: "list_agents".to_owned(),
1478 description: "fixture namespace tool".to_owned(),
1479 parameters: json!({
1480 "type": "object",
1481 "properties": {"path_prefix": {"type": "string"}},
1482 "required": [],
1483 "additionalProperties": false
1484 }),
1485 strict: true,
1486 }];
1487 initial.tool_choice = ToolChoice::Required;
1488 let (request_id, attempt_id) = ids(30);
1489 let first = provider
1490 .prepare(&initial, request_id, attempt_id, FixtureBehavior::Normal)
1491 .expect("namespaced request");
1492 assert_eq!(first.transformed.tool_names, ["collaboration.list_agents"]);
1493 let (call_id, emitted_namespace, emitted_name) = first
1494 .events
1495 .iter()
1496 .find_map(|event| match event {
1497 CanonicalEvent::OutputItemAdded {
1498 item_kind: OutputItemKind::FunctionCall,
1499 call_id: Some(call_id),
1500 namespace,
1501 name: Some(name),
1502 ..
1503 } => Some((call_id.clone(), namespace.clone(), name.clone())),
1504 _ => None,
1505 })
1506 .expect("namespaced function call event");
1507 assert_eq!(emitted_namespace.as_deref(), Some("collaboration"));
1508 assert_eq!(emitted_name, "list_agents");
1509 assert!(first.events.iter().any(|event| matches!(
1510 event,
1511 CanonicalEvent::FunctionCallArgumentsDone { arguments, .. }
1512 if arguments == &json!({})
1513 )));
1514 first.commit();
1515
1516 let mut mismatch = request("session-namespace", "turn-namespace", Some(call_id.clone()));
1517 mismatch.input.insert(
1518 0,
1519 InputItem::FunctionCall {
1520 call_id: call_id.clone(),
1521 namespace: Some("different_namespace".to_owned()),
1522 name: "list_agents".to_owned(),
1523 arguments: json!({}),
1524 },
1525 );
1526 let (request_id, attempt_id) = ids(31);
1527 let error =
1528 match provider.prepare(&mismatch, request_id, attempt_id, FixtureBehavior::Normal) {
1529 Err(error) => error,
1530 Ok(_) => panic!("namespace mismatch must fail"),
1531 };
1532 assert_eq!(error.code, PublicErrorCode::CorrelationMismatch);
1533
1534 let mut followup = request("session-namespace", "turn-namespace", Some(call_id.clone()));
1535 followup.input.insert(
1536 0,
1537 InputItem::FunctionCall {
1538 call_id,
1539 namespace: Some("collaboration".to_owned()),
1540 name: "list_agents".to_owned(),
1541 arguments: json!({}),
1542 },
1543 );
1544 let (request_id, attempt_id) = ids(32);
1545 let second = provider
1546 .prepare(&followup, request_id, attempt_id, FixtureBehavior::Normal)
1547 .expect("matching namespace correlation");
1548 assert!(matches!(
1549 second.events.last(),
1550 Some(CanonicalEvent::ResponseCompleted { .. })
1551 ));
1552 }
1553
1554 #[test]
1555 fn required_safe_zero_required_namespaced_tool_wins_over_effectful_tool() {
1556 let provider = FixtureProvider::default();
1557 let mut initial = request("session-safe-selection", "turn-safe-selection", None);
1558 initial.tools = vec![
1559 FunctionTool {
1560 namespace: Some("collaboration".to_owned()),
1561 name: "send_message".to_owned(),
1562 description: "effectful fixture tool".to_owned(),
1563 parameters: json!({
1564 "type": "object",
1565 "properties": {
1566 "target": {"type": "string"},
1567 "message": {"type": "string"}
1568 },
1569 "required": [],
1570 "additionalProperties": false
1571 }),
1572 strict: true,
1573 },
1574 FunctionTool {
1575 namespace: Some("collaboration".to_owned()),
1576 name: "list_agents".to_owned(),
1577 description: "read-only fixture tool".to_owned(),
1578 parameters: json!({
1579 "type": "object",
1580 "properties": {"path_prefix": {"type": "string"}},
1581 "required": [],
1582 "additionalProperties": false
1583 }),
1584 strict: true,
1585 },
1586 ];
1587 initial.tool_choice = ToolChoice::Required;
1588 let selected_schema = initial.tools[1].parameters.clone();
1589 let (request_id, attempt_id) = ids(40);
1590 let attempt = provider
1591 .prepare(&initial, request_id, attempt_id, FixtureBehavior::Normal)
1592 .expect("safe tool is eligible");
1593 let (namespace, name) = attempt
1594 .events
1595 .iter()
1596 .find_map(|event| match event {
1597 CanonicalEvent::OutputItemAdded {
1598 item_kind: OutputItemKind::FunctionCall,
1599 namespace,
1600 name: Some(name),
1601 ..
1602 } => Some((namespace.clone(), name.clone())),
1603 _ => None,
1604 })
1605 .expect("selected function item");
1606 let arguments = attempt
1607 .events
1608 .iter()
1609 .find_map(|event| match event {
1610 CanonicalEvent::FunctionCallArgumentsDone { arguments, .. } => {
1611 Some(arguments.clone())
1612 }
1613 _ => None,
1614 })
1615 .expect("function arguments");
1616 assert_eq!(namespace.as_deref(), Some("collaboration"));
1617 assert_eq!(name, "list_agents");
1618 assert_eq!(arguments, json!({}));
1619 assert!(schema_accepts_empty_object(&selected_schema));
1620 assert_eq!(
1621 attempt.transformed.tool_selection,
1622 Some(ToolSelectionDiagnostics {
1623 stage: ToolSelectionStage::SafeZeroRequired,
1624 total_tools: 2,
1625 zero_required_candidates: 2,
1626 reviewed_safe_matches: 1,
1627 reviewed_identity_matches: 1,
1628 collaboration_wait_agent_matches: 0,
1629 matched_category: Some(ReviewedSafeCategory::CollaborationListAgents),
1630 })
1631 );
1632 }
1633
1634 #[test]
1635 fn named_choice_cannot_silently_select_a_different_tool() {
1636 let provider = FixtureProvider::default();
1637 let mut initial = request("session-named", "turn-named", None);
1638 initial.tools = vec![
1639 FunctionTool {
1640 namespace: None,
1641 name: "list_mcp_resources".to_owned(),
1642 description: "first safe fixture".to_owned(),
1643 parameters: json!({"type": "object", "required": []}),
1644 strict: true,
1645 },
1646 FunctionTool {
1647 namespace: None,
1648 name: "get_goal".to_owned(),
1649 description: "named safe fixture".to_owned(),
1650 parameters: json!({"type": "object", "required": []}),
1651 strict: true,
1652 },
1653 ];
1654 initial.tool_choice = ToolChoice::Named("get_goal".to_owned());
1655 let (request_id, attempt_id) = ids(41);
1656 let attempt = provider
1657 .prepare(&initial, request_id, attempt_id, FixtureBehavior::Normal)
1658 .expect("named safe tool is eligible");
1659 assert!(attempt.events.iter().any(|event| matches!(
1660 event,
1661 CanonicalEvent::OutputItemAdded {
1662 item_kind: OutputItemKind::FunctionCall,
1663 name: Some(name),
1664 ..
1665 } if name == "get_goal"
1666 )));
1667 }
1668
1669 #[test]
1670 fn auto_degrades_without_calling_an_effectful_zero_required_tool() {
1671 let provider = FixtureProvider::default();
1672 let mut initial = request("session-effectful", "turn-effectful", None);
1673 initial.tools = vec![FunctionTool {
1674 namespace: Some("collaboration".to_owned()),
1675 name: "send_message".to_owned(),
1676 description: "effectful fixture tool".to_owned(),
1677 parameters: json!({
1678 "type": "object",
1679 "properties": {
1680 "target": {"type": "string"},
1681 "message": {"type": "string"}
1682 },
1683 "required": [],
1684 "additionalProperties": false
1685 }),
1686 strict: true,
1687 }];
1688 let (request_id, attempt_id) = ids(41);
1689 let attempt = provider
1690 .prepare(&initial, request_id, attempt_id, FixtureBehavior::Normal)
1691 .expect("auto may use the reviewed no-tool degradation");
1692 assert!(!attempt.events.iter().any(|event| matches!(
1693 event,
1694 CanonicalEvent::OutputItemAdded {
1695 item_kind: OutputItemKind::FunctionCall,
1696 ..
1697 }
1698 )));
1699 assert!(attempt.events.iter().any(|event| matches!(
1700 event,
1701 CanonicalEvent::OutputTextDone { text, .. }
1702 if text == "Bounded fixture completed without a tool."
1703 )));
1704 assert!(matches!(
1705 attempt.events.last(),
1706 Some(CanonicalEvent::ResponseCompleted { .. })
1707 ));
1708 assert_eq!(
1709 attempt.transformed.tool_selection,
1710 Some(ToolSelectionDiagnostics {
1711 stage: ToolSelectionStage::NoReviewedSafeTool,
1712 total_tools: 1,
1713 zero_required_candidates: 1,
1714 reviewed_safe_matches: 0,
1715 reviewed_identity_matches: 0,
1716 collaboration_wait_agent_matches: 0,
1717 matched_category: None,
1718 })
1719 );
1720 assert_eq!(provider.pending_calls(), 0);
1721 }
1722
1723 #[test]
1724 fn auto_selects_a_declared_reviewed_tool_and_never_invents_one() {
1725 let provider = FixtureProvider::default();
1726 let initial = request("session-auto", "turn-auto", None);
1727 let (request_id, attempt_id) = ids(73);
1728 let attempt = provider
1729 .prepare(&initial, request_id, attempt_id, FixtureBehavior::Normal)
1730 .expect("declared reviewed tool is eligible for auto");
1731 assert!(attempt.events.iter().any(|event| matches!(
1732 event,
1733 CanonicalEvent::OutputItemAdded {
1734 item_kind: OutputItemKind::FunctionCall,
1735 namespace: None,
1736 name: Some(name),
1737 ..
1738 } if name == "get_weather"
1739 )));
1740 assert_eq!(
1741 attempt
1742 .transformed
1743 .tool_selection
1744 .map(|diagnostics| diagnostics.stage),
1745 Some(ToolSelectionStage::ReviewedRequiredPrimitive)
1746 );
1747
1748 let mut empty = request("session-auto", "turn-empty", None);
1749 empty.tools.clear();
1750 let (request_id, attempt_id) = ids(74);
1751 let degraded = provider
1752 .prepare(&empty, request_id, attempt_id, FixtureBehavior::Normal)
1753 .expect("empty auto declaration uses explicit no-tool degradation");
1754 assert!(!degraded.events.iter().any(|event| matches!(
1755 event,
1756 CanonicalEvent::OutputItemAdded {
1757 item_kind: OutputItemKind::FunctionCall,
1758 ..
1759 }
1760 )));
1761 assert_eq!(
1762 degraded
1763 .transformed
1764 .tool_selection
1765 .map(|diagnostics| diagnostics.stage),
1766 Some(ToolSelectionStage::NoReviewedSafeTool)
1767 );
1768 }
1769
1770 #[test]
1771 fn none_choice_uses_no_tool_completion() {
1772 let provider = FixtureProvider::default();
1773 let mut initial = request("session-none", "turn-none", None);
1774 initial.tool_choice = ToolChoice::None;
1775 initial.tools = vec![FunctionTool {
1776 namespace: Some("collaboration".to_owned()),
1777 name: "send_message".to_owned(),
1778 description: "effectful fixture tool".to_owned(),
1779 parameters: json!({"type": "object", "properties": {}, "required": []}),
1780 strict: true,
1781 }];
1782 let (request_id, attempt_id) = ids(44);
1783 let attempt = provider
1784 .prepare(&initial, request_id, attempt_id, FixtureBehavior::Normal)
1785 .expect("none choice completes without a tool");
1786 assert!(!attempt.events.iter().any(|event| matches!(
1787 event,
1788 CanonicalEvent::OutputItemAdded {
1789 item_kind: OutputItemKind::FunctionCall,
1790 ..
1791 }
1792 )));
1793 assert!(matches!(
1794 attempt.events.last(),
1795 Some(CanonicalEvent::ResponseCompleted { .. })
1796 ));
1797 }
1798
1799 #[test]
1800 fn required_choice_fails_when_no_reviewed_safe_tool_is_eligible() {
1801 let provider = FixtureProvider::default();
1802 let mut initial = request("session-required", "turn-required", None);
1803 initial.tool_choice = ToolChoice::Required;
1804 initial.tools = vec![FunctionTool {
1805 namespace: Some("collaboration".to_owned()),
1806 name: "send_message".to_owned(),
1807 description: "effectful fixture tool".to_owned(),
1808 parameters: json!({"type": "object", "properties": {}, "required": []}),
1809 strict: true,
1810 }];
1811 let (request_id, attempt_id) = ids(45);
1812 let error =
1813 match provider.prepare(&initial, request_id, attempt_id, FixtureBehavior::Normal) {
1814 Err(error) => error,
1815 Ok(_) => panic!("required choice must fail closed"),
1816 };
1817 assert_eq!(error.code, PublicErrorCode::UnsupportedCapability);
1818 assert_eq!(
1819 error.details,
1820 Some(json!({
1821 "failure_stage": "fixture_tool_selector.ineligible",
1822 "total_tools": 1,
1823 "zero_required_candidates": 1,
1824 "reviewed_safe_matches": 0,
1825 "reviewed_identity_matches": 0,
1826 "collaboration_wait_agent_matches": 0
1827 }))
1828 );
1829 assert_eq!(
1830 select_fixture_tool(&initial.tools),
1831 Err(ToolSelectionDiagnostics {
1832 stage: ToolSelectionStage::Ineligible,
1833 total_tools: 1,
1834 zero_required_candidates: 1,
1835 reviewed_safe_matches: 0,
1836 reviewed_identity_matches: 0,
1837 collaboration_wait_agent_matches: 0,
1838 matched_category: None,
1839 })
1840 );
1841 }
1842
1843 #[test]
1844 fn reviewed_required_primitive_default_matches_preserved_schema() {
1845 let provider = FixtureProvider::default();
1846 let mut initial = request("session-reviewed", "turn-reviewed", None);
1847 initial.tools = vec![FunctionTool {
1848 namespace: None,
1849 name: "get_weather".to_owned(),
1850 description: "reviewed fixture tool".to_owned(),
1851 parameters: json!({
1852 "type": "object",
1853 "properties": {"city": {"type": "string"}},
1854 "required": ["city"],
1855 "additionalProperties": false
1856 }),
1857 strict: true,
1858 }];
1859 initial.tool_choice = ToolChoice::Required;
1860 let schema = initial.tools[0].parameters.clone();
1861 let (request_id, attempt_id) = ids(42);
1862 let attempt = provider
1863 .prepare(&initial, request_id, attempt_id, FixtureBehavior::Normal)
1864 .expect("reviewed primitive default is eligible");
1865 let arguments = attempt
1866 .events
1867 .iter()
1868 .find_map(|event| match event {
1869 CanonicalEvent::FunctionCallArgumentsDone { arguments, .. } => {
1870 Some(arguments.clone())
1871 }
1872 _ => None,
1873 })
1874 .expect("function arguments");
1875 assert_eq!(arguments, json!({"city": "San Francisco"}));
1876 assert!(arguments_validate_against_reviewed_schema(
1877 &arguments, &schema, "city"
1878 ));
1879
1880 initial.tools[0].parameters["properties"]["city"]["type"] =
1881 Value::String("integer".to_owned());
1882 initial.tool_choice = ToolChoice::Auto;
1883 let (request_id, attempt_id) = ids(43);
1884 let attempt = provider
1885 .prepare(&initial, request_id, attempt_id, FixtureBehavior::Normal)
1886 .expect("auto degrades when the primitive schema is unreviewed");
1887 assert!(!attempt.events.iter().any(|event| matches!(
1888 event,
1889 CanonicalEvent::OutputItemAdded {
1890 item_kind: OutputItemKind::FunctionCall,
1891 ..
1892 }
1893 )));
1894 assert_eq!(
1895 attempt
1896 .transformed
1897 .tool_selection
1898 .map(|diagnostics| diagnostics.stage),
1899 Some(ToolSelectionStage::NoReviewedSafeTool)
1900 );
1901 }
1902}