Skip to main content

provider_core/
lib.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3#![forbid(unsafe_code)]
4
5//! Deterministic credential-free transformed provider used only by M1 tests.
6
7pub 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}