diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 64b391dd..15b6be40 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -198,7 +198,7 @@ access happen — those live in `tool/`, `executor/`, and `storage/` respectivel (the normalized `FunctionTool` and `ToolChoice`, distinct from tool *declarations*), `usage.rs` (token accounting structs). - **`types/tools/params.rs`** — the tool **declaration** shapes a client sends: - `ResponsesTool` (tagged enum: `Function`, `Mcp`, `WebSearch`, `FileSearch`, + `ResponsesTool` (tagged enum: `Function`, `ToolSearch`, `Mcp`, `WebSearch`, `FileSearch`, `CodeInterpreter`, `Namespace`, `Custom`, `Unknown`) and each variant's param struct. This is a good concrete example of the module boundary: `ResponsesTool` is *defined* here as a pure shape, but its behavior — `validate()` and `to_function_tools()` — is @@ -329,9 +329,9 @@ via `process_event`/`synthetic_event`/`emit_sse_frame`. #### `function_sse.rs` — `FunctionSseTranslator` -vLLM only ever emits `function_call` SSE events, regardless of which tool type the -call is routed to. This translator looks up each call's name in the tool registry and -reshapes the raw stream accordingly: +Upstreams without native support for a declared tool type emit `function_call` SSE +events instead. This translator borrows the request-scoped tool registry for +classification and reshapes those raw calls accordingly: - **Custom tools** — rewritten into the public `custom_tool_call` event shape (`output_item.added` / `custom_tool_call_input.delta` / `.done` / `output_item.done`), reconstructing the `input` JSON incrementally from the streamed `arguments`. @@ -339,6 +339,9 @@ reshapes the raw stream accordingly: frames are suppressed entirely. Their real client-visible events are synthesized later, once the call has actually executed, by `gateway.rs`. - **Client-owned tools** (`Function`, `CodexNamespace`) — pass through unchanged. +- **Tool search** — native `tool_search_call` events pass through as typed items; + synthetic `function_call` events named `tool_search` are projected into that same + public lifecycle after validation. It also buffers function-call events that arrive before the call's name is known (bounded at 256 KiB) and replays them once the name resolves. diff --git a/crates/agentic-server-core/src/events/types.rs b/crates/agentic-server-core/src/events/types.rs index ae012736..25b6ea5e 100644 --- a/crates/agentic-server-core/src/events/types.rs +++ b/crates/agentic-server-core/src/events/types.rs @@ -8,6 +8,7 @@ use crate::types::io::ResponseUsage; pub enum SSEItemType { Reasoning, FunctionCall, + ToolSearchCall, CustomToolCall, WebSearchCall, McpCall, @@ -22,6 +23,7 @@ impl SSEItemType { match self { Self::Reasoning => "reasoning", Self::FunctionCall => "function_call", + Self::ToolSearchCall => "tool_search_call", Self::CustomToolCall => "custom_tool_call", Self::WebSearchCall => "web_search_call", Self::McpCall => "mcp_call", @@ -37,6 +39,7 @@ impl From<&str> for SSEItemType { match s { "reasoning" => Self::Reasoning, "function_call" => Self::FunctionCall, + "tool_search_call" => Self::ToolSearchCall, "custom_tool_call" => Self::CustomToolCall, "web_search_call" => Self::WebSearchCall, "mcp_call" => Self::McpCall, diff --git a/crates/agentic-server-core/src/executor/accumulator.rs b/crates/agentic-server-core/src/executor/accumulator.rs index 2c68b542..451b5252 100644 --- a/crates/agentic-server-core/src/executor/accumulator.rs +++ b/crates/agentic-server-core/src/executor/accumulator.rs @@ -34,6 +34,7 @@ enum InFlight { Message { item: OutputMessage, text: String }, Reasoning { item: ReasoningOutput, text: String }, FunctionCall { item: FunctionToolCall, arguments: String }, + ToolSearchCall { item: crate::types::io::ToolSearchCall }, CustomToolCall { item: CustomToolCall, input: String }, WebSearchCall { item: Option }, McpCall { item: McpCall }, @@ -47,6 +48,7 @@ impl std::fmt::Debug for InFlight { Self::Message { .. } => write!(f, "InFlight::Message {{ .. }}"), Self::Reasoning { .. } => write!(f, "InFlight::Reasoning {{ .. }}"), Self::FunctionCall { .. } => write!(f, "InFlight::FunctionCall {{ .. }}"), + Self::ToolSearchCall { .. } => write!(f, "InFlight::ToolSearchCall {{ .. }}"), Self::CustomToolCall { .. } => write!(f, "InFlight::CustomToolCall {{ .. }}"), Self::WebSearchCall { .. } => write!(f, "InFlight::WebSearchCall {{ .. }}"), Self::McpCall { .. } => write!(f, "InFlight::McpCall {{ .. }}"), @@ -72,6 +74,7 @@ impl InFlight { item.status = MessageStatus::Completed; Some(OutputItem::FunctionCall(item)) } + Self::ToolSearchCall { item } => Some(OutputItem::ToolSearchCall(item)), Self::Message { mut item, text } => { if !text.is_empty() { item.content.push(OutputTextContent::new(text)); @@ -469,6 +472,9 @@ impl ResponseAccumulator { item, arguments: String::with_capacity(128), }), + SSEItemType::ToolSearchCall => crate::types::io::ToolSearchCall::try_from(payload) + .ok() + .map(|item| InFlight::ToolSearchCall { item }), SSEItemType::CustomToolCall => { CustomToolCall::try_from(payload) .ok() @@ -535,6 +541,7 @@ impl ResponseAccumulator { if let Some(entry) = in_flight_key.as_deref().and_then(|key| self.in_flight.get_mut(key)) { match (&mut entry.item, done_item) { (InFlight::FunctionCall { item, arguments }, _) => item.apply_done(payload, arguments), + (InFlight::ToolSearchCall { item }, _) => item.apply_done(payload, &mut String::new()), (InFlight::CustomToolCall { item, input }, _) => item.apply_done(payload, input), (InFlight::McpCall { item }, _) => item.apply_done(payload, &mut String::new()), (InFlight::McpListTools { item }, _) => item.apply_done(payload, &mut String::new()), @@ -555,6 +562,7 @@ impl ResponseAccumulator { if let Some( mut output_item @ (OutputItem::FunctionCall(_) + | OutputItem::ToolSearchCall(_) | OutputItem::CustomToolCall(_) | OutputItem::WebSearchCall(_) | OutputItem::McpCall(_) @@ -618,6 +626,8 @@ impl ResponseAccumulator { previous_response_id: previous_response_id.map(str::to_string), conversation_id: self.conversation_id, instructions: instructions.map(str::to_string), + tools: None, + tool_choice: None, } } } @@ -626,6 +636,7 @@ fn in_flight_matches_call_type(item: &InFlight, item_type: SSEItemType) -> bool matches!( (item, item_type), (InFlight::FunctionCall { .. }, SSEItemType::FunctionCall) + | (InFlight::ToolSearchCall { .. }, SSEItemType::ToolSearchCall) | (InFlight::CustomToolCall { .. }, SSEItemType::CustomToolCall) | (InFlight::WebSearchCall { .. }, SSEItemType::WebSearchCall) | (InFlight::McpCall { .. }, SSEItemType::McpCall) @@ -1843,4 +1854,44 @@ mod tests { assert_eq!(call.name, "raw_echo"); assert_eq!(call.input, "hello"); } + + #[test] + fn native_tool_search_call_accumulates_from_added_and_done() { + let acc = ResponseAccumulator::from_sse_lines( + [ + r#"data: {"type":"response.output_item.added","output_index":0,"item":{"type":"tool_search_call","id":"tsc_native","call_id":"call_search","execution":"client","arguments":{},"status":"in_progress"}}"#.to_owned(), + r#"data: {"type":"response.output_item.done","output_index":0,"item":{"type":"tool_search_call","id":"tsc_native","call_id":"call_search","execution":"client","arguments":{"query":"weather"},"status":"completed"}}"#.to_owned(), + r#"data: {"type":"response.completed","response":{"id":"resp_1","status":"completed"}}"#.to_owned(), + ], + None, + ); + + let [OutputItem::ToolSearchCall(call)] = acc.output.as_slice() else { + panic!("expected native tool_search_call"); + }; + assert_eq!(call.id, "tsc_native"); + assert_eq!(call.call_id, "call_search"); + assert_eq!(call.arguments["query"], "weather"); + assert_eq!(call.status, crate::types::tools::ToolSearchStatus::Completed); + } + + #[test] + fn blocking_native_tool_search_call_remains_typed() { + let body = serde_json::json!({ + "id": "resp_1", + "status": "completed", + "output": [{ + "type": "tool_search_call", + "id": "tsc_native", + "call_id": "call_search", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + }] + }) + .to_string(); + + let acc = ResponseAccumulator::from_json(&body, None).expect("valid blocking response"); + assert!(matches!(acc.output.as_slice(), [OutputItem::ToolSearchCall(_)])); + } } diff --git a/crates/agentic-server-core/src/executor/compaction.rs b/crates/agentic-server-core/src/executor/compaction.rs index 6c84e282..daefbcfb 100644 --- a/crates/agentic-server-core/src/executor/compaction.rs +++ b/crates/agentic-server-core/src/executor/compaction.rs @@ -1,4 +1,6 @@ use crate::executor::error::{ExecutorError, ExecutorResult}; +use crate::executor::persist::persist_prepared_turn; +use crate::executor::prepare::prepare_request_tools; use crate::executor::rehydrate::rehydrate_conversation; use crate::executor::request::{ExecutionContext, RequestContext}; use crate::executor::upstream::fetch_blocking_payload; @@ -87,6 +89,8 @@ fn item_has_meaningful_context(item: &InputItem) -> bool { }, InputItem::FunctionCall(call) => !call.name.trim().is_empty() || !call.arguments.trim().is_empty(), InputItem::FunctionCallOutput(output) => output.output.has_content(), + InputItem::ToolSearchCall(call) => !call.call_id.trim().is_empty() || !call.arguments.is_empty(), + InputItem::ToolSearchOutput(output) => !output.call_id.trim().is_empty() || !output.tools.is_empty(), InputItem::CustomToolCall(call) => !call.name.trim().is_empty() || !call.input.trim().is_empty(), InputItem::CustomToolCallOutput(output) => output.output.has_content(), InputItem::Reasoning(reasoning) => { @@ -203,7 +207,7 @@ pub(crate) async fn compact_items( conversation_id: None, conversation_version: None, }; - let response = fetch_blocking_payload(&ctx, exec_ctx, auth).await?; + let response = fetch_blocking_payload(&ctx, exec_ctx, auth, &crate::tool::ToolRegistry::default()).await?; let summary = completed_summary_text(&response)?; Ok(( @@ -278,7 +282,8 @@ pub async fn compact_response( request.instructions, ); payload.previous_response_id = request.previous_response_id; - let mut ctx = rehydrate_conversation(payload, exec_ctx).await?; + let ctx = rehydrate_conversation(payload, exec_ctx).await?; + let (mut ctx, registry) = prepare_request_tools(ctx, &exec_ctx.conv_handler, &exec_ctx.resp_handler).await?; let model = ctx.enriched_request.model.clone(); let instructions = ctx.enriched_request.instructions.clone(); let input = std::mem::replace(&mut ctx.enriched_request.input, ResponsesInput::Items(Vec::new())); @@ -286,7 +291,15 @@ pub async fn compact_response( let response_id = ctx.response_id.clone(); ctx.new_input_items.clone_from(&output); - match exec_ctx.resp_handler.execute_turn(ctx, Vec::new()).await { + match persist_prepared_turn( + ctx, + registry, + Vec::new(), + &exec_ctx.conv_handler, + &exec_ctx.resp_handler, + ) + .await + { Ok(()) | Err(ExecutorError::Storage(crate::StorageError::NotConfigured)) => {} Err(error) => return Err(error), } @@ -548,6 +561,38 @@ mod tests { server.abort(); } + #[tokio::test] + async fn compaction_prepares_tool_search_before_summarization() { + let (exec_ctx, server) = mock_execution_context(ResponseStore::disabled()).await; + let request = serde_json::from_value(serde_json::json!({ + "model": "test-model", + "input": [{ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "arguments": {"query": "weather"} + }, { + "type": "tool_search_output", + "call_id": "call_search_1", + "tools": [] + }] + })) + .expect("valid compact request"); + + let compacted = compact_response(request, &exec_ctx, None) + .await + .expect("compaction prepares and summarizes public search history"); + + assert!(matches!(compacted.output.last(), Some(InputItem::Compaction(_)))); + assert!( + compacted + .output + .iter() + .all(|item| !matches!(item, InputItem::ToolSearchCall(_) | InputItem::ToolSearchOutput(_))) + ); + server.abort(); + } + #[tokio::test] async fn compaction_persists_a_reusable_response_checkpoint() { let pool = create_pool_with_schema(Some("sqlite::memory:")) @@ -585,6 +630,7 @@ mod tests { model: "test-model".to_owned(), previous_response_id: None, effective_tools: None, + tool_search_loaded_tools: None, effective_tool_choice: crate::ToolChoice::Auto, effective_instructions: None, }, diff --git a/crates/agentic-server-core/src/executor/engine.rs b/crates/agentic-server-core/src/executor/engine.rs index a17b676e..127cbe38 100644 --- a/crates/agentic-server-core/src/executor/engine.rs +++ b/crates/agentic-server-core/src/executor/engine.rs @@ -22,13 +22,14 @@ use super::gateway::{ }; use super::gateway_accumulator::{GatewayStreamAccumulator, StreamEvent, error_sse_chunk}; use crate::events::EventFrame; -use crate::executor::error::ExecutorResult; +use crate::executor::error::{ExecutorError, ExecutorResult}; use crate::executor::inference::DONE_MARKER; use crate::executor::persist::persist_if_needed; +use crate::executor::prepare::prepare_request_tools; use crate::executor::rehydrate::rehydrate_conversation; use crate::executor::request::{ExecutionContext, RequestContext}; use crate::executor::upstream::{emit_deferred_stream_events, fetch_blocking_payload, fetch_stream_payload}; -use crate::tool::{ToolRegistry, mcp}; +use crate::tool::{ToolRegistry, mcp::handler::list_tools_output_item}; use crate::types::io::{InputItem, OutputItem, ResponseUsage, ResponsesInput, ToolChoice}; use crate::types::request_response::{IncompleteDetails, RequestPayload, ResponsePayload}; use crate::utils::common::utcnow_str; @@ -97,12 +98,13 @@ impl Drop for AbortOnDrop { async fn run_until_gateway_tools_complete( ctx: RequestContext, + registry: ToolRegistry, exec_ctx: &ExecutionContext, auth: Option<&str>, stream_upstream: bool, mut stream: Option<(&mut GatewayStreamAccumulator, &mpsc::UnboundedSender)>, -) -> ExecutorResult<(ResponsePayload, RequestContext)> { - if ctx.enriched_request.input.has_compaction_trigger() { +) -> ExecutorResult<(ResponsePayload, RequestContext, ToolRegistry)> { + if ctx.original_request.input.has_compaction_trigger() { let (payload, ctx) = run_compaction_trigger(ctx, exec_ctx, auth).await?; if let Some((stream_accumulator, stream_sender)) = stream.as_mut() { emit_response_start_events(&payload, stream_accumulator, stream_sender)?; @@ -110,28 +112,28 @@ async fn run_until_gateway_tools_complete( emit_gateway_start_events(&event_plans, stream_accumulator, stream_sender)?; emit_gateway_completed_events(&payload.output, &event_plans, stream_accumulator, stream_sender)?; } - return Ok((payload, ctx)); + return Ok((payload, ctx, registry)); } - run_gateway_tool_loop(ctx, exec_ctx, auth, stream_upstream, stream).await + run_gateway_tool_loop(ctx, registry, exec_ctx, auth, stream_upstream, stream).await } async fn run_gateway_tool_loop( mut ctx: RequestContext, + registry: ToolRegistry, exec_ctx: &ExecutionContext, auth: Option<&str>, stream_upstream: bool, mut stream: Option<(&mut GatewayStreamAccumulator, &mpsc::UnboundedSender)>, -) -> ExecutorResult<(ResponsePayload, RequestContext)> { +) -> ExecutorResult<(ResponsePayload, RequestContext, ToolRegistry)> { let mut executors = exec_ctx.gateway_executors.request_scoped(); - let registry: ToolRegistry = match ctx.enriched_request.tools.as_mut() { - Some(tools) => ToolRegistry::build_with_handlers(tools, &mut executors).await?, - None => ToolRegistry::default(), - }; - let mut combined_output: Vec = registry + let mut registry = registry + .build_prepared_with_handlers(ctx.enriched_request.tools.as_mut(), &mut executors) + .await?; + let mut combined_output: Vec<_> = registry .mcp_list_tools_items() .iter() - .map(mcp::handler::list_tools_output_item) + .map(list_tools_output_item) .collect(); let mut combined_usage = None; @@ -144,7 +146,7 @@ async fn run_gateway_tool_loop( &ctx, exec_ctx, auth, - ®istry, + &mut registry, stream .as_mut() .map(|(accumulator, sender)| (&mut **accumulator, *sender)), @@ -153,22 +155,20 @@ async fn run_gateway_tool_loop( .await?; (stream_payload.payload, stream_payload.deferred_events) } else { - (fetch_blocking_payload(&ctx, exec_ctx, auth).await?, Vec::new()) + ( + fetch_blocking_payload(&ctx, exec_ctx, auth, ®istry).await?, + Vec::new(), + ) }; registry.restore_final_payload_output(&mut payload.output); accumulate_usage(&mut combined_usage, payload.usage.take()); let current_output = std::mem::take(&mut payload.output); - for item in ¤t_output { - if let OutputItem::CustomToolCall(call) = item { - debug!( - response_id = %ctx.response_id, - call_id = %call.call_id, - name = %call.name, - input_bytes = call.input.len(), - "custom tool call requires client execution" - ); - } + if matches!(payload.status.as_str(), "error" | "failed") { + combined_output.extend(current_output); + finalize_loop(&mut payload, combined_output, combined_usage, &ctx, ®istry); + return Ok((payload, ctx, registry)); } + log_custom_tool_calls(¤t_output, &ctx.response_id); let has_client_owned = has_client_owned_calls(¤t_output, ®istry); let gateway_results = execute_and_emit_round_output_calls( ¤t_output, @@ -181,26 +181,27 @@ async fn run_gateway_tool_loop( .map(|(accumulator, sender)| (&mut **accumulator, *sender)), ) .await?; - let public_output = public_output_items(¤t_output, ®istry, &gateway_results); - combined_output.extend(public_output); + combined_output.extend(public_output_items(¤t_output, ®istry, &gateway_results)); + + if payload.status == "incomplete" { + record_gateway_round_input(&mut ctx, ¤t_output, ®istry, gateway_results); + finalize_loop(&mut payload, combined_output, combined_usage, &ctx, ®istry); + return Ok((payload, ctx, registry)); + } match classify_round(has_client_owned, &gateway_results, round, MAX_GATEWAY_TOOL_ROUNDS) { // Client-owned calls (plain function or Codex namespace tools) are // handed back to the caller. Gateway calls in the same turn are // still recorded so the returned conversation is complete. LoopDecision::RequiresClientAction => { - append_gateway_calls_to_new_input(&mut ctx, ¤t_output, ®istry); - append_tool_outputs( - &mut ctx, - gateway_results.into_iter().map(|result| result.input_item).collect(), - ); - finalize_loop(&mut payload, combined_output, combined_usage, &ctx); - return Ok((payload, ctx)); + record_gateway_round_input(&mut ctx, ¤t_output, ®istry, gateway_results); + finalize_loop(&mut payload, combined_output, combined_usage, &ctx, ®istry); + return Ok((payload, ctx, registry)); } // No gateway work remains — this turn is the final response. LoopDecision::Done => { - finalize_loop(&mut payload, combined_output, combined_usage, &ctx); - return Ok((payload, ctx)); + finalize_loop(&mut payload, combined_output, combined_usage, &ctx, ®istry); + return Ok((payload, ctx, registry)); } // Budget exhausted while the model was still requesting gateway // tools: surface the accumulated work as a partial @@ -208,25 +209,17 @@ async fn run_gateway_tool_loop( // The final round's gateway calls and outputs are recorded so a // continuation is not fed a dangling tool call. LoopDecision::Incomplete(reason) => { - append_gateway_calls_to_new_input(&mut ctx, ¤t_output, ®istry); - append_tool_outputs( - &mut ctx, - gateway_results.into_iter().map(|result| result.input_item).collect(), - ); - finalize_loop(&mut payload, combined_output, combined_usage, &ctx); + record_gateway_round_input(&mut ctx, ¤t_output, ®istry, gateway_results); + finalize_loop(&mut payload, combined_output, combined_usage, &ctx, ®istry); "incomplete".clone_into(&mut payload.status); payload.incomplete_details = Some(IncompleteDetails { reason: Some(reason) }); - return Ok((payload, ctx)); + return Ok((payload, ctx, registry)); } // Gateway tools ran and rounds remain; feed outputs back and loop. LoopDecision::Continue => { ctx.enriched_request.tool_choice = Some(ToolChoice::Auto); append_output_items_to_input(&mut ctx.enriched_request.input, ¤t_output); - append_gateway_calls_to_new_input(&mut ctx, ¤t_output, ®istry); - append_tool_outputs( - &mut ctx, - gateway_results.into_iter().map(|result| result.input_item).collect(), - ); + record_gateway_round_input(&mut ctx, ¤t_output, ®istry, gateway_results); } } } @@ -234,6 +227,30 @@ async fn run_gateway_tool_loop( unreachable!("the final round returns Done, RequiresClientAction, or Incomplete"); } +fn record_gateway_round_input( + ctx: &mut RequestContext, + output: &[OutputItem], + registry: &ToolRegistry, + results: Vec, +) { + append_gateway_calls_to_new_input(ctx, output, registry); + append_tool_outputs(ctx, results.into_iter().map(|result| result.input_item).collect()); +} + +fn log_custom_tool_calls(output: &[OutputItem], response_id: &str) { + for item in output { + if let OutputItem::CustomToolCall(call) = item { + debug!( + response_id, + call_id = %call.call_id, + name = %call.name, + input_bytes = call.input.len(), + "custom tool call requires client execution" + ); + } + } +} + /// Codex CLI remote-compaction V2: the client appends a `compaction_trigger` /// item to the input and expects the server to run its own summarization turn /// and stream back exactly one `compaction` output item plus `response.completed`. @@ -265,6 +282,8 @@ async fn run_compaction_trigger( previous_response_id: ctx.original_request.previous_response_id.clone(), conversation_id: ctx.conversation_id.clone(), instructions, + tools: None, + tool_choice: None, }; ctx.inject_ids(&mut payload); Ok((payload, ctx)) @@ -393,28 +412,40 @@ fn finalize_loop( combined_output: Vec, combined_usage: Option, ctx: &RequestContext, + registry: &ToolRegistry, ) { payload.output = combined_output; payload.usage = combined_usage; ctx.inject_ids(payload); + if let Some(tools) = registry.tool_search_response_tools() { + payload.tools = Some(tools); + payload.tool_choice = Some(ctx.enriched_request.tool_choice.clone().unwrap_or_default()); + } } async fn run_blocking( ctx: RequestContext, + registry: ToolRegistry, exec_ctx: &ExecutionContext, auth: Option<&str>, ) -> ExecutorResult { - let (payload, ctx) = run_until_gateway_tools_complete(ctx, exec_ctx, auth, false, None).await?; + let (payload, ctx, registry) = run_until_gateway_tools_complete(ctx, registry, exec_ctx, auth, false, None).await?; let ch = exec_ctx.conv_handler.clone(); let rh = exec_ctx.resp_handler.clone(); - persist_if_needed(payload.clone(), ctx, ch, rh).await?; + persist_if_needed(payload.clone(), ctx, registry, ch, rh).await?; Ok(payload) } -fn run_stream(ctx: RequestContext, exec_ctx: Arc, auth: Option) -> BoxStream { +fn run_stream( + ctx: RequestContext, + registry: ToolRegistry, + exec_ctx: Arc, + auth: Option, +) -> BoxStream { Box::pin(stream! { + let failure_context = StreamFailureContext::from(&ctx); let (event_tx, mut event_rx) = mpsc::unbounded_channel(); let exec_ctx_for_run = Arc::clone(&exec_ctx); let event_tx_for_run = event_tx.clone(); @@ -423,6 +454,7 @@ fn run_stream(ctx: RequestContext, exec_ctx: Arc, auth: Option let mut stream_accumulator = stream_accumulator; let result = run_until_gateway_tools_complete( ctx, + registry, exec_ctx_for_run.as_ref(), auth.as_deref(), true, @@ -449,10 +481,18 @@ fn run_stream(ctx: RequestContext, exec_ctx: Arc, auth: Option while let Ok(event) = event_rx.try_recv() { yield consume_stream_event(event, &mut next_sequence_number); } - yield stream_accumulator.executor_error_chunk(&e); + if e.is_invalid_upstream_tool_search() { + let payload = failure_context.failed_payload(&e); + match stream_accumulator.terminal_response_chunk(&payload) { + Ok(chunk) => yield chunk, + Err(serialize_error) => yield stream_accumulator.executor_error_chunk(&serialize_error), + } + } else { + yield stream_accumulator.executor_error_chunk(&e); + } yield DONE_MARKER.to_string(); } - Ok((Ok((payload, ctx)), mut stream_accumulator)) => { + Ok((Ok((payload, ctx, registry)), mut stream_accumulator)) => { while let Ok(event) = event_rx.try_recv() { yield consume_stream_event(event, &mut next_sequence_number); } @@ -464,7 +504,7 @@ fn run_stream(ctx: RequestContext, exec_ctx: Arc, auth: Option let rh = exec_ctx.resp_handler.clone(); let mut terminal_accumulator = stream_accumulator.clone(); let terminal_chunk = terminal_accumulator.terminal_response_chunk(&payload); - match persist_if_needed(payload, ctx, ch, rh).await { + match persist_if_needed(payload, ctx, registry, ch, rh).await { Ok(()) => match terminal_chunk { Ok(chunk) => yield chunk, Err(e) => yield stream_accumulator.executor_error_chunk(&e), @@ -481,6 +521,51 @@ fn run_stream(ctx: RequestContext, exec_ctx: Arc, auth: Option }) } +struct StreamFailureContext { + response_id: String, + conversation_id: Option, + model: String, + previous_response_id: Option, + instructions: Option, +} + +impl From<&RequestContext> for StreamFailureContext { + fn from(ctx: &RequestContext) -> Self { + Self { + response_id: ctx.response_id.clone(), + conversation_id: ctx.conversation_id.clone(), + model: ctx.enriched_request.model.clone(), + previous_response_id: ctx.original_request.previous_response_id.clone(), + instructions: ctx.original_request.instructions.clone(), + } + } +} + +impl StreamFailureContext { + fn failed_payload(&self, error: &ExecutorError) -> ResponsePayload { + ResponsePayload { + id: self.response_id.clone(), + object: "response".to_owned(), + created_at: utcnow_str(), + model: self.model.clone(), + status: "failed".to_owned(), + output: Vec::new(), + usage: None, + incomplete_details: None, + error: Some(serde_json::json!({ + "message": error.error_message(), + "type": error.error_type(), + "code": error.error_code(), + })), + previous_response_id: self.previous_response_id.clone(), + conversation_id: self.conversation_id.clone(), + instructions: self.instructions.clone(), + tools: None, + tool_choice: None, + } + } +} + fn consume_stream_event(event: StreamEvent, next_sequence_number: &mut u64) -> String { *next_sequence_number = event.sequence_number.saturating_add(1); event.content @@ -563,11 +648,18 @@ impl ExecuteRequest { "executor received responses request" ); let ctx = rehydrate_conversation(self.payload, &self.exec_ctx).await?; + let (ctx, registry) = + prepare_request_tools(ctx, &self.exec_ctx.conv_handler, &self.exec_ctx.resp_handler).await?; if ctx.original_request.stream { - Ok(Either::Right(run_stream(ctx, self.exec_ctx, self.client_auth))) + Ok(Either::Right(run_stream( + ctx, + registry, + self.exec_ctx, + self.client_auth, + ))) } else { Ok(Either::Left( - run_blocking(ctx, &self.exec_ctx, self.client_auth.as_deref()).await?, + run_blocking(ctx, registry, &self.exec_ctx, self.client_auth.as_deref()).await?, )) } } @@ -740,6 +832,101 @@ mod tests { server.abort(); } + #[tokio::test] + async fn compaction_trigger_lowers_tool_search_history_and_retains_loaded_metadata() { + let captured = Arc::new(Mutex::new(None)); + let (mut exec_ctx, server) = trigger_execution_context(Arc::clone(&captured)).await; + let pool = create_pool_with_schema(Some("sqlite::memory:")) + .await + .expect("create response store"); + let response_store = ResponseStore::new(pool); + exec_ctx.resp_handler = ResponseHandler::new(response_store.clone()); + + let payload: RequestPayload = serde_json::from_value(serde_json::json!({ + "model": "test-model", + "store": true, + "parallel_tool_calls": false, + "tools": [ + { + "type": "tool_search", + "execution": "client", + "description": "Find a tool", + "parameters": {"type": "object"} + }, + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + } + ], + "input": [ + {"role": "user", "content": "find weather"}, + { + "type": "tool_search_call", + "id": "tsc_search", + "call_id": "call_search", + "arguments": {"query": "weather"} + }, + { + "type": "tool_search_output", + "call_id": "call_search", + "tools": [{ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }] + }, + {"type": "compaction_trigger"} + ] + })) + .expect("valid tool-search trigger request"); + let Either::Left(response) = ExecuteRequest::new(payload, Arc::new(exec_ctx)) + .run() + .await + .expect("tool-search trigger request succeeds") + else { + panic!("non-streaming trigger request must return a payload"); + }; + + let [OutputItem::Compaction(compaction)] = response.output.as_slice() else { + panic!("tool-search compaction trigger must return one compaction item"); + }; + assert_eq!(compaction.encrypted_content, "durable summary"); + + let upstream = captured.lock().await.take().expect("summary inference ran"); + let upstream_input = upstream["input"].as_array().expect("summary input items"); + assert!(upstream_input.iter().any(|item| { + item["type"] == "function_call" && item["name"] == "tool_search" && item["call_id"] == "call_search" + })); + assert!( + upstream_input + .iter() + .any(|item| { item["type"] == "function_call_output" && item["call_id"] == "call_search" }) + ); + assert!(upstream_input.iter().all(|item| { + !matches!( + item["type"].as_str(), + Some("tool_search_call" | "tool_search_output" | "compaction_trigger") + ) + })); + + let stored = response_store.get(&response.id).await.expect("stored trigger response"); + let loaded = stored + .metadata + .tool_search_loaded_tools + .as_deref() + .expect("loaded tool metadata retained"); + assert_eq!(loaded.len(), 1); + let loaded = serde_json::to_value(&loaded[0]).expect("loaded tool serializes"); + assert_eq!(loaded["name"], "get_weather"); + assert_eq!(loaded["defer_loading"], true); + server.abort(); + } + #[tokio::test] async fn compaction_trigger_streams_one_compaction_item_then_completed() { let captured = Arc::new(Mutex::new(None)); diff --git a/crates/agentic-server-core/src/executor/error.rs b/crates/agentic-server-core/src/executor/error.rs index 5879bf74..0fe6d4d6 100644 --- a/crates/agentic-server-core/src/executor/error.rs +++ b/crates/agentic-server-core/src/executor/error.rs @@ -89,6 +89,13 @@ pub enum ExecutorError { } impl ExecutorError { + pub(crate) fn is_invalid_upstream_tool_search(&self) -> bool { + matches!( + self, + Self::Tool(ToolError::InvalidUpstreamToolSearch | ToolError::UpstreamWithheldFunctionCall) + ) + } + fn client_visible_error(&self) -> &Self { match self { Self::Persistence(source) if source.contains_conversation_locked() => source.client_visible_error(), @@ -114,7 +121,12 @@ impl ExecutorError { | Self::Tool(ToolError::Config(_)) | Self::InvalidRequest(_) | Self::JsonError(_) => StatusCode::BAD_REQUEST, - Self::Tool(ToolError::Execution(_)) | Self::CompactionFailed { .. } => StatusCode::BAD_GATEWAY, + Self::Tool( + ToolError::Execution(_) + | ToolError::InvalidUpstreamToolSearch + | ToolError::UpstreamWithheldFunctionCall, + ) + | Self::CompactionFailed { .. } => StatusCode::BAD_GATEWAY, Self::ParseError(_) => StatusCode::UNPROCESSABLE_ENTITY, _ => StatusCode::INTERNAL_SERVER_ERROR, } @@ -131,7 +143,11 @@ impl ExecutorError { | Self::JsonError(_) => "invalid_request_error", Self::Storage(e) if e.is_not_found() => "not_found", Self::LLMRequest { .. } | Self::LLMTransport { .. } | Self::CompactionFailed { .. } => "upstream_error", - Self::Tool(ToolError::Execution(_)) => "tool_error", + Self::Tool( + ToolError::Execution(_) + | ToolError::InvalidUpstreamToolSearch + | ToolError::UpstreamWithheldFunctionCall, + ) => "tool_error", _ => "server_error", } } @@ -217,6 +233,14 @@ mod tests { assert!(exec_err.to_string().contains("storage error")); } + #[test] + fn tool_search_configuration_errors_are_bad_requests() { + let error = ExecutorError::from(ToolError::Config("invalid tool_search request".to_owned())); + + assert_eq!(error.http_status(), StatusCode::BAD_REQUEST); + assert_eq!(error.error_type(), "invalid_request_error"); + } + #[test] fn test_executor_error_json_preserves_source() { use std::error::Error; diff --git a/crates/agentic-server-core/src/executor/function_sse.rs b/crates/agentic-server-core/src/executor/function_sse.rs index 2f654e44..d7d47229 100644 --- a/crates/agentic-server-core/src/executor/function_sse.rs +++ b/crates/agentic-server-core/src/executor/function_sse.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use serde_json::Value; @@ -6,8 +6,9 @@ use crate::events::{EventFrame, EventPayload, SSEEventType, SSEItemType}; use crate::executor::accumulator::AccumulatedFunctionCall; use crate::executor::error::{ExecutorError, ExecutorResult}; use crate::executor::gateway_accumulator::synthetic_event; -use crate::tool::ToolType; -use crate::utils::common::serialize_to_string; +use crate::tool::{ToolRegistry, ToolType, tool_search}; +use crate::types::io::OutputItem; +use crate::utils::common::{serialize_to_string, serialize_to_value}; const MAX_PENDING_FUNCTION_BYTES: usize = 256 * 1024; @@ -16,6 +17,14 @@ enum FunctionCallShape { PublicFunction, GatewayOwned, Custom(CustomCallState), + ToolSearch { internal_item_id: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StreamTerminalState { + Open, + Completed, + Aborted, } #[derive(Debug)] @@ -31,6 +40,7 @@ struct CustomCallState { #[derive(Debug, Default)] struct PendingFunctionCall { output_index: u32, + internal_item_id: Option, frames: Vec, bytes: usize, } @@ -41,23 +51,35 @@ pub(super) struct FunctionSseTranslation { pub(super) defer_from_output_index: Option, } -/// Restores normalized upstream function-call SSE to the public call shape. -/// Tool routing remains outside this type; it receives only the request's -/// model-visible name-to-type mapping. #[derive(Debug, Default)] -pub(super) struct FunctionSseTranslator { - tool_types: HashMap, +pub(super) struct FunctionSseOutcome { + pub(super) unfinished_tool_search_item_ids: HashSet, +} + +/// Restores normalized upstream function-call SSE to the public call shape. +/// Tool routing remains in the request-scoped registry; this type borrows its +/// classification facts while owning only per-stream lifecycle state. +#[derive(Debug)] +pub(super) struct FunctionSseTranslator<'a> { + registry: &'a ToolRegistry, active: HashMap, pending_unnamed: HashMap, pending_bytes: usize, first_gateway_output_index: Option, + active_native_tool_search: HashSet, + terminal: StreamTerminalState, } -impl FunctionSseTranslator { - pub(super) fn new(tool_types: HashMap) -> Self { +impl<'a> FunctionSseTranslator<'a> { + pub(super) fn new(registry: &'a ToolRegistry) -> Self { Self { - tool_types, - ..Self::default() + registry, + active: HashMap::new(), + pending_unnamed: HashMap::new(), + pending_bytes: 0, + first_gateway_output_index: None, + active_native_tool_search: HashSet::new(), + terminal: StreamTerminalState::Open, } } @@ -66,6 +88,13 @@ impl FunctionSseTranslator { frame: EventFrame, call: Option>, ) -> ExecutorResult { + self.validate_frame(&frame)?; + self.track_native_tool_search(&frame)?; + self.terminal = match frame.event_type { + SSEEventType::ResponseCompleted => StreamTerminalState::Completed, + SSEEventType::ResponseFailed | SSEEventType::ResponseIncomplete => StreamTerminalState::Aborted, + _ => self.terminal, + }; let mut translated = match &frame.payload { EventPayload::OutputItemAdded { item_id, @@ -75,12 +104,15 @@ impl FunctionSseTranslator { .. } => self.start_call(item_id, name, *output_index, Some(frame.clone()), call), EventPayload::OutputItemAdded { - item_id: _, + item_id, item_type: SSEItemType::FunctionCall, output_index, name: None, .. - } => self.buffer_unnamed(*output_index, frame), + } => { + let item_id = item_id.clone(); + self.buffer_unnamed(&item_id, *output_index, frame, call) + } EventPayload::FunctionCallArgsDelta { item_id, output_index, .. } => self.translate_delta(item_id, *output_index, frame.clone(), call), @@ -108,6 +140,18 @@ impl FunctionSseTranslator { Ok(translated) } + pub(super) fn finish(self) -> ExecutorResult { + let unfinished_tool_search_item_ids = self.unfinished_tool_search_item_ids(); + if self.terminal != StreamTerminalState::Aborted + && (!unfinished_tool_search_item_ids.is_empty() || !self.active_native_tool_search.is_empty()) + { + return Err(tool_search::invalid_upstream_search_call().into()); + } + Ok(FunctionSseOutcome { + unfinished_tool_search_item_ids, + }) + } + fn start_call( &mut self, item_id: &str, @@ -116,7 +160,13 @@ impl FunctionSseTranslator { original: Option, call: Option>, ) -> ExecutorResult { - match self.tool_type(name) { + let tool_type = self.registry.tool_type(name); + if tool_type == ToolType::ToolSearch + && let Some(original) = original.as_ref() + { + validate_tool_search_added(original, name)?; + } + match tool_type { ToolType::Custom => { let public_item_id = call.as_ref().map_or_else( || crate::tool::custom::public_item_id(item_id), @@ -149,6 +199,20 @@ impl FunctionSseTranslator { self.active.insert(output_index, FunctionCallShape::GatewayOwned); Ok(FunctionSseTranslation::default()) } + ToolType::ToolSearch => { + let call = call.ok_or_else(|| ExecutorError::Tool(tool_search::invalid_upstream_search_call()))?; + let public = tool_search::started_public_call(call.item)?; + self.active.insert( + output_index, + FunctionCallShape::ToolSearch { + internal_item_id: call.item.id.clone(), + }, + ); + Ok(FunctionSseTranslation { + frames: vec![tool_search_frame(SSEEventType::OutputItemAdded, output_index, &public)?], + defer_from_output_index: None, + }) + } ToolType::Function | ToolType::CodexNamespace => { self.active.insert(output_index, FunctionCallShape::PublicFunction); Ok(FunctionSseTranslation { @@ -161,7 +225,7 @@ impl FunctionSseTranslator { fn translate_delta( &mut self, - _item_id: &str, + item_id: &str, output_index: u32, original: EventFrame, call: Option>, @@ -182,7 +246,13 @@ impl FunctionSseTranslator { defer_from_output_index: None, }) } - None => self.buffer_unnamed(output_index, original), + Some(FunctionCallShape::ToolSearch { .. }) => { + if let Some(call) = call { + ensure_function_call_size(call.arguments())?; + } + Ok(FunctionSseTranslation::default()) + } + None => self.buffer_unnamed(item_id, output_index, original, call), } } @@ -203,6 +273,11 @@ impl FunctionSseTranslator { translated.frames.extend(finish_custom_input(state, call.arguments())?); } } + Some(FunctionCallShape::ToolSearch { .. }) => { + let call = call.ok_or_else(|| ExecutorError::Tool(tool_search::invalid_upstream_search_call()))?; + ensure_function_call_size(call.arguments())?; + tool_search::validate_public_arguments(call.arguments())?; + } } Ok(translated) } @@ -227,6 +302,18 @@ impl FunctionSseTranslator { translated.frames.push(custom_done_frame(&state, &call)?); } } + Some(shape @ FunctionCallShape::ToolSearch { .. }) => { + let call = call.ok_or_else(|| ExecutorError::Tool(tool_search::invalid_upstream_search_call()))?; + ensure_function_call_size(call.arguments())?; + if call.item.status == crate::types::event::MessageStatus::Completed { + let public = tool_search::completed_public_call(call.item)?; + translated + .frames + .push(tool_search_frame(SSEEventType::OutputItemDone, output_index, &public)?); + } else { + self.active.insert(output_index, shape); + } + } } Ok(translated) } @@ -263,8 +350,131 @@ impl FunctionSseTranslator { Ok(translated) } - fn tool_type(&self, name: &str) -> ToolType { - self.tool_types.get(name).copied().unwrap_or(ToolType::Function) + fn validate_frame(&self, frame: &EventFrame) -> ExecutorResult<()> { + let lifecycle_name = match &frame.payload { + EventPayload::OutputItemAdded { + item_type: SSEItemType::FunctionCall, + name: Some(name), + .. + } + | EventPayload::FunctionCallArgsDone { name, .. } => Some(name.as_str()), + EventPayload::OutputItemDone { + item_type: SSEItemType::FunctionCall, + item, + .. + } => item.get("name").and_then(Value::as_str), + _ => None, + }; + if let Some(name) = lifecycle_name { + tool_search::ensure_function_is_available(self.registry.is_withheld_function(name))?; + } + + match &frame.payload { + EventPayload::OutputItemDone { + item_type: SSEItemType::FunctionCall, + item, + .. + } if item + .get("name") + .and_then(Value::as_str) + .is_some_and(|name| self.registry.tool_type(name) == ToolType::ToolSearch) => + { + tool_search::strict_function_call(item)?; + } + EventPayload::Response { .. } + if matches!( + frame.event_type, + SSEEventType::ResponseCompleted | SSEEventType::ResponseFailed | SSEEventType::ResponseIncomplete + ) => + { + self.validate_terminal_output(frame)?; + } + _ => {} + } + Ok(()) + } + + fn validate_terminal_output(&self, frame: &EventFrame) -> ExecutorResult<()> { + let Some(output) = frame + .wire + .rest + .get("response") + .and_then(|response| response.get("output")) + .and_then(Value::as_array) + else { + return Ok(()); + }; + let completed = frame.event_type == SSEEventType::ResponseCompleted; + for item in output { + match item.get("type").and_then(Value::as_str) { + Some("function_call") => { + let name = item.get("name").and_then(Value::as_str).unwrap_or_default(); + tool_search::ensure_function_is_available(self.registry.is_withheld_function(name))?; + if self.registry.tool_type(name) == ToolType::ToolSearch { + let call = tool_search::strict_function_call(item)?; + ensure_function_call_size(&call.arguments)?; + if completed && call.status != crate::types::event::MessageStatus::Completed { + return Err(tool_search::invalid_upstream_search_call().into()); + } + } + } + Some("tool_search_call") => { + let call = tool_search::strict_native_call(item.clone())?; + let arguments = serialize_to_string(&call.arguments).map_err(ExecutorError::JsonError)?; + ensure_function_call_size(&arguments)?; + if completed && call.status != crate::types::tools::ToolSearchStatus::Completed { + return Err(tool_search::invalid_upstream_search_call().into()); + } + } + _ => {} + } + } + Ok(()) + } + + fn track_native_tool_search(&mut self, frame: &EventFrame) -> ExecutorResult<()> { + match &frame.payload { + EventPayload::OutputItemAdded { + item_type: SSEItemType::ToolSearchCall, + output_index, + .. + } => { + validate_native_tool_search_frame(frame)?; + self.active_native_tool_search.insert(*output_index); + } + EventPayload::OutputItemDone { + item_type: SSEItemType::ToolSearchCall, + output_index, + .. + } => { + let call = validate_native_tool_search_frame(frame)?; + if call.status == crate::types::tools::ToolSearchStatus::Completed { + self.active_native_tool_search.remove(output_index); + } else { + self.active_native_tool_search.insert(*output_index); + } + } + _ => {} + } + Ok(()) + } + + fn unfinished_tool_search_item_ids(&self) -> HashSet { + let active = self.active.values().filter_map(|shape| match shape { + FunctionCallShape::ToolSearch { internal_item_id } => Some(internal_item_id.clone()), + FunctionCallShape::PublicFunction | FunctionCallShape::GatewayOwned | FunctionCallShape::Custom(_) => None, + }); + let pending = self + .registry + .tool_search_is_active() + .then(|| { + self.pending_unnamed + .values() + .filter_map(|pending| pending.internal_item_id.clone()) + }) + .into_iter() + .flatten(); + active.chain(pending).collect() } fn defer_from_output_index(&self) -> Option { @@ -274,7 +484,13 @@ impl FunctionSseTranslator { .min() } - fn buffer_unnamed(&mut self, output_index: u32, frame: EventFrame) -> ExecutorResult { + fn buffer_unnamed( + &mut self, + item_id: &str, + output_index: u32, + frame: EventFrame, + call: Option>, + ) -> ExecutorResult { let bytes = serialize_to_string(&frame.wire) .map_err(ExecutorError::JsonError)? .len(); @@ -290,6 +506,14 @@ impl FunctionSseTranslator { output_index, ..PendingFunctionCall::default() }); + if let Some(internal_item_id) = call + .map(|call| call.item.id.as_str()) + .filter(|item_id| !item_id.is_empty()) + { + pending.internal_item_id = Some(internal_item_id.to_owned()); + } else if pending.internal_item_id.is_none() && !item_id.is_empty() { + pending.internal_item_id = Some(item_id.to_owned()); + } pending.frames.push(frame); pending.bytes = pending.bytes.saturating_add(bytes); self.pending_bytes = self.pending_bytes.saturating_add(bytes); @@ -305,6 +529,53 @@ impl FunctionSseTranslator { } } +fn validate_tool_search_added(frame: &EventFrame, name: &str) -> ExecutorResult<()> { + let Some(item) = frame.wire.rest.get("item") else { + return Err(tool_search::invalid_upstream_search_call().into()); + }; + let mut item = item.clone(); + match item.get("name") { + None | Some(Value::Null) => { + item.as_object_mut() + .ok_or_else(tool_search::invalid_upstream_search_call)? + .insert("name".to_owned(), Value::String(name.to_owned())); + } + Some(Value::String(_)) => {} + Some(_) => return Err(tool_search::invalid_upstream_search_call().into()), + } + tool_search::strict_started_function(&item)?; + Ok(()) +} + +fn validate_native_tool_search_frame(frame: &EventFrame) -> ExecutorResult { + let item = frame + .wire + .rest + .get("item") + .cloned() + .ok_or_else(tool_search::invalid_upstream_search_call)?; + let call = tool_search::strict_native_call(item)?; + let arguments = serialize_to_string(&call.arguments).map_err(ExecutorError::JsonError)?; + ensure_function_call_size(&arguments)?; + if frame.event_type == SSEEventType::OutputItemAdded + && (call.status != crate::types::tools::ToolSearchStatus::InProgress || !call.arguments.is_empty()) + { + return Err(tool_search::invalid_upstream_search_call().into()); + } + Ok(call) +} + +fn tool_search_frame( + event_type: SSEEventType, + output_index: u32, + call: &crate::types::io::ToolSearchCall, +) -> ExecutorResult { + let item = serialize_to_value(&OutputItem::ToolSearchCall(call.clone())).map_err(ExecutorError::JsonError)?; + let mut frame = synthetic_event(event_type, [("item".to_owned(), item)])?; + frame.wire.output_index = Some(u64::from(output_index)); + Ok(frame) +} + fn custom_added_frame(call: &AccumulatedFunctionCall<'_>) -> ExecutorResult { custom_frame( SSEEventType::OutputItemAdded, @@ -513,6 +784,10 @@ mod tests { use super::*; use crate::executor::accumulator::ResponseAccumulator; + fn test_registry(tool_types: HashMap) -> ToolRegistry { + ToolRegistry::from_tool_types(tool_types) + } + fn sse(value: &Value) -> String { format!("data: {value}") } @@ -531,7 +806,8 @@ mod tests { #[test] fn custom_function_arguments_are_emitted_incrementally() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let registry = test_registry(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let mut translator = FunctionSseTranslator::new(®istry); let mut frames = Vec::new(); for event in [ @@ -612,7 +888,8 @@ mod tests { #[test] fn custom_input_deltas_match_authoritative_done_input() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let registry = test_registry(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let mut translator = FunctionSseTranslator::new(®istry); let events = [ serde_json::json!({ "type": "response.output_item.added", "output_index": 0, @@ -656,7 +933,8 @@ mod tests { #[test] fn custom_input_rejects_authoritative_value_that_contradicts_deltas() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let registry = test_registry(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let mut translator = FunctionSseTranslator::new(®istry); let events = [ serde_json::json!({ "type": "response.output_item.added", "output_index": 0, @@ -686,7 +964,8 @@ mod tests { #[test] fn malformed_custom_input_escape_is_rejected() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let registry = test_registry(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let mut translator = FunctionSseTranslator::new(®istry); let added = serde_json::json!({ "type": "response.output_item.added", "output_index": 0, "item": {"id": "fc_1", "type": "function_call", "call_id": "call_1", @@ -707,7 +986,8 @@ mod tests { #[test] fn custom_input_waits_for_split_unicode_surrogate_pair() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let registry = test_registry(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let mut translator = FunctionSseTranslator::new(®istry); let events = [ serde_json::json!({ "type": "response.output_item.added", "output_index": 0, @@ -740,7 +1020,8 @@ mod tests { #[test] fn custom_input_over_limit_is_rejected() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let registry = test_registry(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let mut translator = FunctionSseTranslator::new(®istry); let added = serde_json::json!({ "type": "response.output_item.added", "output_index": 0, "item": {"id": "fc_1", "type": "function_call", "call_id": "call_1", @@ -762,7 +1043,8 @@ mod tests { #[test] fn ordinary_functions_pass_through_unchanged() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([("echo".to_owned(), ToolType::Function)])); + let registry = test_registry(HashMap::from([("echo".to_owned(), ToolType::Function)])); + let mut translator = FunctionSseTranslator::new(®istry); let event = serde_json::json!({ "type": "response.output_item.added", "output_index": 3, @@ -786,7 +1068,8 @@ mod tests { #[test] fn unnamed_function_frames_are_recovered_by_output_index_when_done_changes_id() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([("echo".to_owned(), ToolType::Function)])); + let registry = test_registry(HashMap::from([("echo".to_owned(), ToolType::Function)])); + let mut translator = FunctionSseTranslator::new(®istry); let mut frames = Vec::new(); let mut defer_boundaries = Vec::new(); @@ -844,10 +1127,11 @@ mod tests { #[test] fn parallel_unnamed_functions_with_empty_ids_remain_distinct() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([ + let registry = test_registry(HashMap::from([ ("first".to_owned(), ToolType::Function), ("second".to_owned(), ToolType::Function), ])); + let mut translator = FunctionSseTranslator::new(®istry); let events = [ serde_json::json!({ "type": "response.output_item.added", "output_index": 0, @@ -901,10 +1185,11 @@ mod tests { #[test] fn parallel_named_custom_functions_with_empty_ids_remain_distinct() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([ + let registry = test_registry(HashMap::from([ ("first".to_owned(), ToolType::Custom), ("second".to_owned(), ToolType::Custom), ])); + let mut translator = FunctionSseTranslator::new(®istry); let events = [ serde_json::json!({ "type": "response.output_item.added", "output_index": 0, @@ -942,7 +1227,8 @@ mod tests { #[test] fn unnamed_custom_function_with_empty_id_uses_one_public_id() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let registry = test_registry(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)])); + let mut translator = FunctionSseTranslator::new(®istry); let events = [ serde_json::json!({ "type": "response.output_item.added", "output_index": 0, @@ -983,8 +1269,8 @@ mod tests { #[test] fn gateway_owned_functions_are_suppressed_and_mark_the_defer_boundary() { let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); - let mut translator = - FunctionSseTranslator::new(HashMap::from([("web_search".to_owned(), ToolType::WebSearch)])); + let registry = test_registry(HashMap::from([("web_search".to_owned(), ToolType::WebSearch)])); + let mut translator = FunctionSseTranslator::new(®istry); let added = serde_json::json!({ "type": "response.output_item.added", "output_index": 2, @@ -1012,4 +1298,426 @@ mod tests { assert!(delta.frames.is_empty()); assert_eq!(delta.defer_from_output_index, Some(2)); } + + #[test] + fn synthetic_tool_search_emits_public_frames_but_accumulates_function_call() { + let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let registry = test_registry(HashMap::from([("tool_search".to_owned(), ToolType::ToolSearch)])); + let mut translator = FunctionSseTranslator::new(®istry); + let events = [ + serde_json::json!({ + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "fc_search", + "type": "function_call", + "call_id": "call_search", + "name": "tool_search", + "arguments": "", + "status": "in_progress" + } + }), + serde_json::json!({ + "type": "response.function_call_arguments.delta", + "output_index": 0, + "item_id": "fc_search", + "call_id": "call_search", + "delta": "{\"query\":\"weather\"}" + }), + serde_json::json!({ + "type": "response.function_call_arguments.done", + "output_index": 0, + "item_id": "fc_search", + "call_id": "call_search", + "name": "tool_search", + "arguments": "{\"query\":\"weather\"}" + }), + serde_json::json!({ + "type": "response.output_item.done", + "output_index": 0, + "item": { + "id": "fc_search", + "type": "function_call", + "call_id": "call_search", + "name": "tool_search", + "arguments": "{\"query\":\"weather\"}", + "status": "completed" + } + }), + serde_json::json!({ + "type": "response.completed", + "response": {"id": "resp_1", "status": "completed", "output": []} + }), + ]; + + let frames = events + .iter() + .flat_map(|event| translate(&mut accumulator, &mut translator, event).frames) + .collect::>(); + let outcome = translator.finish().expect("completed lifecycle"); + assert!(outcome.unfinished_tool_search_item_ids.is_empty()); + assert_eq!( + frames + .iter() + .filter(|frame| { + matches!( + frame.event_type, + SSEEventType::OutputItemAdded | SSEEventType::OutputItemDone + ) && frame.wire.rest["item"]["type"] == "tool_search_call" + }) + .count(), + 2 + ); + assert!( + frames + .iter() + .all(|frame| !matches!(frame.event_type, SSEEventType::FunctionCallArgumentsDelta)) + ); + + let payload = accumulator.finalize("test", None, None); + assert!(matches!(payload.output.as_slice(), [OutputItem::FunctionCall(_)])); + } + + #[test] + fn synthetic_tool_search_rejects_non_string_name_in_buffered_added_item() { + let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let registry = test_registry(HashMap::from([("tool_search".to_owned(), ToolType::ToolSearch)])); + let mut translator = FunctionSseTranslator::new(®istry); + translate( + &mut accumulator, + &mut translator, + &serde_json::json!({ + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "fc_search", "type": "function_call", "call_id": "call_search", + "name": 7, "arguments": "", "status": "in_progress" + } + }), + ); + let arguments_done = serde_json::json!({ + "type": "response.function_call_arguments.done", + "output_index": 0, + "item_id": "fc_search", + "call_id": "call_search", + "name": "tool_search", + "arguments": "{}" + }); + + let error = accumulator + .process_sse_line_with_translator(&sse(&arguments_done), &mut translator) + .expect_err("a malformed buffered name must not be overwritten"); + assert!(matches!( + error, + ExecutorError::Tool(crate::tool::ToolError::InvalidUpstreamToolSearch) + )); + } + + #[test] + fn native_tool_search_frames_pass_through_and_accumulate_natively() { + let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let registry = test_registry(HashMap::new()); + let mut translator = FunctionSseTranslator::new(®istry); + let events = [ + serde_json::json!({ + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "tsc_native", + "type": "tool_search_call", + "call_id": "call_search", + "execution": "client", + "arguments": {}, + "status": "in_progress" + } + }), + serde_json::json!({ + "type": "response.output_item.done", + "output_index": 0, + "item": { + "id": "tsc_native", + "type": "tool_search_call", + "call_id": "call_search", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + } + }), + serde_json::json!({ + "type": "response.completed", + "response": {"id": "resp_1", "status": "completed", "output": []} + }), + ]; + + let frames = events + .iter() + .flat_map(|event| translate(&mut accumulator, &mut translator, event).frames) + .collect::>(); + translator.finish().expect("completed native lifecycle"); + assert_eq!(frames[0].wire.rest["item"]["type"], "tool_search_call"); + assert_eq!(frames[1].wire.rest["item"]["type"], "tool_search_call"); + + let payload = accumulator.finalize("test", None, None); + let [OutputItem::ToolSearchCall(call)] = payload.output.as_slice() else { + panic!("native tool_search_call must remain typed"); + }; + assert_eq!(call.arguments["query"], "weather"); + } + + #[test] + fn oversized_native_tool_search_done_arguments_are_rejected() { + let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let registry = test_registry(HashMap::new()); + let mut translator = FunctionSseTranslator::new(®istry); + let done = serde_json::json!({ + "type": "response.output_item.done", + "output_index": 0, + "item": { + "id": "tsc_native", + "type": "tool_search_call", + "call_id": "call_search", + "execution": "client", + "arguments": {"query": "x".repeat(MAX_PENDING_FUNCTION_BYTES)}, + "status": "completed" + } + }); + + let error = accumulator + .process_sse_line_with_translator(&sse(&done), &mut translator) + .expect_err("oversized native arguments must fail"); + assert!(error.to_string().contains("function-call SSE exceeded")); + } + + #[test] + fn oversized_synthetic_tool_search_done_arguments_are_rejected() { + let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let registry = test_registry(HashMap::from([("tool_search".to_owned(), ToolType::ToolSearch)])); + let mut translator = FunctionSseTranslator::new(®istry); + let done = serde_json::json!({ + "type": "response.output_item.done", + "output_index": 0, + "item": { + "id": "fc_search", + "type": "function_call", + "call_id": "call_search", + "name": "tool_search", + "arguments": format!("{{\"query\":\"{}\"}}", "x".repeat(MAX_PENDING_FUNCTION_BYTES)), + "status": "completed" + } + }); + + let error = accumulator + .process_sse_line_with_translator(&sse(&done), &mut translator) + .expect_err("oversized synthetic arguments must fail"); + assert!(error.to_string().contains("function-call SSE exceeded")); + } + + #[test] + fn successful_eof_rejects_unfinished_synthetic_and_native_searches() { + let synthetic_registry = test_registry(HashMap::from([("tool_search".to_owned(), ToolType::ToolSearch)])); + let mut synthetic_accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let mut synthetic = FunctionSseTranslator::new(&synthetic_registry); + translate( + &mut synthetic_accumulator, + &mut synthetic, + &serde_json::json!({ + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "fc_search", "type": "function_call", "call_id": "call_search", + "name": "tool_search", "arguments": "", "status": "in_progress" + } + }), + ); + assert!(synthetic.finish().is_err()); + + let native_registry = test_registry(HashMap::new()); + let mut native_accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let mut native = FunctionSseTranslator::new(&native_registry); + translate( + &mut native_accumulator, + &mut native, + &serde_json::json!({ + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "tsc_native", "type": "tool_search_call", "call_id": "call_search", + "execution": "client", "arguments": {}, "status": "in_progress" + } + }), + ); + assert!(native.finish().is_err()); + } + + #[test] + fn aborted_stream_reports_and_discards_unfinished_synthetic_search() { + let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let registry = test_registry(HashMap::from([("tool_search".to_owned(), ToolType::ToolSearch)])); + let mut translator = FunctionSseTranslator::new(®istry); + for event in [ + serde_json::json!({ + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "fc_search", "type": "function_call", "call_id": "call_search", + "name": "tool_search", "arguments": "", "status": "in_progress" + } + }), + serde_json::json!({ + "type": "response.output_item.done", + "output_index": 0, + "item": { + "id": "fc_search", "type": "function_call", "call_id": "call_search", + "name": "tool_search", "arguments": "{\"query\":", "status": "in_progress" + } + }), + serde_json::json!({ + "type": "response.incomplete", + "response": {"id": "resp_1", "status": "incomplete", "output": []} + }), + ] { + translate(&mut accumulator, &mut translator, &event); + } + + let outcome = translator.finish().expect("aborted lifecycle may remain unfinished"); + assert_eq!( + outcome.unfinished_tool_search_item_ids, + HashSet::from(["fc_search".to_owned()]) + ); + let mut payload = accumulator.finalize("test", None, None); + registry + .normalize_response_output( + &mut payload.output, + crate::types::event::ResponseStatus::Incomplete, + &outcome.unfinished_tool_search_item_ids, + ) + .expect("unfinished synthetic call is discarded"); + assert!(payload.output.is_empty()); + } + + #[test] + fn aborted_stream_discards_unnamed_search_candidate_with_empty_raw_id() { + for (terminal_event, terminal_status, response_status) in [ + ( + "response.incomplete", + "incomplete", + crate::types::event::ResponseStatus::Incomplete, + ), + ("response.failed", "failed", crate::types::event::ResponseStatus::Error), + ] { + let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let registry = test_registry(HashMap::from([("tool_search".to_owned(), ToolType::ToolSearch)])); + let mut translator = FunctionSseTranslator::new(®istry); + for event in [ + serde_json::json!({ + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "", "type": "function_call", "call_id": "call_search", + "arguments": "", "status": "in_progress" + } + }), + serde_json::json!({ + "type": terminal_event, + "response": {"id": "resp_1", "status": terminal_status, "output": []} + }), + ] { + translate(&mut accumulator, &mut translator, &event); + } + + let outcome = translator + .finish() + .expect("aborted lifecycle may leave the call unnamed"); + assert_eq!(outcome.unfinished_tool_search_item_ids.len(), 1); + let internal_item_id = outcome + .unfinished_tool_search_item_ids + .iter() + .next() + .expect("accumulator-generated item id"); + assert!(internal_item_id.starts_with("fc_")); + + let mut payload = accumulator.finalize("test", None, None); + let [OutputItem::FunctionCall(call)] = payload.output.as_slice() else { + panic!("unfinished unnamed call must be accumulated as a function call"); + }; + assert!(call.name.is_empty()); + assert_eq!(&call.id, internal_item_id); + registry + .normalize_response_output( + &mut payload.output, + response_status, + &outcome.unfinished_tool_search_item_ids, + ) + .expect("unfinished unnamed search candidate is discarded"); + assert!(payload.output.is_empty()); + } + } + + #[test] + fn aborted_stream_discards_unfinished_native_search() { + let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let registry = test_registry(HashMap::new()); + let mut translator = FunctionSseTranslator::new(®istry); + for event in [ + serde_json::json!({ + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "tsc_native", "type": "tool_search_call", "call_id": "call_search", + "execution": "client", "arguments": {}, "status": "in_progress" + } + }), + serde_json::json!({ + "type": "response.output_item.done", + "output_index": 0, + "item": { + "id": "tsc_native", "type": "tool_search_call", "call_id": "call_search", + "execution": "client", "arguments": {}, "status": "incomplete" + } + }), + serde_json::json!({ + "type": "response.failed", + "response": {"id": "resp_1", "status": "failed", "output": []} + }), + ] { + translate(&mut accumulator, &mut translator, &event); + } + + let outcome = translator + .finish() + .expect("aborted native lifecycle may remain unfinished"); + assert!(outcome.unfinished_tool_search_item_ids.is_empty()); + let mut payload = accumulator.finalize("test", None, None); + registry + .normalize_response_output( + &mut payload.output, + crate::types::event::ResponseStatus::Error, + &outcome.unfinished_tool_search_item_ids, + ) + .expect("unfinished native call is discarded"); + assert!(payload.output.is_empty()); + } + + #[test] + fn unresolved_ordinary_function_does_not_become_tool_search_failure() { + let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None); + let registry = test_registry(HashMap::from([("echo".to_owned(), ToolType::Function)])); + let mut translator = FunctionSseTranslator::new(®istry); + translate( + &mut accumulator, + &mut translator, + &serde_json::json!({ + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "fc_ordinary", "type": "function_call", "call_id": "call_ordinary", + "arguments": "", "status": "in_progress" + } + }), + ); + + translator + .finish() + .expect("ordinary unnamed call is not a search candidate"); + } } diff --git a/crates/agentic-server-core/src/executor/gateway.rs b/crates/agentic-server-core/src/executor/gateway.rs index f5721718..b2e48cd0 100644 --- a/crates/agentic-server-core/src/executor/gateway.rs +++ b/crates/agentic-server-core/src/executor/gateway.rs @@ -197,6 +197,10 @@ async fn execute_gateway_call_with_timeout( Err(ToolError::Execution(message) | ToolError::Config(message)) => { (execution_error_output(&call, &message)?, GatewayCallStatus::Failed) } + Err(error @ (ToolError::InvalidUpstreamToolSearch | ToolError::UpstreamWithheldFunctionCall)) => ( + execution_error_output(&call, &error.to_string())?, + GatewayCallStatus::Failed, + ), }; let public_output = gateway_public_output(dispatch.tool_type, &call, &output, status, registry); Ok(GatewayCallResult { @@ -219,6 +223,7 @@ fn gateway_public_output( .mcp_tool_ref(&call.name) .map(|tool_ref| crate::tool::mcp::handler::output_item(call, output, status, tool_ref)), ToolType::Function + | ToolType::ToolSearch | ToolType::Custom | ToolType::CodexNamespace | ToolType::FileSearch @@ -296,6 +301,7 @@ pub(super) fn gateway_event_plans( .mcp_tool_ref(&call.name) .map(|tool_ref| crate::tool::mcp::handler::started_output_item(call, tool_ref)), ToolType::Function + | ToolType::ToolSearch | ToolType::Custom | ToolType::CodexNamespace | ToolType::FileSearch @@ -455,6 +461,7 @@ pub(super) fn emit_gateway_start_events( } OutputItem::Message(_) | OutputItem::FunctionCall(_) + | OutputItem::ToolSearchCall(_) | OutputItem::CustomToolCall(_) | OutputItem::Reasoning(_) | OutputItem::Compaction(_) @@ -502,6 +509,7 @@ pub(super) fn emit_gateway_completed_events( OutputItem::Compaction(_) => None, OutputItem::Message(_) | OutputItem::FunctionCall(_) + | OutputItem::ToolSearchCall(_) | OutputItem::CustomToolCall(_) | OutputItem::Reasoning(_) | OutputItem::Unknown => continue, diff --git a/crates/agentic-server-core/src/executor/mod.rs b/crates/agentic-server-core/src/executor/mod.rs index 7f261d72..6023f0a5 100644 --- a/crates/agentic-server-core/src/executor/mod.rs +++ b/crates/agentic-server-core/src/executor/mod.rs @@ -11,6 +11,7 @@ mod messages_request; pub mod messages_stream; pub mod modes; pub mod persist; +mod prepare; pub mod rehydrate; pub mod request; diff --git a/crates/agentic-server-core/src/executor/modes/conversation.rs b/crates/agentic-server-core/src/executor/modes/conversation.rs index e5479fab..4a771ad6 100644 --- a/crates/agentic-server-core/src/executor/modes/conversation.rs +++ b/crates/agentic-server-core/src/executor/modes/conversation.rs @@ -1,7 +1,8 @@ //! Conversation storage handler — owns all conversation store operations. use crate::storage::{ - ConversationData, ConversationSnapshot, ConversationStore, InOutItem, ResponseMetadata, StorageError, + ConversationData, ConversationSnapshot, ConversationStore, ConversationVersion, InOutItem, ResponseMetadata, + StorageError, }; use crate::types::io::OutputItem; @@ -91,16 +92,54 @@ impl ConversationHandler { .map_err(ExecutorError::Storage) } + /// Loads metadata for the persisted turn matching a captured conversation version. + /// + /// # Errors + /// Returns `ExecutorError` if `conversation_id` is absent, the store is + /// disabled, or the database query fails. + pub(crate) async fn response_metadata_at_version( + &self, + ctx: &RequestContext, + version: ConversationVersion, + ) -> ExecutorResult> { + let conv_id = ctx.original_request.conversation_id.as_deref().ok_or_else(|| { + ExecutorError::InvalidRequest("conversation_id is required for response metadata lookup".into()) + })?; + self.store + .response_metadata_at_version(conv_id, version) + .await + .map_err(ExecutorError::Storage) + } + /// Persists one conversation turn — only the new items from this turn. /// /// Takes `ctx` and `output_items` by value so fields can be moved directly - /// into [`ResponseMetadata`] without cloning. The store tracks sequence + /// into [`ResponseMetadata`]. The store tracks sequence /// numbers and appends, so prior history must not be re-inserted. /// /// # Errors /// Returns `ExecutorError` if `conversation_id` is absent on the context, /// the store is disabled, or the database operation fails. - pub async fn execute_turn(&self, ctx: RequestContext, output_items: Vec) -> ExecutorResult<()> { + pub async fn execute_turn(&self, mut ctx: RequestContext, output_items: Vec) -> ExecutorResult<()> { + let metadata = ResponseMetadata { + model: std::mem::take(&mut ctx.enriched_request.model), + previous_response_id: ctx.original_request.previous_response_id.take(), + effective_tools: ctx.enriched_request.tools.take(), + tool_search_loaded_tools: None, + effective_tool_choice: ctx.enriched_request.tool_choice.take().unwrap_or_default(), + effective_instructions: ctx.enriched_request.instructions.take(), + }; + + self.execute_turn_with_metadata(ctx, output_items, metadata).await + } + + /// Persists a conversation turn using metadata prepared by request-scoped tool behavior. + pub(crate) async fn execute_turn_with_metadata( + &self, + ctx: RequestContext, + output_items: Vec, + metadata: ResponseMetadata, + ) -> ExecutorResult<()> { let conversation_id = ctx .conversation_id .ok_or_else(|| ExecutorError::InvalidRequest("conversation_id is required for execute_turn".into()))?; @@ -108,14 +147,6 @@ impl ConversationHandler { .conversation_version .ok_or_else(|| ExecutorError::InvalidRequest("conversation version is required for execute_turn".into()))?; - let metadata = ResponseMetadata { - model: ctx.enriched_request.model, - previous_response_id: ctx.original_request.previous_response_id, - effective_tools: ctx.enriched_request.tools, - effective_tool_choice: ctx.enriched_request.tool_choice.unwrap_or_default(), - effective_instructions: ctx.enriched_request.instructions, - }; - let mut new_items = Vec::with_capacity(ctx.new_input_items.len() + output_items.len()); new_items.extend(ctx.new_input_items.into_iter().map(InOutItem::Input)); new_items.extend(output_items.into_iter().map(InOutItem::Output)); @@ -140,7 +171,7 @@ impl ConversationHandler { #[cfg(test)] mod tests { use super::*; - use crate::storage::{ConversationVersion, create_pool_with_schema}; + use crate::storage::{ConversationVersion, ResponseMetadata, create_pool_with_schema}; use crate::types::io::ResponsesInput; use crate::types::request_response::RequestPayload; diff --git a/crates/agentic-server-core/src/executor/modes/response.rs b/crates/agentic-server-core/src/executor/modes/response.rs index 842634ed..5d287ad4 100644 --- a/crates/agentic-server-core/src/executor/modes/response.rs +++ b/crates/agentic-server-core/src/executor/modes/response.rs @@ -63,20 +63,31 @@ impl ResponseHandler { /// Persists a response record — only the new items from this turn. /// /// Takes `ctx` and `output_items` by value so fields can be moved directly - /// into [`ResponseMetadata`] without cloning. Prior history must not be + /// into [`crate::storage::ResponseMetadata`]. Prior history must not be /// re-inserted; the response store records item IDs for this response only. /// /// # Errors /// Returns `ExecutorError` if the store is disabled or the database operation fails. - pub async fn execute_turn(&self, ctx: RequestContext, output_items: Vec) -> ExecutorResult<()> { + pub async fn execute_turn(&self, mut ctx: RequestContext, output_items: Vec) -> ExecutorResult<()> { let metadata = ResponseMetadata { - model: ctx.enriched_request.model, - previous_response_id: ctx.original_request.previous_response_id, - effective_tools: ctx.enriched_request.tools, - effective_tool_choice: ctx.enriched_request.tool_choice.unwrap_or_default(), - effective_instructions: ctx.enriched_request.instructions, + model: std::mem::take(&mut ctx.enriched_request.model), + previous_response_id: ctx.original_request.previous_response_id.take(), + effective_tools: ctx.enriched_request.tools.take(), + tool_search_loaded_tools: None, + effective_tool_choice: ctx.enriched_request.tool_choice.take().unwrap_or_default(), + effective_instructions: ctx.enriched_request.instructions.take(), }; + self.execute_turn_with_metadata(ctx, output_items, metadata).await + } + + /// Persists a response using metadata prepared by request-scoped tool behavior. + pub(crate) async fn execute_turn_with_metadata( + &self, + ctx: RequestContext, + output_items: Vec, + metadata: ResponseMetadata, + ) -> ExecutorResult<()> { let mut new_items = Vec::with_capacity(ctx.new_input_items.len() + output_items.len()); new_items.extend(ctx.new_input_items.into_iter().map(InOutItem::Input)); new_items.extend(output_items.into_iter().map(InOutItem::Output)); diff --git a/crates/agentic-server-core/src/executor/persist.rs b/crates/agentic-server-core/src/executor/persist.rs index d67ab5e8..e607fa75 100644 --- a/crates/agentic-server-core/src/executor/persist.rs +++ b/crates/agentic-server-core/src/executor/persist.rs @@ -5,7 +5,10 @@ use crate::executor::error::{ExecutorError, ExecutorResult}; use crate::executor::modes::{ConversationHandler, ResponseHandler}; +use crate::executor::prepare::prepare_request_tools; use crate::executor::request::RequestContext; +use crate::storage::ResponseMetadata; +use crate::tool::ToolRegistry; use crate::types::event::ResponseStatus; use crate::types::io::OutputItem; use crate::types::request_response::ResponsePayload; @@ -21,11 +24,12 @@ pub(crate) fn should_persist(ctx: &RequestContext) -> bool { pub(crate) async fn persist_if_needed( payload: ResponsePayload, ctx: RequestContext, + registry: ToolRegistry, conv_handler: ConversationHandler, resp_handler: ResponseHandler, ) -> ExecutorResult<()> { if should_persist(&ctx) { - persist_response(payload, ctx, conv_handler, resp_handler) + persist_prepared_response(payload, ctx, registry, conv_handler, resp_handler) .await .map_err(|source| { error!(error = ?source, "failed to persist response"); @@ -59,7 +63,26 @@ pub async fn persist_response( return Ok(()); } - persist_turn(ctx, payload.output, &conv_handler, &resp_handler).await + let (ctx, registry) = prepare_request_tools(ctx, &conv_handler, &resp_handler).await?; + persist_prepared_turn(ctx, registry, payload.output, &conv_handler, &resp_handler).await +} + +async fn persist_prepared_response( + payload: ResponsePayload, + ctx: RequestContext, + registry: ToolRegistry, + conv_handler: ConversationHandler, + resp_handler: ResponseHandler, +) -> ExecutorResult<()> { + if !matches!( + payload.status.parse::().unwrap_or_default(), + ResponseStatus::Completed | ResponseStatus::Incomplete + ) || payload.id.is_empty() + { + return Ok(()); + } + + persist_prepared_turn(ctx, registry, payload.output, &conv_handler, &resp_handler).await } /// Persists one completed turn with the handler selected by its explicit conversation discriminator. @@ -72,9 +95,37 @@ pub async fn persist_turn( conv_handler: &ConversationHandler, resp_handler: &ResponseHandler, ) -> ExecutorResult<()> { + let (ctx, registry) = prepare_request_tools(ctx, conv_handler, resp_handler).await?; + persist_prepared_turn(ctx, registry, output_items, conv_handler, resp_handler).await +} + +pub(crate) async fn persist_prepared_turn( + mut ctx: RequestContext, + mut registry: ToolRegistry, + output_items: Vec, + conv_handler: &ConversationHandler, + resp_handler: &ResponseHandler, +) -> ExecutorResult<()> { + let public_metadata = registry.take_tool_search_metadata(); + let mut metadata = ResponseMetadata { + model: std::mem::take(&mut ctx.enriched_request.model), + previous_response_id: ctx.original_request.previous_response_id.take(), + effective_tools: ctx.enriched_request.tools.take(), + tool_search_loaded_tools: None, + effective_tool_choice: ctx.enriched_request.tool_choice.take().unwrap_or_default(), + effective_instructions: ctx.enriched_request.instructions.take(), + }; + if let Some((effective_tools, loaded_tools)) = public_metadata { + metadata.effective_tools = effective_tools; + metadata.tool_search_loaded_tools = Some(loaded_tools); + } if ctx.original_request.conversation_id.is_some() { - conv_handler.execute_turn(ctx, output_items).await + conv_handler + .execute_turn_with_metadata(ctx, output_items, metadata) + .await } else { - resp_handler.execute_turn(ctx, output_items).await + resp_handler + .execute_turn_with_metadata(ctx, output_items, metadata) + .await } } diff --git a/crates/agentic-server-core/src/executor/prepare.rs b/crates/agentic-server-core/src/executor/prepare.rs new file mode 100644 index 00000000..e6fd817a --- /dev/null +++ b/crates/agentic-server-core/src/executor/prepare.rs @@ -0,0 +1,57 @@ +//! Explicit request-scoped tool preparation after public rehydration. + +use crate::executor::error::ExecutorResult; +use crate::executor::modes::{ConversationHandler, ResponseHandler}; +use crate::executor::rehydrate::apply_effective_settings; +use crate::executor::request::RequestContext; +use crate::tool::ToolRegistry; +use crate::types::tools::ResponsesTool; + +/// Prepare the tool-search projection for a fully rehydrated public request. +/// +/// Compaction may remove the call/output pair that records which deferred +/// definitions were loaded. Only that path performs a targeted metadata read; +/// ordinary rehydration does not gain an additional storage query. +pub(crate) async fn prepare_request_tools( + mut ctx: RequestContext, + conv_handler: &ConversationHandler, + resp_handler: &ResponseHandler, +) -> ExecutorResult<(RequestContext, ToolRegistry)> { + let restored_loaded_tools = restored_loaded_tools(&mut ctx, conv_handler, resp_handler).await?; + let restore_only_declared = ctx.original_request.tools.is_some(); + let registry = + ToolRegistry::prepare_request(&mut ctx.enriched_request, &restored_loaded_tools, restore_only_declared)?; + Ok((ctx, registry)) +} + +async fn restored_loaded_tools( + ctx: &mut RequestContext, + conv_handler: &ConversationHandler, + resp_handler: &ResponseHandler, +) -> ExecutorResult> { + if !ctx.enriched_request.input.contains_compaction() { + return Ok(Vec::new()); + } + + if ctx.original_request.previous_response_id.is_some() { + return Ok(resp_handler + .get(ctx) + .await? + .metadata + .tool_search_loaded_tools + .unwrap_or_default()); + } + + let Some(version) = ctx.conversation_version else { + return Ok(Vec::new()); + }; + let metadata = conv_handler.response_metadata_at_version(ctx, version).await?; + if let Some(metadata) = metadata { + let restored = metadata.tool_search_loaded_tools.clone().unwrap_or_default(); + if metadata.tool_search_loaded_tools.is_some() { + apply_effective_settings(ctx, &metadata); + } + return Ok(restored); + } + Ok(Vec::new()) +} diff --git a/crates/agentic-server-core/src/executor/rehydrate.rs b/crates/agentic-server-core/src/executor/rehydrate.rs index 6b9fdf08..0c1472d6 100644 --- a/crates/agentic-server-core/src/executor/rehydrate.rs +++ b/crates/agentic-server-core/src/executor/rehydrate.rs @@ -76,16 +76,7 @@ async fn from_response(ctx: &mut RequestContext, exec_ctx: &ExecutionContext) -> ctx.enriched_request.previous_response_id = None; ctx.enriched_request.input = ResponsesInput::Items(items); - ctx.enriched_request.tools = resolve_tools( - ctx.original_request.tools.as_deref(), - stored.metadata.effective_tools.as_deref(), - ctx.original_request.tools.is_some(), - ); - ctx.enriched_request.tool_choice = Some(resolve_tool_choice( - ctx.original_request.tool_choice.as_ref(), - &stored.metadata.effective_tool_choice, - ctx.original_request.tool_choice.is_some(), - )); + apply_effective_settings(ctx, &stored.metadata); ctx.conversation_id = stored.conversation_id; Ok(()) } @@ -116,6 +107,20 @@ async fn from_conversation(ctx: &mut RequestContext, exec_ctx: &ExecutionContext Ok(()) } +pub(crate) fn apply_effective_settings(ctx: &mut RequestContext, stored: &crate::storage::ResponseMetadata) { + let tools_explicitly_set = ctx.original_request.tools.is_some(); + ctx.enriched_request.tools = resolve_tools( + ctx.original_request.tools.as_deref(), + stored.effective_tools.as_deref(), + tools_explicitly_set, + ); + ctx.enriched_request.tool_choice = Some(resolve_tool_choice( + ctx.original_request.tool_choice.as_ref(), + &stored.effective_tool_choice, + ctx.original_request.tool_choice.is_some(), + )); +} + #[cfg(test)] mod tests { use std::sync::Arc; @@ -125,6 +130,7 @@ mod tests { use crate::storage::{ ConversationStore, ConversationVersion, InOutItem, ResponseMetadata, ResponseStore, create_pool_with_schema, }; + use crate::tool::ToolError; use crate::types::request_response::RequestPayload; fn request(conversation_id: Option<&str>, previous_response_id: Option<&str>) -> RequestPayload { @@ -208,6 +214,173 @@ mod tests { Ok(()) } + #[tokio::test] + async fn rehydration_remains_public_until_explicit_tool_search_preparation() { + let exec_ctx = execution_context(ConversationStore::disabled(), ResponseStore::disabled()); + let request: RequestPayload = serde_json::from_value(serde_json::json!({ + "model": "test", + "input": "find weather tools", + "store": false, + "tools": [{ + "type": "tool_search", + "execution": "client", + "description": "Find a tool", + "parameters": {"type": "object"} + }] + })) + .expect("valid tool-search request"); + + let ctx = rehydrate_conversation(request, &exec_ctx) + .await + .expect("blocking store:false search rehydrates"); + + assert!(matches!( + ctx.enriched_request.tools.as_deref(), + Some([crate::types::tools::ResponsesTool::ToolSearch(search)]) + if search.execution == crate::types::tools::ToolSearchExecution::Client + )); + + let (ctx, registry) = + crate::executor::prepare::prepare_request_tools(ctx, &exec_ctx.conv_handler, &exec_ctx.resp_handler) + .await + .expect("explicit handler preparation accepts the rehydrated request"); + + assert!( + registry + .tool_search_state() + .is_some_and(crate::tool::ToolSearchState::is_active) + ); + let upstream = ctx + .enriched_request + .to_upstream_request(false) + .expect("prepared tool-search request lowers at the upstream boundary"); + assert!(matches!( + upstream.tools.as_deref(), + Some([crate::types::request_response::UpstreamTool::Function(function)]) + if function.name == "tool_search" + )); + } + + #[tokio::test] + async fn execution_preparation_validates_tool_search_after_full_rehydration() { + let pool = create_pool_with_schema(Some("sqlite://?mode=memory")) + .await + .expect("create response store"); + let response_store = ResponseStore::new(pool); + let orphan: InputItem = serde_json::from_value(serde_json::json!({ + "type": "tool_search_output", + "call_id": "call_search_1", + "tools": [] + })) + .expect("valid public output item"); + response_store + .persist( + "resp_search", + None, + vec![InOutItem::Input(orphan)], + &ResponseMetadata::default(), + ) + .await + .expect("seed prior response"); + let exec_ctx = execution_context(ConversationStore::disabled(), response_store); + + let ctx = rehydrate_conversation(request(None, Some("resp_search")), &exec_ctx) + .await + .expect("orphan history remains a valid rehydrated public shape"); + let error = + crate::executor::prepare::prepare_request_tools(ctx, &exec_ctx.conv_handler, &exec_ctx.resp_handler) + .await + .expect_err("explicit preparation rejects orphan stored public history"); + + assert!( + matches!(error, ExecutorError::Tool(ToolError::Config(ref message)) if message.contains("orphan")), + "unexpected error: {error}" + ); + assert_eq!(error.http_status(), http::StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn stored_public_search_call_pairs_with_new_output_after_rehydration() { + let pool = create_pool_with_schema(Some("sqlite://?mode=memory")) + .await + .expect("create response store"); + let response_store = ResponseStore::new(pool); + let stored_call: crate::types::io::OutputItem = serde_json::from_value(serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_stored", + "call_id": "call_search_stored", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + })) + .expect("valid emitted public search call"); + let effective_tools = serde_json::from_value(serde_json::json!([ + { + "type": "tool_search", + "execution": "client", + "description": "Find a tool", + "parameters": {"type": "object"} + }, + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + } + ])) + .expect("valid effective public declarations"); + let metadata = ResponseMetadata { + effective_tools: Some(effective_tools), + ..ResponseMetadata::default() + }; + response_store + .persist( + "resp_stored_search", + None, + vec![InOutItem::Output(stored_call)], + &metadata, + ) + .await + .expect("persist public search call"); + let exec_ctx = execution_context(ConversationStore::disabled(), response_store); + let mut continuation = request(None, Some("resp_stored_search")); + continuation.input = serde_json::from_value(serde_json::json!([{ + "type": "tool_search_output", + "call_id": "call_search_stored", + "tools": [{ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }] + }])) + .expect("valid new public search output"); + + let ctx = rehydrate_conversation(continuation, &exec_ctx) + .await + .expect("stored public call rehydrates before new output"); + let (ctx, registry) = + crate::executor::prepare::prepare_request_tools(ctx, &exec_ctx.conv_handler, &exec_ctx.resp_handler) + .await + .expect("stored continuation derives valid tool-search state"); + + let state = registry + .tool_search_state() + .expect("valid state was prepared after rehydration"); + assert!(state.is_active()); + assert_eq!(state.loaded_public_tools().len(), 1); + assert!(matches!( + &state.loaded_public_tools()[0], + crate::types::tools::ResponsesTool::Function(function) if function.name.as_str() == "get_weather" + )); + let private_input = + serde_json::to_value(&ctx.enriched_request.input).expect("prepared private history serializes"); + assert_eq!(private_input[0]["call_id"], "call_search_stored"); + assert_eq!(private_input[1]["call_id"], "call_search_stored"); + } + #[tokio::test] async fn previous_response_rehydration_has_no_conversation_version() -> Result<(), Box> { let pool = create_pool_with_schema(Some("sqlite://?mode=memory")).await?; diff --git a/crates/agentic-server-core/src/executor/upstream.rs b/crates/agentic-server-core/src/executor/upstream.rs index 90a48c80..64e72bd2 100644 --- a/crates/agentic-server-core/src/executor/upstream.rs +++ b/crates/agentic-server-core/src/executor/upstream.rs @@ -36,20 +36,24 @@ pub(super) async fn fetch_blocking_payload( ctx: &RequestContext, exec_ctx: &ExecutionContext, auth: Option<&str>, + registry: &ToolRegistry, ) -> ExecutorResult { let url = exec_ctx.responses_url(); // Non-streaming request: stream=false -> full JSON body -> from_json. + registry.ensure_request_prepared(&ctx.enriched_request)?; let upstream_request = ctx.enriched_request.to_upstream_request(false)?; let upstream_json = serialize_to_string(&upstream_request).map_err(ExecutorError::JsonError)?; let body = fetch_response_json(upstream_json, &url, &exec_ctx.client, auth).await?; - + registry.validate_blocking_response(&body)?; let acc = ResponseAccumulator::from_json(&body, ctx.conversation_id.as_deref())?; let mut payload = acc.finalize( &ctx.enriched_request.model, ctx.original_request.previous_response_id.as_deref(), ctx.original_request.instructions.as_deref(), ); + let status = payload.status.parse().unwrap_or_default(); + registry.normalize_response_output(&mut payload.output, status, &std::collections::HashSet::new())?; ctx.inject_ids(&mut payload); Ok(payload) @@ -59,7 +63,7 @@ pub(super) async fn fetch_stream_payload( ctx: &RequestContext, exec_ctx: &ExecutionContext, auth: Option<&str>, - registry: &ToolRegistry, + registry: &mut ToolRegistry, mut stream: Option<( &mut GatewayStreamAccumulator, &tokio::sync::mpsc::UnboundedSender, @@ -67,6 +71,7 @@ pub(super) async fn fetch_stream_payload( output_offset: usize, ) -> ExecutorResult { let url = exec_ctx.responses_url(); + registry.ensure_request_prepared(&ctx.enriched_request)?; let upstream_request = ctx.enriched_request.to_upstream_request(true)?; let upstream_json = serialize_to_string(&upstream_request).map_err(ExecutorError::JsonError)?; let mut line_stream = Box::pin(call_inference( @@ -77,18 +82,12 @@ pub(super) async fn fetch_stream_payload( exec_ctx.streaming_timeout, )); let mut acc = ResponseAccumulator::new(ctx.response_id.clone(), ctx.conversation_id.clone()); - let mut function_sse = FunctionSseTranslator::new(registry.tool_type_map()); + let mut function_sse = FunctionSseTranslator::new(registry); let mut defer_from_output_index = None; let mut deferred_events = Vec::new(); let mut deferred_bytes = 0; while let Some(line_result) = line_stream.next().await { let line = line_result?; - if stream.is_none() { - if let Some(frame) = acc.process_sse_line(&line) { - log_upstream_failure(&frame, &ctx.response_id); - } - continue; - } if let Some(translation) = acc.process_sse_line_with_translator(&line, &mut function_sse)? { let previous_defer_from_output_index = defer_from_output_index; defer_from_output_index = translation.defer_from_output_index.map(u64::from); @@ -129,12 +128,19 @@ pub(super) async fn fetch_stream_payload( } } } + let function_sse_outcome = function_sse.finish()?; acc.finish_stream(); let mut payload = acc.finalize( &ctx.enriched_request.model, ctx.original_request.previous_response_id.as_deref(), ctx.original_request.instructions.as_deref(), ); + let status = payload.status.parse().unwrap_or_default(); + registry.normalize_response_output( + &mut payload.output, + status, + &function_sse_outcome.unfinished_tool_search_item_ids, + )?; ctx.inject_ids(&mut payload); Ok(StreamPayload { payload, @@ -202,6 +208,7 @@ fn should_defer_stream_event(frame: &EventFrame, defer_from_output_index: Option fn emit_stream_frame(frame: &mut EventFrame, emit_ctx: &mut StreamEmitContext<'_>) -> ExecutorResult { apply_context_response_ids(&mut frame.wire, emit_ctx.request); + emit_ctx.registry.restore_tool_search_response_tools(&mut frame.wire)?; emit_ctx.registry.restore_stream_event_wire(&mut frame.wire); let emitted = emit_ctx.accumulator.process_event(frame, emit_ctx.output_offset); if emitted { diff --git a/crates/agentic-server-core/src/lib.rs b/crates/agentic-server-core/src/lib.rs index 1981fb85..a64c73ba 100644 --- a/crates/agentic-server-core/src/lib.rs +++ b/crates/agentic-server-core/src/lib.rs @@ -16,7 +16,7 @@ pub use storage::{ }; pub use tool::{ CodexNamespaceHandler, FunctionHandler, GatewayExecutor, GatewayExecutorRegistration, McpServerEntry, ToolEntry, - ToolError, ToolHandler, ToolOutput, ToolRegistry, ToolType, WebSearchHandler, + ToolError, ToolHandler, ToolOutput, ToolRegistry, ToolSearchHandler, ToolType, WebSearchHandler, }; pub use types::{ AllowedTool, AllowedToolsMode, CodeInterpreterToolParam, CodexNamespaceMember, CodexNamespaceToolParam, diff --git a/crates/agentic-server-core/src/storage/conversation.rs b/crates/agentic-server-core/src/storage/conversation.rs index 4792dec2..43ef120e 100644 --- a/crates/agentic-server-core/src/storage/conversation.rs +++ b/crates/agentic-server-core/src/storage/conversation.rs @@ -105,6 +105,27 @@ impl ConversationStore { }) } + /// Loads metadata from the item-bearing persisted turn at a captured version. + /// + /// # Errors + /// + /// Returns an error if either targeted database lookup fails. + pub async fn response_metadata_at_version( + &self, + conversation_id: &str, + version: ConversationVersion, + ) -> StoreResult> { + let ConversationVersion::LastSequence(sequence) = version else { + return Ok(None); + }; + let pool = self.pool()?; + let Some(item_id) = item::get_id_by_conversation_sequence(pool, conversation_id, sequence).await? else { + return Ok(None); + }; + let response = response::get_conversation_turn_for_item(pool, conversation_id, &item_id).await?; + Ok(response.and_then(|row| row.metadata_as())) + } + /// Persists conversation turn with new items and response metadata. /// /// Creates items in the conversation and stores the associated response record. diff --git a/crates/agentic-server-core/src/storage/models/item.rs b/crates/agentic-server-core/src/storage/models/item.rs index e6cc81a8..e9fe4997 100644 --- a/crates/agentic-server-core/src/storage/models/item.rs +++ b/crates/agentic-server-core/src/storage/models/item.rs @@ -232,6 +232,22 @@ pub async fn get_items_by_conversation(pool: &DbPool, conversation_id: &str) -> .await } +/// Get a conversation item ID by its sequence number. +/// +/// # Errors +/// Returns `DbResult::Err` if the database query fails. +pub async fn get_id_by_conversation_sequence( + pool: &DbPool, + conversation_id: &str, + sequence: i64, +) -> DbResult> { + sqlx::query_scalar("SELECT id FROM items WHERE conversation_id = $1 AND seq = $2") + .bind(conversation_id) + .bind(sequence) + .fetch_optional(pool) + .await +} + /// Returns the last stored item sequence for a conversation inside a transaction. /// /// # Errors diff --git a/crates/agentic-server-core/src/storage/models/response.rs b/crates/agentic-server-core/src/storage/models/response.rs index a613b82d..83d759f0 100644 --- a/crates/agentic-server-core/src/storage/models/response.rs +++ b/crates/agentic-server-core/src/storage/models/response.rs @@ -67,6 +67,33 @@ pub async fn get(pool: &DbPool, id: &str) -> DbResult> { .await } +/// Get the conversation turn that persisted a specific item. +/// +/// Responses branched through `previous_response_id` retain the originating +/// conversation ID for response-chain rehydration, but are not conversation turns. +/// +/// # Errors +/// Returns `DbResult::Err` if the database query fails. +pub async fn get_conversation_turn_for_item( + pool: &DbPool, + conversation_id: &str, + item_id: &str, +) -> DbResult> { + let escaped_item_id = item_id.replace('!', "!!").replace('%', "!%").replace('_', "!_"); + let history_suffix = format!("%\"{escaped_item_id}\"]"); + sqlx::query_as::<_, Response>( + "SELECT * FROM responses \ + WHERE conversation_id = $1 \ + AND previous_response_id IS NULL \ + AND history_item_ids LIKE $2 ESCAPE '!' \ + LIMIT 1", + ) + .bind(conversation_id) + .bind(history_suffix) + .fetch_optional(pool) + .await +} + impl Response { /// Deserialize `history_item_ids` from JSON string to Vec. #[must_use] diff --git a/crates/agentic-server-core/src/storage/types/response.rs b/crates/agentic-server-core/src/storage/types/response.rs index 9572f398..b5db56c1 100644 --- a/crates/agentic-server-core/src/storage/types/response.rs +++ b/crates/agentic-server-core/src/storage/types/response.rs @@ -16,6 +16,11 @@ pub struct ResponseMetadata { pub model: String, pub previous_response_id: Option, pub effective_tools: Option>, + /// Public definitions whose deferred availability was resolved by tool search. + /// + /// This is separate from `effective_tools` so public `defer_loading` stays + /// unchanged while compaction may remove the call/output pair that loaded it. + pub tool_search_loaded_tools: Option>, pub effective_tool_choice: ToolChoice, pub effective_instructions: Option, } @@ -63,6 +68,11 @@ impl TryFrom<&ResponseMetadata> for String { tool.sanitize_for_persistence(); } } + if let Some(tools) = persisted.tool_search_loaded_tools.as_mut() { + for tool in tools { + tool.sanitize_for_persistence(); + } + } serialize_to_string(&persisted).map_err(StorageError::Serialization) } } @@ -117,6 +127,7 @@ mod tests { model: "gpt-4".to_string(), previous_response_id: Some("resp_1".to_string()), effective_tools: None, + tool_search_loaded_tools: None, effective_tool_choice: ToolChoice::Auto, effective_instructions: Some("be helpful".to_string()), }; @@ -155,6 +166,7 @@ mod tests { }); let metadata = ResponseMetadata { effective_tools: Some(vec![tool]), + tool_search_loaded_tools: None, ..ResponseMetadata::default() }; @@ -183,6 +195,7 @@ mod tests { assert_eq!(metadata.model, ""); assert!(metadata.previous_response_id.is_none()); assert!(metadata.effective_tools.is_none()); + assert!(metadata.tool_search_loaded_tools.is_none()); assert!(metadata.effective_instructions.is_none()); } diff --git a/crates/agentic-server-core/src/tool/codex.rs b/crates/agentic-server-core/src/tool/codex.rs index 5130b8a6..aca938df 100644 --- a/crates/agentic-server-core/src/tool/codex.rs +++ b/crates/agentic-server-core/src/tool/codex.rs @@ -1,9 +1,10 @@ +use std::borrow::Cow; use std::collections::HashMap; use serde_json::{Map, Value}; use crate::events::WireEvent; -use crate::types::io::{FunctionTool, FunctionToolCall, OutputItem, ToolChoice}; +use crate::types::io::{FunctionTool, FunctionToolCall, InputItem, OutputItem, ResponsesInput, ToolChoice}; use crate::types::tools::{CodexNamespaceMember, CodexNamespaceToolParam, NonEmptyToolName, ResponsesTool}; use crate::utils::common::serialize_to_value_or_custom_default; @@ -292,6 +293,31 @@ impl CodexNamespaceHandler { rewrite_tool_choice_with_map(tool_choice, map) } + /// Rewrite public namespaced function-call history to the exact flat names + /// from the request-scoped namespace map used for tool declarations. + #[must_use] + pub fn resolve_input<'a>( + &self, + map: Option<&NamespaceMap>, + input: Cow<'a, ResponsesInput>, + ) -> Cow<'a, ResponsesInput> { + let Some(map) = map else { + return input; + }; + let should_rewrite = matches!(&*input, ResponsesInput::Items(items) if items.iter().any(|item| { + matches!(item, InputItem::FunctionCall(call) + if call.namespace.as_deref().is_some_and(|namespace| { + map.mapping_for_member(namespace, &call.name).is_some() + })) + })); + if !should_rewrite { + return input; + } + let mut input = input.into_owned(); + rewrite_input_with_map(&mut input, map); + Cow::Owned(input) + } + pub fn restore_output_items(&self, output: &mut [OutputItem], map: Option<&NamespaceMap>) { let Some(map) = map else { return; @@ -364,6 +390,25 @@ fn namespace_map_from_tools(tools: Option<&[ResponsesTool]>) -> Result HashMap "web_search".to_owned(), ResponsesTool::FileSearch(_) => "file_search".to_owned(), ResponsesTool::CodeInterpreter(_) => "code_interpreter".to_owned(), - ResponsesTool::Mcp(_) + ResponsesTool::ToolSearch(_) + | ResponsesTool::Mcp(_) | ResponsesTool::Namespace(_) | ResponsesTool::Custom(_) | ResponsesTool::Unknown => return None, @@ -489,6 +535,10 @@ fn rewrite_tool_choice_with_map(choice: &ToolChoice, map: &NamespaceMap) -> Tool fn restore_response_value_with_map(value: &mut Value, map: &NamespaceMap) -> bool { let mut changed = false; + if let Some(object) = value.as_object_mut() { + changed |= restore_response_metadata_with_map(object, map); + } + if let Some(item) = value.as_object_mut().and_then(|object| object.get_mut("item")) { changed |= restore_call_value_with_map(item, map); } @@ -539,8 +589,35 @@ fn restore_call_value_with_map(value: &mut Value, map: &NamespaceMap) -> bool { true } +fn restore_response_metadata_with_map(object: &mut Map, map: &NamespaceMap) -> bool { + object + .get_mut("tool_choice") + .is_some_and(|choice| restore_tool_choice_with_map(choice, map)) +} + +fn restore_tool_choice_with_map(choice: &mut Value, map: &NamespaceMap) -> bool { + let Some(object) = choice.as_object_mut() else { + return false; + }; + if object.get("type").and_then(Value::as_str) != Some("function") + || object.get("namespace").and_then(Value::as_str).is_some() + { + return false; + } + let Some(mapping) = object + .get("name") + .and_then(Value::as_str) + .and_then(|name| map.mapping_for_call(name)) + else { + return false; + }; + object.insert("namespace".to_owned(), Value::String(mapping.member.namespace.clone())); + object.insert("name".to_owned(), Value::String(mapping.member.name.clone())); + true +} + fn restore_response_map_with_map(object: &mut Map, map: &NamespaceMap) -> bool { - let mut changed = false; + let mut changed = restore_response_metadata_with_map(object, map); if let Some(item) = object.get_mut("item") { changed |= restore_call_value_with_map(item, map); } @@ -929,4 +1006,37 @@ mod tests { assert_eq!(value["item"]["name"], "add_numbers"); assert_eq!(value["item"]["arguments"], "{\"numbers\":[8,0]}"); } + + #[test] + fn response_lifecycle_metadata_restores_public_namespace_tool_choice() { + let tools: Vec = serde_json::from_value(serde_json::json!([{ + "type": "namespace", + "name": "travel", + "tools": [{"type": "function", "name": "get_timezone"}] + }])) + .unwrap(); + let map = CodexNamespaceHandler + .build_namespace_map(Some(&tools)) + .expect("valid namespace map"); + let mut wire = WireEvent::new("response.created"); + wire.rest.insert( + "response".to_owned(), + serde_json::json!({ + "tool_choice": { + "type": "function", + "name": "agentic_ns__travel__get_timezone" + } + }), + ); + + assert!(CodexNamespaceHandler.restore_response_wire(&mut wire, map.as_ref())); + assert_eq!( + wire.rest["response"]["tool_choice"], + serde_json::json!({ + "type": "function", + "namespace": "travel", + "name": "get_timezone" + }) + ); + } } diff --git a/crates/agentic-server-core/src/tool/handler.rs b/crates/agentic-server-core/src/tool/handler.rs index 4ec6953f..99f99dfb 100644 --- a/crates/agentic-server-core/src/tool/handler.rs +++ b/crates/agentic-server-core/src/tool/handler.rs @@ -17,6 +17,10 @@ pub enum ToolError { Execution(String), #[error("invalid tool config: {0}")] Config(String), + #[error("upstream returned an invalid tool-search call")] + InvalidUpstreamToolSearch, + #[error("upstream returned a call for a function that has not been loaded")] + UpstreamWithheldFunctionCall, } /// Trait implemented by every tool type — client-owned and gateway-owned alike. @@ -45,8 +49,9 @@ pub trait ToolHandler: Send + Sync { /// Extension of [`ToolHandler`] for tool types that are executed by the gateway. /// /// Only gateway-owned tools (`Mcp`, `WebSearch`, `FileSearch`, `CodeInterpreter`) -/// implement this trait. Client-owned tools (`Function`) do not — the type system -/// makes it impossible to call `execute()` on them. +/// implement this trait. Client-owned tools (`Function`, `ToolSearch`, `Custom`, +/// `CodexNamespace`) do not — the type system makes it impossible to call +/// `execute()` on them. /// /// ## Note on `async fn` in traits /// diff --git a/crates/agentic-server-core/src/tool/mod.rs b/crates/agentic-server-core/src/tool/mod.rs index 1802d32b..703ff326 100644 --- a/crates/agentic-server-core/src/tool/mod.rs +++ b/crates/agentic-server-core/src/tool/mod.rs @@ -11,6 +11,7 @@ pub mod handler; pub mod mcp; pub mod normalize; pub mod registry; +pub mod tool_search; pub mod web_search; pub use codex::{CodexNamespaceHandler, NamespaceMap, model_visible_namespace_member_name}; @@ -20,4 +21,5 @@ pub use function::FunctionHandler; pub use handler::{GatewayExecutor, ToolError, ToolHandler, ToolOutput}; pub use mcp::{McpClient, McpClientPool, McpDiscoveredHandler, McpError, McpHandler, McpOperation, McpServerEntry}; pub use registry::{GatewayDispatchResult, ToolEntry, ToolRegistry, ToolType}; +pub use tool_search::{ToolSearchHandler, ToolSearchState}; pub use web_search::WebSearchHandler; diff --git a/crates/agentic-server-core/src/tool/normalize.rs b/crates/agentic-server-core/src/tool/normalize.rs index c76ae580..5b75d769 100644 --- a/crates/agentic-server-core/src/tool/normalize.rs +++ b/crates/agentic-server-core/src/tool/normalize.rs @@ -9,6 +9,7 @@ use super::function::FunctionHandler; use super::handler::{ToolError, ToolHandler, ToolOutput}; use super::mcp::McpHandler; use super::registry::ToolType; +use super::tool_search::ToolSearchHandler; use super::web_search::web_search_function_tool; impl ResponsesTool { @@ -34,6 +35,12 @@ impl ResponsesTool { |param| McpHandler::spec_from_param(¶m).validate(¶m), Err(ToolError::Config("MCP tool config serialization failed".to_owned())), ), + Self::ToolSearch(param) => serialize_to_value_or_custom_default( + param, + "tool_search config serialization failed", + |param| ToolSearchHandler.validate(¶m), + Err(ToolError::Config("tool_search config serialization failed".to_owned())), + ), Self::WebSearch(_) | Self::FileSearch(_) | Self::CodeInterpreter(_) | Self::Unknown => Ok(()), Self::Namespace(param) => serialize_to_value_or_custom_default( param, @@ -57,6 +64,7 @@ impl ResponsesTool { pub fn tool_type(&self) -> Option { match self { Self::Function(_) => Some(ToolType::Function), + Self::ToolSearch(_) => Some(ToolType::ToolSearch), Self::Mcp(_) => Some(ToolType::Mcp), Self::WebSearch(_) => Some(ToolType::WebSearch), Self::FileSearch(_) => Some(ToolType::FileSearch), @@ -76,6 +84,8 @@ impl ResponsesTool { /// /// - `Function` variants convert via [`From<&FunctionToolParam>`] for `FunctionTool`. /// Returns an empty list and logs at `debug` level if the name is empty. + /// - `ToolSearch` variants lower through [`ToolSearchHandler`] to the + /// synthetic client-executed function understood by vLLM. /// - `Mcp` variants convert gateway MCP built-ins to the function specs /// vLLM can call. /// - Unformatted `Custom` variants become function tools with one string @@ -97,6 +107,12 @@ impl ResponsesTool { |param| FunctionHandler.normalize(¶m).into_iter().take(1).collect(), vec![], ), + Self::ToolSearch(param) => serialize_to_value_or_custom_default( + param, + "tool_search config serialization failed", + |param| ToolSearchHandler.normalize(¶m).into_iter().take(1).collect(), + vec![], + ), Self::Mcp(p) => serialize_to_value_or_custom_default( p, "MCP tool config serialization failed", diff --git a/crates/agentic-server-core/src/tool/registry.rs b/crates/agentic-server-core/src/tool/registry.rs index 5f2be463..573a8932 100644 --- a/crates/agentic-server-core/src/tool/registry.rs +++ b/crates/agentic-server-core/src/tool/registry.rs @@ -1,5 +1,5 @@ -use std::collections::HashMap; use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use serde::{Deserialize, Serialize}; @@ -11,19 +11,25 @@ use super::executors::GatewayExecutors; use super::function::insert_function_entry; use super::mcp::handler::{McpToolMap, McpToolRef}; use super::mcp::registry::insert_discovered_mcp_entry; +use super::tool_search::{ + TOOL_SEARCH_NAME, ensure_request_prepared, insert_tool_search_entry, validate_blocking_response, +}; use super::web_search::insert_web_search_entry; -use super::{CodexNamespaceHandler, GatewayExecutor, McpHandler, NamespaceMap, ToolError, ToolOutput}; +use super::{CodexNamespaceHandler, GatewayExecutor, McpHandler, NamespaceMap, ToolError, ToolOutput, ToolSearchState}; use crate::events::WireEvent; +use crate::types::event::ResponseStatus; use crate::types::io::OutputItem; use crate::types::io::output::{FunctionToolCall, McpListTools}; +use crate::types::request_response::RequestPayload; use crate::types::tools::{CodeInterpreterToolParam, FileSearchToolParam, ResponsesTool}; -use crate::utils::common::serialize_to_value_or_custom_default; +use crate::utils::common::{serialize_to_value, serialize_to_value_or_custom_default}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ToolType { Function, + ToolSearch, Custom, CodexNamespace, Mcp, @@ -40,6 +46,7 @@ impl ToolType { pub(crate) const fn description(self) -> &'static str { match self { Self::Function => "function tool", + Self::ToolSearch => "tool search", Self::Custom => "custom tool", Self::CodexNamespace => "Codex namespace tool", Self::Mcp => "MCP tool", @@ -51,7 +58,10 @@ impl ToolType { #[must_use] pub const fn is_gateway_owned(self) -> bool { - !matches!(self, Self::Function | Self::Custom | Self::CodexNamespace) + !matches!( + self, + Self::Function | Self::ToolSearch | Self::Custom | Self::CodexNamespace + ) } } @@ -162,6 +172,9 @@ fn insert_code_interpreter_entry( pub struct ToolRegistry { entries: HashMap, + /// Prepared public/private tool-search projection for this request. + tool_search: Option>, + /// Built once from the declared tools, so final payload and streaming event /// restoration don't rebuild it on every call. namespace_map: Option, @@ -179,6 +192,13 @@ pub struct ToolRegistry { } impl ToolRegistry { + /// Whether a public request needs tool-search preparation and therefore + /// must use the executor rather than transparent upstream pass-through. + #[must_use] + pub fn request_has_tool_search_state(request: &RequestPayload) -> bool { + super::tool_search::request_has_tool_search_state(request) + } + /// Build a registry from declared tools and attach gateway handlers for dispatchable tool types. /// /// # Errors @@ -203,9 +223,12 @@ impl ToolRegistry { let mut entries = HashMap::with_capacity(tools.len()); let mut mcp_tool_map = McpToolMap::default(); let mut mcp_list_tools_items = Vec::new(); + // Validate every declaration before MCP discovery performs external I/O. + tools.iter().try_for_each(ResponsesTool::validate)?; + // Namespace members must be keyed by the same flat, model-visible name - // the model will call, so resolve them first — the same pure pass used - // to build the upstream request. + // the model will call. Discovered MCP names retain their separate + // post-list collision pass. let resolved_tools = CodexNamespaceHandler.resolve_namespace_members(tools)?; McpHandler::validate_server_labels(&resolved_tools)?; @@ -214,6 +237,11 @@ impl ToolRegistry { ResponsesTool::Function(p) => { insert_unique_tool_entries(&mut entries, |resolved| insert_function_entry(resolved, p))?; } + ResponsesTool::ToolSearch(param) => { + insert_unique_tool_entries(&mut entries, |resolved| { + insert_tool_search_entry(resolved, param); + })?; + } ResponsesTool::Mcp(p) => { let tool_set = match executors.mcp_server_tools(p).await { Ok(tool_set) => tool_set, @@ -268,6 +296,7 @@ impl ToolRegistry { Ok(Self { entries, + tool_search: None, namespace_map, custom_tool_map, mcp_tool_map, @@ -275,16 +304,188 @@ impl ToolRegistry { }) } + /// Prepare the request's private inference projection, build the normal + /// dispatch table, and retain the public tool-search projection in this + /// request-scoped registry. + pub(crate) fn prepare_request( + request: &mut RequestPayload, + restored_loaded_tools: &[ResponsesTool], + restore_only_declared: bool, + ) -> Result { + let state = super::ToolSearchHandler::prepare_request(request, restored_loaded_tools, restore_only_declared)?; + let mut registry = Self::default(); + registry.install_tool_search_state(state.map(Box::new), false)?; + Ok(registry) + } + + /// Build the normal dispatch table after generic request preparation has + /// had a chance to short-circuit (for example, explicit compaction). + pub(crate) async fn build_prepared_with_handlers( + mut self, + tools: Option<&mut Vec>, + executors: &mut GatewayExecutors, + ) -> Result { + let mut registry = match tools { + Some(tools) => Self::build_with_handlers(tools, executors).await?, + None => Self::default(), + }; + registry.install_tool_search_state(self.tool_search.take(), true)?; + Ok(registry) + } + + fn install_tool_search_state( + &mut self, + state: Option>, + validate_private_routes: bool, + ) -> Result<(), ToolError> { + if let Some(state) = state { + if validate_private_routes { + self.validate_tool_search_state(&state)?; + } + self.tool_search = Some(state); + } + Ok(()) + } + + /// Public declarations to expose in response metadata. `Some([])` is + /// intentionally distinct from an inactive request. + #[must_use] + pub(crate) fn tool_search_response_tools(&self) -> Option> { + let state = self.tool_search.as_deref().filter(|state| state.is_active())?; + let mut tools = state.public_response_tools(); + for tool in &mut tools { + tool.sanitize_for_persistence(); + } + Some(tools) + } + + /// Move the public tool projection into response persistence metadata. + pub(crate) fn take_tool_search_metadata(&mut self) -> Option<(Option>, Vec)> { + self.tool_search + .as_deref_mut() + .filter(|state| state.is_active()) + .map(ToolSearchState::take_public_metadata) + } + + pub(crate) fn validate_blocking_response(&self, body: &str) -> Result<(), ToolError> { + let empty = HashSet::new(); + let state = self.tool_search.as_deref(); + validate_blocking_response( + body, + state.is_some_and(ToolSearchState::is_active), + state.map_or(&empty, ToolSearchState::withheld_function_names), + ) + } + + /// Ensure tool-search requests went through the request-scoped preparation seam. + pub(crate) fn ensure_request_prepared(&self, request: &RequestPayload) -> Result<(), ToolError> { + ensure_request_prepared(request, self.tool_search.is_some()) + } + + pub(crate) fn normalize_response_output( + &self, + output: &mut Vec, + status: ResponseStatus, + unfinished_stream_item_ids: &HashSet, + ) -> Result<(), ToolError> { + let discard_unfinished = matches!(status, ResponseStatus::Error | ResponseStatus::Incomplete); + let mut normalized = Vec::with_capacity(output.len()); + for item in std::mem::take(output) { + match item { + OutputItem::FunctionCall(call) + if discard_unfinished && unfinished_stream_item_ids.contains(&call.id) => {} + OutputItem::FunctionCall(call) => { + super::tool_search::ensure_function_is_available(self.is_withheld_function(&call.name))?; + if self.tool_type(&call.name) == ToolType::ToolSearch { + if let Some(public) = super::tool_search::project_synthetic_call( + &call, + discard_unfinished, + unfinished_stream_item_ids.contains(&call.id), + )? { + normalized.push(OutputItem::ToolSearchCall(public)); + } + } else { + normalized.push(OutputItem::FunctionCall(call)); + } + } + OutputItem::ToolSearchCall(call) => { + if let Some(public) = super::tool_search::project_native_call(&call, discard_unfinished)? { + normalized.push(OutputItem::ToolSearchCall(public)); + } + } + item => normalized.push(item), + } + } + *output = normalized; + Ok(()) + } + + pub(crate) fn restore_tool_search_response_tools(&self, wire: &mut WireEvent) -> Result<(), ToolError> { + let Some(response) = wire.rest.get_mut("response").and_then(Value::as_object_mut) else { + return Ok(()); + }; + if !response.contains_key("tools") { + return Ok(()); + } + let Some(tools) = self.tool_search_response_tools() else { + return Ok(()); + }; + response.insert( + "tools".to_owned(), + serialize_to_value(&tools).map_err(|_| super::tool_search::invalid_upstream_search_call())?, + ); + Ok(()) + } + + #[cfg(test)] + pub(crate) fn tool_search_state(&self) -> Option<&ToolSearchState> { + self.tool_search.as_deref() + } + #[must_use] pub fn lookup(&self, tool_name: &str) -> Option<&ToolEntry> { self.entries.get(tool_name) } - pub(crate) fn tool_type_map(&self) -> HashMap { + pub(crate) fn tool_type(&self, name: &str) -> ToolType { + if name == TOOL_SEARCH_NAME && self.tool_search.as_deref().is_some_and(ToolSearchState::is_active) { + return ToolType::ToolSearch; + } self.entries - .iter() - .map(|(name, entry)| (name.clone(), entry.tool_type)) - .collect() + .get(name) + .map_or(ToolType::Function, |entry| entry.tool_type) + } + + pub(crate) fn is_withheld_function(&self, name: &str) -> bool { + self.tool_search + .as_deref() + .is_some_and(|state| state.withheld_function_names().contains(name)) + } + + pub(crate) fn tool_search_is_active(&self) -> bool { + self.tool_type(TOOL_SEARCH_NAME) == ToolType::ToolSearch + } + + #[cfg(test)] + pub(crate) fn from_tool_types(tool_types: HashMap) -> Self { + let entries = tool_types + .into_iter() + .map(|(name, tool_type)| { + ( + name, + ToolEntry { + tool_type, + config: Value::Null, + server_label: None, + handler: None, + }, + ) + }) + .collect(); + Self { + entries, + ..Self::default() + } } #[must_use] @@ -311,6 +512,34 @@ impl ToolRegistry { &self.mcp_list_tools_items } + /// Validate the private dispatch table against prepared tool-search state. + fn validate_tool_search_state(&self, state: &ToolSearchState) -> Result<(), ToolError> { + if !state.is_active() { + return Ok(()); + } + if self + .entries + .keys() + .any(|name| state.withheld_function_names().contains(name)) + { + return Err(ToolError::Config( + "a loaded tool collides with a withheld function name".to_owned(), + )); + } + let Some(_) = state.synthetic_tool_search() else { + return Ok(()); + }; + let entry = self.entries.get(TOOL_SEARCH_NAME).ok_or_else(|| { + ToolError::Config("prepared tool-search declaration is missing from the private registry".to_owned()) + })?; + if entry.tool_type != ToolType::ToolSearch || entry.tool_type.is_gateway_owned() || entry.handler.is_some() { + return Err(ToolError::Config( + "prepared tool-search declaration has invalid private registry ownership".to_owned(), + )); + } + Ok(()) + } + pub fn restore_final_payload_output(&self, output: &mut [OutputItem]) { CodexNamespaceHandler.restore_output_items(output, self.namespace_map.as_ref()); } @@ -373,8 +602,10 @@ mod tests { use super::*; use crate::tool::executors::GatewayExecutorRegistration; use crate::tool::mcp::{McpDiscoveredHandler, McpHandler}; + use crate::tool::tool_search; use crate::types::event::MessageStatus; use crate::types::tools::McpDiscoveredToolParam; + use crate::utils::common::serialize_to_value; fn declaration(server_label: &str) -> ResponsesTool { serde_json::from_value(serde_json::json!({ @@ -415,6 +646,221 @@ mod tests { } } + #[tokio::test] + async fn tool_search_declaration_has_client_owned_registry_entry() { + let mut tools: Vec = serde_json::from_value(serde_json::json!([{ + "type": "tool_search", + "execution": "client", + "description": "Find a tool", + "parameters": {"type": "object"} + }])) + .expect("tool-search declaration"); + let mut executors = GatewayExecutors::default(); + + let registry = ToolRegistry::build_with_handlers(&mut tools, &mut executors) + .await + .expect("client-owned declaration does not require a handler"); + + let entry = registry.lookup("tool_search").expect("tool-search entry"); + assert_eq!(entry.tool_type, ToolType::ToolSearch); + assert_eq!(entry.config["description"], "Find a tool"); + assert!(entry.server_label.is_none()); + assert!(entry.handler.is_none()); + assert_eq!(registry.len(), 1); + } + + #[tokio::test] + async fn synthetic_tool_search_is_client_owned_never_dispatched_and_converts_to_typed_output() { + let (request, mut state) = prepared_search_state(); + let mut tools = private_tools(&mut state, &request); + let mut executors = GatewayExecutors::default(); + let mut registry = ToolRegistry::build_with_handlers(&mut tools, &mut executors) + .await + .expect("typed tool-search declaration builds normally"); + + registry + .install_tool_search_state(Some(Box::new(state)), true) + .expect("prepared tool-search entry is already classified"); + let entry = registry.lookup("tool_search").expect("tool-search entry"); + assert_eq!(entry.tool_type, ToolType::ToolSearch); + assert!(!entry.tool_type.is_gateway_owned()); + + let call: FunctionToolCall = serde_json::from_value(serde_json::json!({ + "type": "function_call", + "id": "fc_search_1", + "call_id": "call_search_1", + "name": "tool_search", + "arguments": "{\"query\":\"weather\"}", + "status": "completed" + })) + .expect("normalized call"); + assert!( + registry.dispatch(&call).await.is_none(), + "tool search has no gateway handler" + ); + + let output = OutputItem::ToolSearchCall(tool_search::completed_public_call(&call).unwrap()); + assert_eq!( + serialize_to_value(&output).unwrap(), + serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_search_1", + "call_id": "call_search_1", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + }) + ); + } + + #[tokio::test] + async fn declaration_free_replay_enables_state_driven_classification() { + let (request, mut state) = replayed_search_state(); + assert!(state.is_active()); + assert!(state.synthetic_tool_search().is_none()); + let mut tools = private_tools(&mut state, &request); + let mut registry = ToolRegistry::build_with_handlers(&mut tools, &mut GatewayExecutors::default()) + .await + .expect("loaded function registry"); + assert!(registry.lookup("tool_search").is_none()); + registry + .install_tool_search_state(Some(Box::new(state)), true) + .expect("enable replay translation"); + + let valid: FunctionToolCall = serde_json::from_value(serde_json::json!({ + "type": "function_call", "id": "fc_search", "call_id": "call_search", + "name": "tool_search", "namespace": null, "arguments": "{\"query\":\"news\"}", + "status": "completed" + })) + .unwrap(); + assert_eq!(registry.tool_type("tool_search"), ToolType::ToolSearch); + assert!(tool_search::completed_public_call(&valid).is_ok()); + + let malformed: FunctionToolCall = serde_json::from_value(serde_json::json!({ + "type": "function_call", "id": "fc_search", "call_id": "call_search", + "name": "tool_search", "namespace": null, "arguments": "{}", "status": "in_progress" + })) + .unwrap(); + assert!(tool_search::completed_public_call(&malformed).is_err()); + } + + #[tokio::test] + async fn ordinary_function_named_tool_search_remains_a_function() { + let mut tools: Vec = serde_json::from_value(serde_json::json!([{ + "type": "function", + "name": "tool_search", + "parameters": {"type": "object"} + }])) + .unwrap(); + let registry = ToolRegistry::build_with_handlers(&mut tools, &mut GatewayExecutors::default()) + .await + .unwrap(); + assert_eq!(registry.tool_type("tool_search"), ToolType::Function); + } + + #[tokio::test] + async fn withheld_namespace_guard_is_exact_not_prefix_based() { + let request = serde_json::from_value(serde_json::json!({ + "model": "test", + "input": "find a tool", + "tools": [ + { + "type": "tool_search", "execution": "client", "description": "Find a tool", + "parameters": {"type": "object"} + }, + { + "type": "namespace", "name": "weather", "tools": [{ + "type": "function", "name": "forecast", "defer_loading": true + }] + }, + { + "type": "function", "name": "agentic_ns__weather__ordinary_prefix_like", + "parameters": {"type": "object"} + } + ], + "store": false, + "stream": false + })) + .expect("active namespace request"); + let mut state = ToolSearchState::build(&request).expect("prepared namespace state"); + let mut tools = private_tools(&mut state, &request); + let mut registry = ToolRegistry::build_with_handlers(&mut tools, &mut GatewayExecutors::default()) + .await + .expect("private registry"); + registry + .install_tool_search_state(Some(Box::new(state)), true) + .expect("state application"); + + assert!( + !registry + .tool_search_state() + .expect("tool-search state") + .withheld_function_names() + .contains("agentic_ns__weather__ordinary_prefix_like") + ); + assert!( + registry + .tool_search_state() + .expect("tool-search state") + .withheld_function_names() + .contains("agentic_ns__weather__forecast") + ); + } + + fn private_tools( + state: &mut ToolSearchState, + request: &crate::types::request_response::RequestPayload, + ) -> Vec { + let mut request = request.clone(); + state + .prepare_inference_request(&mut request) + .expect("prepared state materializes a private inference request"); + request.tools.expect("private tools") + } + + fn prepared_search_state() -> (crate::types::request_response::RequestPayload, ToolSearchState) { + let request: crate::types::request_response::RequestPayload = serde_json::from_value(serde_json::json!({ + "model": "test", + "input": "find a tool", + "tools": [{ + "type": "tool_search", + "execution": "client", + "description": "Search the client catalog", + "parameters": {"type": "object"} + }], + "store": false, + "stream": false + })) + .expect("public request"); + let state = ToolSearchState::build(&request).expect("prepared search state"); + (request, state) + } + + fn replayed_search_state() -> (crate::types::request_response::RequestPayload, ToolSearchState) { + let request = serde_json::from_value(serde_json::json!({ + "model": "test", + "input": [ + { + "type": "tool_search_call", "id": "tsc_1", "call_id": "call_1", + "execution": "client", "arguments": {"query": "weather"}, "status": "completed" + }, + { + "type": "tool_search_output", "call_id": "call_1", "execution": "client", + "status": "completed", "tools": [{ + "type": "function", "name": "get_weather", "parameters": {"type": "object"}, + "defer_loading": true + }] + } + ], + "tools": [], + "store": false, + "stream": false + })) + .expect("declaration-free replay request"); + let state = ToolSearchState::build(&request).expect("prepared replay state"); + (request, state) + } + fn mixed_tool_declarations() -> Vec { serde_json::from_value(serde_json::json!([ { diff --git a/crates/agentic-server-core/src/tool/tool_search.rs b/crates/agentic-server-core/src/tool/tool_search.rs new file mode 100644 index 00000000..79fa7945 --- /dev/null +++ b/crates/agentic-server-core/src/tool/tool_search.rs @@ -0,0 +1,2136 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt; + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +use crate::types::event::{MessageStatus, ResponseStatus}; +use crate::types::io::output::FunctionToolCall; +use crate::types::io::{ + FunctionTool, FunctionToolResultMessage, InputFunctionToolCall, InputItem, InputToolSearchCall, ResponsesInput, + ToolCallOutput, ToolChoice, ToolSearchCall, ToolSearchOutputMessage, +}; +use crate::types::request_response::RequestPayload; +use crate::types::tools::{ + CodexNamespaceMember, CodexNamespaceToolParam, FunctionToolParam, ResponsesTool, ToolSearchStatus, + ToolSearchToolParam, +}; +use crate::utils::common::{ + deserialize_from_str, deserialize_from_value, serialize_to_string, serialize_to_value, + serialize_to_value_or_custom_default, +}; + +use super::CodexNamespaceHandler; +use super::handler::{ToolError, ToolHandler}; +use super::registry::{ToolEntry, ToolType}; + +pub(crate) const TOOL_SEARCH_NAME: &str = "tool_search"; +const DEFAULT_DESCRIPTION: &str = "Search the client tool catalog"; +const DEFAULT_QUERY_DESCRIPTION: &str = "A concise description of the needed capabilities."; + +/// Handler for client-executed `type: "tool_search"` declarations. +/// +/// The declaration remains a first-class tool-search type throughout request +/// preparation and registry construction. This handler performs the one +/// provider-specific lowering step to the ordinary function shape understood +/// by upstreams without native tool-search support. +#[derive(Debug)] +pub struct ToolSearchHandler; + +impl ToolSearchHandler { + /// Prepare the private inference view from fully rehydrated public state. + /// + /// # Errors + /// + /// Returns [`ToolError::Config`] when the public tool-search history or + /// effective tool selection is invalid. + pub(crate) fn prepare_request( + request: &mut RequestPayload, + restored_loaded_tools: &[ResponsesTool], + restore_only_declared: bool, + ) -> Result, ToolError> { + let mut state = + ToolSearchState::build_with_loaded_tools(request, restored_loaded_tools, restore_only_declared)?; + if !state.is_active() { + return Ok(None); + } + state.prepare_inference_request(request)?; + Ok(Some(state)) + } + + #[must_use] + pub(crate) fn normalized_param(param: &ToolSearchToolParam) -> ToolSearchToolParam { + let mut normalized = param.clone(); + normalized.description = Some( + param + .description + .as_deref() + .filter(|description| !description.trim().is_empty()) + .unwrap_or(DEFAULT_DESCRIPTION) + .to_owned(), + ); + normalized.parameters = Some( + param + .parameters + .as_ref() + .filter(|parameters| parameters.get("type").and_then(Value::as_str) == Some("object")) + .cloned() + .unwrap_or_else(default_parameters), + ); + normalized + } + + #[must_use] + fn function_tool(param: &ToolSearchToolParam) -> FunctionTool { + let normalized = Self::normalized_param(param); + FunctionTool { + type_: "function".to_owned(), + name: TOOL_SEARCH_NAME.to_owned(), + description: normalized.description, + parameters: normalized.parameters.map(Value::Object), + strict: Some(true), + } + } +} + +impl ToolHandler for ToolSearchHandler { + fn tool_type(&self) -> ToolType { + ToolType::ToolSearch + } + + fn validate(&self, param: &Value) -> Result<(), ToolError> { + deserialize_from_value::(param.clone()) + .map(|_| ()) + .map_err(|error| ToolError::Config(format!("invalid tool_search declaration: {error}"))) + } + + fn normalize(&self, param: &Value) -> Vec { + match deserialize_from_value::(param.clone()) { + Ok(param) => vec![Self::function_tool(¶m)], + Err(error) => { + tracing::warn!(%error, "tool_search normalize called before validation"); + vec![] + } + } + } +} + +pub(crate) fn insert_tool_search_entry(entries: &mut HashMap, param: &ToolSearchToolParam) { + serialize_to_value_or_custom_default( + param, + "tool_search config serialization failed", + |config| { + if entries + .insert( + TOOL_SEARCH_NAME.to_owned(), + ToolEntry { + tool_type: ToolType::ToolSearch, + config, + server_label: None, + handler: None, + }, + ) + .is_some() + { + tracing::warn!( + name = TOOL_SEARCH_NAME, + "duplicate tool name — previous definition overwritten" + ); + } + }, + (), + ); +} + +fn default_parameters() -> Map { + let query = Map::from_iter([ + ("type".to_owned(), Value::String("string".to_owned())), + ( + "description".to_owned(), + Value::String(DEFAULT_QUERY_DESCRIPTION.to_owned()), + ), + ]); + let properties = Map::from_iter([("query".to_owned(), Value::Object(query))]); + Map::from_iter([ + ("type".to_owned(), Value::String("object".to_owned())), + ("properties".to_owned(), Value::Object(properties)), + ( + "required".to_owned(), + Value::Array(vec![Value::String("query".to_owned())]), + ), + ("additionalProperties".to_owned(), Value::Bool(false)), + ]) +} + +/// Stable public identity used to compare definitions accumulated from search outputs. +/// +/// Equality remains type-aware while the state builder also indexes the visible name +/// separately, so returning the same name under a different supported kind is rejected. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +enum LoadedToolIdentity { + Function(String), + Namespace(String), +} + +impl LoadedToolIdentity { + fn name(&self) -> &str { + match self { + Self::Function(name) | Self::Namespace(name) => name, + } + } + + const fn kind(&self) -> &'static str { + match self { + Self::Function(_) => "function", + Self::Namespace(_) => "namespace", + } + } +} + +struct DefinitionRecord { + identity: LoadedToolIdentity, + canonical: Value, + public_index: usize, + loaded: bool, + namespace_members: Option, +} + +struct NamespaceMemberRecord { + canonical: Value, + public_member_index: usize, + loaded: bool, +} + +struct NamespaceMemberRecords { + ordered: Vec, + indexes: HashMap, + unloaded_count: usize, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ToolSearchActivity { + Inactive, + Active, +} + +struct PendingSearchCall { + call_id: String, +} + +struct DefinitionAccumulator<'a> { + public_tools: &'a mut Vec, + definitions: &'a mut Vec, + definition_indexes: &'a mut HashMap, + loaded_public_tools: &'a mut Vec, + withheld_function_names: &'a mut HashSet, + prior_unknown_namespace_calls: HashMap>, + unqualified_call_positions: HashMap, + current_history_position: Option, +} + +struct DefinitionViews<'a> { + public_tools: &'a mut Vec, + definitions: &'a mut Vec, + definition_indexes: &'a mut HashMap, + loaded_public_tools: &'a mut Vec, + withheld_function_names: &'a mut HashSet, +} + +#[derive(Serialize)] +#[serde(untagged)] +enum CatalogEntry { + Function { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, + Namespace { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, +} + +impl CatalogEntry { + fn display_name(&self) -> &str { + match self { + Self::Function { name, .. } | Self::Namespace { name, .. } => name, + } + } + + fn description(&self) -> Option<&str> { + match self { + Self::Function { description, .. } | Self::Namespace { description, .. } => description.as_deref(), + } + } +} + +/// Pure, request-scoped state derived from fully rehydrated public history. +/// +/// The state deliberately has no `Serialize` implementation and its `Debug` +/// output contains counts only. +pub struct ToolSearchState { + activity: ToolSearchActivity, + has_completed_search: bool, + public_effective_tools: Option>, + private_upstream_tools: Option>, + private_upstream_input: Option, + loaded_public_tools: Vec, + synthetic_tool_search: Option, + withheld_function_names: HashSet, + unqualified_call_positions: HashMap, +} + +impl fmt::Debug for ToolSearchState { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ToolSearchState") + .field("activity", &self.activity) + .field("active", &self.is_active()) + .field("has_completed_search", &self.has_completed_search) + .field( + "public_effective_tool_count", + &self.public_effective_tools.as_ref().map_or(0, Vec::len), + ) + .field( + "private_upstream_tool_count", + &self.private_upstream_tools.as_ref().map_or(0, Vec::len), + ) + .field("loaded_public_tool_count", &self.loaded_public_tools.len()) + .field("has_private_upstream_input", &self.private_upstream_input.is_some()) + .field("has_synthetic_tool_search", &self.synthetic_tool_search.is_some()) + .field("withheld_function_count", &self.withheld_function_names.len()) + .field("unqualified_history_call_count", &self.unqualified_call_positions.len()) + .finish() + } +} + +impl Default for ToolSearchState { + fn default() -> Self { + Self { + activity: ToolSearchActivity::Inactive, + has_completed_search: false, + public_effective_tools: None, + private_upstream_tools: None, + private_upstream_input: None, + loaded_public_tools: Vec::new(), + synthetic_tool_search: None, + withheld_function_names: HashSet::new(), + unqualified_call_positions: HashMap::new(), + } + } +} + +impl ToolSearchState { + /// Build deterministic public/private views from ordered public history. + /// + /// This function performs no network, storage, clock, random-ID, or + /// transport work. It runs in linear time in input items and definitions; + /// vectors retain declaration/history order and maps are lookup-only. + /// + /// # Errors + /// + /// Returns [`ToolError::Config`] for an invalid public declaration, + /// call/output ordering or linkage error, duplicate/conflicting definition, + /// or normalized-name collision. + pub fn build(request: &RequestPayload) -> Result { + Self::build_with_loaded_tools(request, &[], false) + } + + /// Build state with public loaded definitions restored from typed response + /// metadata when compaction has removed the original search pair. + /// + /// The restored definitions pass through the same validation and loading + /// logic as definitions in a public `tool_search_output`. + /// + /// # Errors + /// + /// Returns [`ToolError::Config`] under the same conditions as [`Self::build`]. + pub fn build_with_loaded_tools( + request: &RequestPayload, + restored_loaded_tools: &[ResponsesTool], + restore_only_declared: bool, + ) -> Result { + let active_input = request.input.model_input(); + if !validate_tool_search_request(request, active_input.as_ref())? { + return Ok(Self::default()); + } + + let input_items = match active_input.as_ref() { + ResponsesInput::Text(_) => &[][..], + ResponsesInput::Items(items) => items.as_slice(), + }; + let has_search_history = input_items + .iter() + .any(|item| matches!(item, InputItem::ToolSearchCall(_) | InputItem::ToolSearchOutput(_))); + let has_completed_search = !restored_loaded_tools.is_empty() + || input_items + .iter() + .any(|item| matches!(item, InputItem::ToolSearchOutput(_))); + let declaration = request + .tools + .as_deref() + .unwrap_or_default() + .iter() + .find_map(|tool| match tool { + ResponsesTool::ToolSearch(declaration) => Some(declaration), + _ => None, + }); + if declaration.is_none() && !has_search_history { + return Err(ToolError::Config( + "defer_loading requires a tool_search declaration or replayed tool-search history".to_owned(), + )); + } + + let tools_were_present = request.tools.is_some(); + let mut public_tools = request.tools.clone().unwrap_or_default(); + let mut definitions = Vec::with_capacity(public_tools.len()); + let mut definition_indexes = HashMap::with_capacity(public_tools.len()); + index_initial_definitions(&public_tools, &mut definitions, &mut definition_indexes)?; + let mut withheld_function_names = + initial_withheld_function_names(&public_tools, &definitions, &definition_indexes)?; + let mut unqualified_call_positions = HashMap::new(); + + let mut loaded_public_tools = Vec::new(); + restore_loaded_definitions( + restored_loaded_tools, + DefinitionViews { + public_tools: &mut public_tools, + definitions: &mut definitions, + definition_indexes: &mut definition_indexes, + loaded_public_tools: &mut loaded_public_tools, + withheld_function_names: &mut withheld_function_names, + }, + restore_only_declared, + )?; + let private_upstream_input = prepare_history( + active_input.as_ref(), + DefinitionViews { + public_tools: &mut public_tools, + definitions: &mut definitions, + definition_indexes: &mut definition_indexes, + loaded_public_tools: &mut loaded_public_tools, + withheld_function_names: &mut withheld_function_names, + }, + &mut unqualified_call_positions, + )?; + + CodexNamespaceHandler.validate_namespace_collisions(Some(&public_tools))?; + + let catalog = build_catalog(&public_tools, &definitions, &definition_indexes); + let synthetic_tool_search = declaration.map(|declaration| synthetic_tool_search(declaration, &catalog)); + let private_tools = build_private_tools( + &public_tools, + &definitions, + &definition_indexes, + synthetic_tool_search.as_ref(), + ); + let public_effective_tools = (tools_were_present || !public_tools.is_empty()).then_some(public_tools); + let private_upstream_tools = (tools_were_present || !private_tools.is_empty()).then_some(private_tools); + + Ok(Self { + activity: ToolSearchActivity::Active, + has_completed_search, + public_effective_tools, + private_upstream_tools, + private_upstream_input: Some(private_upstream_input), + loaded_public_tools, + synthetic_tool_search, + withheld_function_names, + unqualified_call_positions, + }) + } + + #[must_use] + pub const fn is_active(&self) -> bool { + matches!(self.activity, ToolSearchActivity::Active) + } + + #[must_use] + pub fn public_effective_tools(&self) -> Option<&[ResponsesTool]> { + self.public_effective_tools.as_deref() + } + + /// Public declarations available for selection in response metadata. + /// Before search completes, the response echoes the declared catalog. Once + /// search resolves availability, it exposes only initially available and + /// loaded definitions while preserving their public namespace shape. + #[must_use] + pub(crate) fn public_response_tools(&self) -> Vec { + let public_tools = self.public_effective_tools.as_deref().unwrap_or_default(); + if !self.has_completed_search { + return public_tools.to_vec(); + } + available_public_tools(public_tools, &self.loaded_public_tools) + } + + /// Public definitions resolved by completed search outputs, in first-load order. + /// + /// This remains separate from `public_effective_tools`: an initially + /// deferred definition stays deferred publicly even after becoming loaded. + #[must_use] + pub fn loaded_public_tools(&self) -> &[ResponsesTool] { + &self.loaded_public_tools + } + + /// Private tool-search declaration used by request-scoped registry and upstream normalization. + #[must_use] + pub const fn synthetic_tool_search(&self) -> Option<&ToolSearchToolParam> { + self.synthetic_tool_search.as_ref() + } + + #[must_use] + pub(crate) fn withheld_function_names(&self) -> &HashSet { + &self.withheld_function_names + } + + /// Replace the request's public tool-search views with the prepared private + /// input and tools used for inference. The retained state then contains + /// only public metadata needed after inference. + /// + /// # Errors + /// + /// Returns [`ToolError::Config`] when the effective tool choice conflicts + /// with the prepared private tool set. + pub fn prepare_inference_request(&mut self, request: &mut RequestPayload) -> Result<(), ToolError> { + validate_effective_tool_choice(request.tool_choice.as_ref(), &self.withheld_function_names)?; + let input = self.private_upstream_input.take().ok_or_else(|| { + ToolError::Config("tool-search private inference input has already been consumed".to_owned()) + })?; + request.input = input; + request.tools = self.private_upstream_tools.take(); + Ok(()) + } + + pub(crate) fn take_public_metadata(&mut self) -> (Option>, Vec) { + ( + self.public_effective_tools.take(), + std::mem::take(&mut self.loaded_public_tools), + ) + } +} + +fn validate_tool_search_request(request: &RequestPayload, input: &ResponsesInput) -> Result { + if !request_contains_tool_search_state(request, input) { + return Ok(false); + } + + let tools = request.tools.as_deref().unwrap_or_default(); + if tools + .iter() + .filter(|tool| matches!(tool, ResponsesTool::ToolSearch(_))) + .count() + > 1 + { + return Err(ToolError::Config( + "tool search accepts at most one tool_search declaration".to_owned(), + )); + } + if request.parallel_tool_calls == Some(true) { + return Err(ToolError::Config( + "parallel_tool_calls must be false when tool search is active".to_owned(), + )); + } + + for tool in tools { + tool.validate()?; + if has_reserved_tool_search_name(tool) { + return Err(ToolError::Config( + "model-visible tool name 'tool_search' is reserved while tool search is active".to_owned(), + )); + } + } + + Ok(true) +} + +fn request_contains_tool_search_state(request: &RequestPayload, input: &ResponsesInput) -> bool { + input_contains_tool_search_state(input) + || request + .tools + .as_deref() + .is_some_and(|tools| tools.iter().any(tool_activates_tool_search)) +} + +pub(crate) fn request_has_tool_search_state(request: &RequestPayload) -> bool { + request_contains_tool_search_state(request, &request.input) +} + +fn input_contains_tool_search_state(input: &ResponsesInput) -> bool { + matches!( + input, + ResponsesInput::Items(items) + if items + .iter() + .any(|item| matches!(item, InputItem::ToolSearchCall(_) | InputItem::ToolSearchOutput(_))) + ) +} + +fn tool_activates_tool_search(tool: &ResponsesTool) -> bool { + matches!(tool, ResponsesTool::ToolSearch(_)) || tool_has_deferred_definition(tool) +} + +fn tool_has_deferred_definition(tool: &ResponsesTool) -> bool { + match tool { + ResponsesTool::Function(function) => function.defer_loading == Some(true), + ResponsesTool::Namespace(namespace) => namespace.tools.iter().any( + |member| matches!(member, CodexNamespaceMember::Function(function) if function.defer_loading == Some(true)), + ), + ResponsesTool::ToolSearch(_) + | ResponsesTool::Mcp(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Custom(_) + | ResponsesTool::Unknown => false, + } +} + +pub(crate) fn ensure_request_prepared(request: &RequestPayload, prepared: bool) -> Result<(), ToolError> { + if request_has_tool_search_state(request) && !prepared { + return Err(ToolError::Config( + "tool_search requests require prepared request-scoped state before upstream conversion".to_owned(), + )); + } + Ok(()) +} + +fn has_reserved_tool_search_name(tool: &ResponsesTool) -> bool { + match tool { + ResponsesTool::Function(function) => function.name.as_str() == TOOL_SEARCH_NAME, + ResponsesTool::Custom(custom) => custom.name.as_str() == TOOL_SEARCH_NAME, + ResponsesTool::Namespace(namespace) => namespace.name == TOOL_SEARCH_NAME, + ResponsesTool::ToolSearch(_) + | ResponsesTool::Mcp(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Unknown => false, + } +} + +fn validate_effective_tool_choice( + tool_choice: Option<&ToolChoice>, + withheld_function_names: &HashSet, +) -> Result<(), ToolError> { + let targets_withheld = match tool_choice { + Some(ToolChoice::Function { namespace, name }) => { + let model_name = namespace.as_deref().map_or_else( + || name.as_str().to_owned(), + |namespace| super::model_visible_namespace_member_name(namespace, name.as_str()), + ); + withheld_function_names.contains(&model_name) + } + Some(ToolChoice::AllowedTools { tools, .. }) => tools + .iter() + .any(|tool| tool.type_.as_str() == "function" && withheld_function_names.contains(tool.name.as_str())), + _ => false, + }; + if targets_withheld { + return Err(ToolError::Config( + "tool_choice targets a function before its definition is loaded".to_owned(), + )); + } + Ok(()) +} + +fn restore_loaded_definitions( + restored_loaded_tools: &[ResponsesTool], + views: DefinitionViews<'_>, + restore_only_declared: bool, +) -> Result<(), ToolError> { + let DefinitionViews { + public_tools, + definitions, + definition_indexes, + loaded_public_tools, + withheld_function_names, + } = views; + let mut accumulator = DefinitionAccumulator { + public_tools, + definitions, + definition_indexes, + loaded_public_tools, + withheld_function_names, + prior_unknown_namespace_calls: HashMap::new(), + unqualified_call_positions: HashMap::new(), + current_history_position: None, + }; + for tool in restored_loaded_tools { + let Some(tool) = restored_definition_for_load(tool, accumulator.definition_indexes, restore_only_declared)? + else { + continue; + }; + load_definition(&tool, &mut accumulator)?; + } + Ok(()) +} + +fn restored_definition_for_load( + restored: &ResponsesTool, + definition_indexes: &HashMap, + restore_only_declared: bool, +) -> Result, ToolError> { + let identity = loaded_tool_identity(restored)?.ok_or_else(|| { + ToolError::Config("stored tool-search availability contains an unsupported definition".to_owned()) + })?; + if restore_only_declared && !definition_indexes.contains_key(identity.name()) { + return Ok(None); + } + Ok(Some(restored.clone())) +} + +pub(crate) fn public_item_id(item_id: &str) -> String { + if item_id.strip_prefix("tsc_").is_some_and(|suffix| !suffix.is_empty()) { + return item_id.to_owned(); + } + if let Some(suffix) = item_id.strip_prefix("fc_").filter(|suffix| !suffix.is_empty()) { + return format!("tsc_{suffix}"); + } + let domain_separated = format!("tool_search_item:{item_id}"); + format!("tsc_{:016x}", stable_hash(&domain_separated)) +} + +fn stable_hash(value: &str) -> u64 { + value.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| { + (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3) + }) +} + +pub(crate) fn invalid_upstream_search_call() -> ToolError { + ToolError::InvalidUpstreamToolSearch +} + +pub(crate) fn invalid_upstream_withheld_function_call() -> ToolError { + ToolError::UpstreamWithheldFunctionCall +} + +pub(crate) fn started_public_call(call: &FunctionToolCall) -> Result { + if call.id.trim().is_empty() + || call.call_id.trim().is_empty() + || call.name != TOOL_SEARCH_NAME + || call.namespace.is_some() + { + return Err(invalid_upstream_search_call()); + } + Ok(ToolSearchCall { + id: public_item_id(&call.id), + call_id: call.call_id.clone(), + execution: crate::types::tools::ToolSearchExecution::Client, + arguments: Map::new(), + status: ToolSearchStatus::InProgress, + }) +} + +pub(crate) fn completed_public_call(call: &FunctionToolCall) -> Result { + if call.status != MessageStatus::Completed { + return Err(invalid_upstream_search_call()); + } + let mut public = started_public_call(call)?; + public.arguments = json_object(&call.arguments)?; + public.status = ToolSearchStatus::Completed; + Ok(public) +} + +pub(crate) fn project_synthetic_call( + call: &FunctionToolCall, + discard_incomplete: bool, + unfinished_stream_call: bool, +) -> Result, ToolError> { + if unfinished_stream_call || call.status != MessageStatus::Completed { + return if discard_incomplete { + Ok(None) + } else { + Err(invalid_upstream_search_call()) + }; + } + completed_public_call(call).map(Some) +} + +pub(crate) fn project_native_call( + call: &ToolSearchCall, + discard_incomplete: bool, +) -> Result, ToolError> { + if call.status == ToolSearchStatus::Completed { + return Ok(Some(call.clone())); + } + if discard_incomplete { + return Ok(None); + } + Err(invalid_upstream_search_call()) +} + +pub(crate) fn ensure_function_is_available(is_withheld: bool) -> Result<(), ToolError> { + if is_withheld { + return Err(invalid_upstream_withheld_function_call()); + } + Ok(()) +} + +pub(crate) fn validate_public_arguments(arguments: &str) -> Result<(), ToolError> { + json_object(arguments).map(|_| ()) +} + +pub(crate) fn strict_started_function(item: &Value) -> Result { + let function = strict_function_call(item)?; + if function.status != MessageStatus::InProgress || !function.arguments.is_empty() { + return Err(invalid_upstream_search_call()); + } + Ok(function) +} + +#[derive(Debug, Deserialize)] +struct StrictFunctionToolCall { + id: String, + call_id: String, + name: String, + #[serde(default)] + namespace: Option, + arguments: String, + status: MessageStatus, +} + +pub(crate) fn strict_function_call(item: &Value) -> Result { + let call: StrictFunctionToolCall = + deserialize_from_value(item.clone()).map_err(|_| invalid_upstream_search_call())?; + if call.namespace.is_some() { + return Err(invalid_upstream_search_call()); + } + let call = FunctionToolCall { + id: call.id, + call_id: call.call_id, + name: call.name, + namespace: None, + arguments: call.arguments, + status: call.status, + }; + started_public_call(&call)?; + if call.status == MessageStatus::Completed { + json_object(&call.arguments)?; + } + Ok(call) +} + +pub(crate) fn strict_native_call(item: Value) -> Result { + if item.get("namespace").is_some_and(|namespace| !namespace.is_null()) { + return Err(invalid_upstream_search_call()); + } + deserialize_from_value(item).map_err(|_| invalid_upstream_search_call()) +} + +fn json_object(arguments: &str) -> Result, ToolError> { + deserialize_from_str(arguments).map_err(|_| invalid_upstream_search_call()) +} + +pub(crate) fn validate_blocking_response( + body: &str, + tool_search_enabled: bool, + withheld_function_names: &HashSet, +) -> Result<(), ToolError> { + if !tool_search_enabled && withheld_function_names.is_empty() && !might_contain_tool_search_wire(body) { + return Ok(()); + } + let value: Value = match deserialize_from_str(body) { + Ok(value) => value, + Err(_) => return Ok(()), + }; + let status = value + .get("status") + .and_then(Value::as_str) + .map_or(ResponseStatus::Completed, |status| status.parse().unwrap_or_default()); + let discard_unfinished = matches!(status, ResponseStatus::Error | ResponseStatus::Incomplete); + for item in value.get("output").and_then(Value::as_array).into_iter().flatten() { + if item.get("type").and_then(Value::as_str) == Some("function_call") + && item + .get("name") + .and_then(Value::as_str) + .is_some_and(|name| withheld_function_names.contains(name)) + { + return Err(invalid_upstream_withheld_function_call()); + } + match item.get("type").and_then(Value::as_str) { + Some("tool_search_call") => { + let call = strict_native_call(item.clone())?; + if !discard_unfinished && call.status != ToolSearchStatus::Completed { + return Err(invalid_upstream_search_call()); + } + } + Some("function_call") + if tool_search_enabled && item.get("name").and_then(Value::as_str) == Some(TOOL_SEARCH_NAME) => + { + let call = strict_function_call(item)?; + if !discard_unfinished && call.status != MessageStatus::Completed { + return Err(invalid_upstream_search_call()); + } + } + _ => {} + } + } + Ok(()) +} + +fn might_contain_tool_search_wire(wire: &str) -> bool { + wire.contains(TOOL_SEARCH_NAME) +} + +fn index_initial_definitions( + tools: &[ResponsesTool], + definitions: &mut Vec, + definition_indexes: &mut HashMap, +) -> Result<(), ToolError> { + for (public_index, tool) in tools.iter().enumerate() { + let Some(identity) = loaded_tool_identity(tool)? else { + continue; + }; + if definition_indexes.contains_key(identity.name()) { + return Err(ToolError::Config(format!( + "duplicate tool-search definition identity '{}'", + identity.name() + ))); + } + let index = definitions.len(); + definition_indexes.insert(identity.name().to_owned(), index); + definitions.push(definition_record(tool, identity, public_index, false)?); + } + Ok(()) +} + +fn definition_record( + tool: &ResponsesTool, + identity: LoadedToolIdentity, + public_index: usize, + dynamically_loaded: bool, +) -> Result { + let namespace_members = match tool { + ResponsesTool::Namespace(namespace) => Some(namespace_member_records(namespace, dynamically_loaded)?), + ResponsesTool::Function(_) => None, + ResponsesTool::ToolSearch(_) + | ResponsesTool::Mcp(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Custom(_) + | ResponsesTool::Unknown => { + return Err(ToolError::Config( + "tool-search definition record received an unsupported tool".to_owned(), + )); + } + }; + let loaded = namespace_members + .as_ref() + .map_or(dynamically_loaded, |members| members.unloaded_count == 0); + Ok(DefinitionRecord { + identity, + canonical: canonical_definition(tool)?, + public_index, + loaded, + namespace_members, + }) +} + +fn namespace_member_records( + namespace: &CodexNamespaceToolParam, + dynamically_loaded: bool, +) -> Result { + if namespace.tools.is_empty() { + return Err(ToolError::Config( + "tool-search namespaces must contain at least one function member".to_owned(), + )); + } + let mut ordered = Vec::with_capacity(namespace.tools.len()); + let mut indexes = HashMap::with_capacity(namespace.tools.len()); + let mut unloaded_count = 0; + for (public_member_index, member) in namespace.tools.iter().enumerate() { + let CodexNamespaceMember::Function(function) = member else { + return Err(ToolError::Config( + "tool-search namespaces may contain only function members".to_owned(), + )); + }; + let name = function.name.as_str(); + if indexes.insert(name.to_owned(), ordered.len()).is_some() { + return Err(ToolError::Config(format!( + "duplicate namespace member identity '{}.{name}'", + namespace.name + ))); + } + let loaded = dynamically_loaded || function.defer_loading != Some(true); + unloaded_count += usize::from(!loaded); + ordered.push(NamespaceMemberRecord { + canonical: canonical_namespace_member(function)?, + public_member_index, + loaded, + }); + } + Ok(NamespaceMemberRecords { + ordered, + indexes, + unloaded_count, + }) +} + +fn initial_withheld_function_names( + public_tools: &[ResponsesTool], + definitions: &[DefinitionRecord], + definition_indexes: &HashMap, +) -> Result, ToolError> { + let mut withheld = HashSet::new(); + for tool in public_tools { + if let ResponsesTool::Function(function) = tool { + if function.defer_loading == Some(true) { + withheld.insert(function.name.as_str().to_owned()); + } + continue; + } + let ResponsesTool::Namespace(namespace) = tool else { + continue; + }; + let record = definition_indexes + .get(&namespace.name) + .and_then(|index| definitions.get(*index)) + .ok_or_else(|| ToolError::Config("namespace availability state is inconsistent".to_owned()))?; + let members = record + .namespace_members + .as_ref() + .ok_or_else(|| ToolError::Config("namespace availability state is inconsistent".to_owned()))?; + for member in &namespace.tools { + let CodexNamespaceMember::Function(function) = member else { + continue; + }; + let member_index = members + .indexes + .get(function.name.as_str()) + .ok_or_else(|| ToolError::Config("namespace availability state is inconsistent".to_owned()))?; + let is_withheld = !members.ordered[*member_index].loaded; + if is_withheld { + withheld.insert(super::model_visible_namespace_member_name( + &namespace.name, + function.name.as_str(), + )); + } + } + } + Ok(withheld) +} + +#[derive(Serialize)] +struct CanonicalToolSearchOutput<'a> { + tools: &'a [ModelVisibleLoadedTool<'a>], +} + +/// Typed model-output projection, deliberately separate from the raw +/// credential-sensitive definition retained for equality and later execution. +#[derive(Serialize)] +#[serde(untagged)] +enum ModelVisibleLoadedTool<'a> { + Function(ModelVisibleFunction<'a>), + Namespace(ModelVisibleNamespace<'a>), +} + +#[derive(Serialize)] +struct ModelVisibleFunction<'a> { + #[serde(rename = "type")] + type_: &'static str, + #[serde(flatten)] + definition: &'a FunctionToolParam, +} + +#[derive(Serialize)] +struct ModelVisibleNamespace<'a> { + #[serde(rename = "type")] + type_: &'static str, + name: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option<&'a str>, +} + +fn prepare_history( + input: &ResponsesInput, + views: DefinitionViews<'_>, + unqualified_call_positions: &mut HashMap, +) -> Result { + let ResponsesInput::Items(items) = input else { + return Ok(input.clone()); + }; + let mut private_items = Vec::with_capacity(items.len()); + let mut unresolved_call: Option = None; + let mut completed_call_ids = HashSet::new(); + let mut item_ids = HashSet::new(); + let DefinitionViews { + public_tools, + definitions, + definition_indexes, + loaded_public_tools, + withheld_function_names, + } = views; + let mut definition_accumulator = DefinitionAccumulator { + public_tools, + definitions, + definition_indexes, + loaded_public_tools, + withheld_function_names, + prior_unknown_namespace_calls: HashMap::new(), + unqualified_call_positions: std::mem::take(unqualified_call_positions), + current_history_position: None, + }; + + for (position, item) in items.iter().enumerate() { + definition_accumulator.current_history_position = Some(position); + match item { + InputItem::ToolSearchCall(call) => { + private_items.push(prepare_search_call( + call, + &mut unresolved_call, + &completed_call_ids, + &mut item_ids, + )?); + } + InputItem::ToolSearchOutput(output) => { + private_items.push(prepare_search_output( + output, + &mut unresolved_call, + &mut completed_call_ids, + &mut definition_accumulator, + )?); + } + InputItem::FunctionCall(call) => { + ensure_history_call_is_available(call, &mut definition_accumulator)?; + private_items.push(item.clone()); + } + InputItem::CompactionTrigger => {} + InputItem::Message(_) + | InputItem::FunctionCallOutput(_) + | InputItem::CustomToolCall(_) + | InputItem::CustomToolCallOutput(_) + | InputItem::Reasoning(_) + | InputItem::Compaction(_) + | InputItem::Unknown => private_items.push(item.clone()), + } + } + + if unresolved_call.is_some() { + return Err(ToolError::Config( + "unresolved tool_search_call requires a matching completed tool_search_output".to_owned(), + )); + } + *unqualified_call_positions = definition_accumulator.unqualified_call_positions; + Ok(ResponsesInput::Items(private_items)) +} + +fn ensure_history_call_is_available( + call: &InputFunctionToolCall, + definitions: &mut DefinitionAccumulator<'_>, +) -> Result<(), ToolError> { + if let Some(namespace) = call.namespace.as_deref() { + let member = definitions + .definition_indexes + .get(namespace) + .and_then(|index| definitions.definitions.get(*index)) + .filter(|record| matches!(record.identity, LoadedToolIdentity::Namespace(_))) + .and_then(|record| record.namespace_members.as_ref()) + .and_then(|members| members.indexes.get(&call.name).map(|index| &members.ordered[*index])); + match member { + Some(member) if !member.loaded => return Err(withheld_function_history_call()), + Some(_) => {} + None => { + definitions + .prior_unknown_namespace_calls + .entry(namespace.to_owned()) + .or_default() + .insert(call.name.clone()); + } + } + } else { + if definitions.withheld_function_names.contains(&call.name) { + return Err(withheld_function_history_call()); + } + let position = definitions + .current_history_position + .ok_or_else(|| ToolError::Config("tool-search history position is unavailable".to_owned()))?; + definitions + .unqualified_call_positions + .entry(call.name.clone()) + .or_insert(position); + } + Ok(()) +} + +fn withheld_function_history_call() -> ToolError { + ToolError::Config("request history calls a function before its definition is loaded".to_owned()) +} + +fn prepare_search_call( + call: &InputToolSearchCall, + unresolved_call: &mut Option, + completed_call_ids: &HashSet, + item_ids: &mut HashSet, +) -> Result { + if call.id.trim().is_empty() { + return Err(ToolError::Config("tool_search_call id must not be blank".to_owned())); + } + if call.call_id.trim().is_empty() { + return Err(ToolError::Config( + "tool_search_call call_id must not be blank".to_owned(), + )); + } + if !item_ids.insert(call.id.clone()) { + return Err(ToolError::Config("duplicate tool_search_call item id".to_owned())); + } + if unresolved_call.is_some() { + return Err(ToolError::Config( + "ambiguous tool-search history contains a call before the preceding call is resolved".to_owned(), + )); + } + if completed_call_ids.contains(call.call_id.as_str()) { + return Err(ToolError::Config("duplicate tool_search_call call_id".to_owned())); + } + let canonical_arguments = serialize_to_string(&Value::Object(call.arguments.clone())) + .map_err(|_| ToolError::Config("tool_search_call arguments could not be canonicalized safely".to_owned()))?; + *unresolved_call = Some(PendingSearchCall { + call_id: call.call_id.clone(), + }); + Ok(InputItem::FunctionCall(InputFunctionToolCall { + id: Some(call.id.clone()), + call_id: call.call_id.clone(), + name: TOOL_SEARCH_NAME.to_owned(), + namespace: None, + arguments: canonical_arguments, + status: Some(MessageStatus::Completed), + })) +} + +fn prepare_search_output( + output: &ToolSearchOutputMessage, + unresolved_call: &mut Option, + completed_call_ids: &mut HashSet, + definition_accumulator: &mut DefinitionAccumulator<'_>, +) -> Result { + if output.call_id.trim().is_empty() { + return Err(ToolError::Config( + "tool_search_output call_id must not be blank".to_owned(), + )); + } + if completed_call_ids.contains(output.call_id.as_str()) { + return Err(ToolError::Config("duplicate tool_search_output call_id".to_owned())); + } + let Some(pending) = unresolved_call.take() else { + return Err(ToolError::Config( + "orphan tool_search_output has no unresolved call".to_owned(), + )); + }; + if pending.call_id != output.call_id { + return Err(ToolError::Config( + "tool_search_output call_id does not match the preceding unresolved call".to_owned(), + )); + } + if output.status != ToolSearchStatus::Completed { + return Err(ToolError::Config( + "tool_search_output must be completed before it may load tool definitions".to_owned(), + )); + } + for tool in &output.tools { + load_definition(tool, definition_accumulator)?; + } + let projected_tools = model_visible_output_tools(&output.tools)?; + let canonical_value = serialize_to_value(&CanonicalToolSearchOutput { + tools: &projected_tools, + }) + .map_err(|_| ToolError::Config("tool_search_output could not be canonicalized safely".to_owned()))?; + let canonical_output = serialize_to_string(&canonical_value) + .map_err(|_| ToolError::Config("tool_search_output could not be canonicalized safely".to_owned()))?; + completed_call_ids.insert(output.call_id.clone()); + Ok(InputItem::FunctionCallOutput(FunctionToolResultMessage { + call_id: output.call_id.clone(), + output: ToolCallOutput::Text(canonical_output), + })) +} + +fn model_visible_output_tools(tools: &[ResponsesTool]) -> Result>, ToolError> { + tools + .iter() + .map(|tool| match tool { + ResponsesTool::Function(definition) => Ok(ModelVisibleLoadedTool::Function(ModelVisibleFunction { + type_: "function", + definition, + })), + ResponsesTool::Namespace(namespace) => Ok(ModelVisibleLoadedTool::Namespace(ModelVisibleNamespace { + type_: "namespace", + name: &namespace.name, + description: namespace.description.as_deref(), + })), + ResponsesTool::ToolSearch(_) + | ResponsesTool::Mcp(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Custom(_) + | ResponsesTool::Unknown => Err(ToolError::Config( + "tool_search_output contains an unsupported model-output definition".to_owned(), + )), + }) + .collect() +} + +fn load_definition(tool: &ResponsesTool, definitions: &mut DefinitionAccumulator<'_>) -> Result<(), ToolError> { + let identity = loaded_tool_identity(tool)? + .ok_or_else(|| ToolError::Config("tool_search_output contains an unsupported tool definition".to_owned()))?; + let canonical = canonical_definition(tool)?; + if let Some(index) = definitions.definition_indexes.get(identity.name()).copied() { + let record = &mut definitions.definitions[index]; + if record.identity != identity || record.canonical != canonical { + return Err(ToolError::Config(format!( + "loaded definition for identity '{}' conflicts with its existing type, schema, description, or configuration", + identity.name() + ))); + } + if let ResponsesTool::Namespace(returned) = tool { + return load_namespace_members( + returned, + record, + definitions.public_tools, + definitions.loaded_public_tools, + definitions.withheld_function_names, + &definitions.prior_unknown_namespace_calls, + &definitions.unqualified_call_positions, + ); + } + if !record.loaded { + if definitions.withheld_function_names.contains(record.identity.name()) + && definitions + .unqualified_call_positions + .contains_key(record.identity.name()) + { + return Err(withheld_function_history_call()); + } + record.loaded = true; + definitions.withheld_function_names.remove(record.identity.name()); + definitions + .loaded_public_tools + .push(definitions.public_tools[record.public_index].clone()); + } + return Ok(()); + } + + match tool { + ResponsesTool::Function(function) + if definitions + .unqualified_call_positions + .contains_key(function.name.as_str()) => + { + return Err(withheld_function_history_call()); + } + ResponsesTool::Namespace(namespace) => ensure_namespace_members_do_not_resolve_prior_calls( + namespace, + namespace.tools.iter(), + &definitions.prior_unknown_namespace_calls, + &definitions.unqualified_call_positions, + )?, + _ => {} + } + let public_index = definitions.public_tools.len(); + definitions.public_tools.push(tool.clone()); + let index = definitions.definitions.len(); + definitions.definition_indexes.insert(identity.name().to_owned(), index); + definitions + .definitions + .push(definition_record(tool, identity, public_index, true)?); + definitions.loaded_public_tools.push(tool.clone()); + Ok(()) +} + +fn ensure_namespace_members_do_not_resolve_prior_calls<'a>( + namespace: &CodexNamespaceToolParam, + members: impl Iterator, + prior_unknown_namespace_calls: &HashMap>, + unqualified_call_positions: &HashMap, +) -> Result<(), ToolError> { + let prior_public_members = prior_unknown_namespace_calls.get(&namespace.name); + for member in members { + let CodexNamespaceMember::Function(function) = member else { + continue; + }; + let public_match = prior_public_members.is_some_and(|members| members.contains(function.name.as_str())); + let flat_name = super::model_visible_namespace_member_name(&namespace.name, function.name.as_str()); + if public_match || unqualified_call_positions.contains_key(&flat_name) { + return Err(withheld_function_history_call()); + } + } + Ok(()) +} + +fn load_namespace_members( + returned: &CodexNamespaceToolParam, + record: &mut DefinitionRecord, + public_tools: &mut [ResponsesTool], + loaded_public_tools: &mut Vec, + withheld_function_names: &mut HashSet, + prior_unknown_namespace_calls: &HashMap>, + unqualified_call_positions: &HashMap, +) -> Result<(), ToolError> { + if returned.tools.is_empty() { + return Err(ToolError::Config( + "tool_search_output namespaces must contain at least one function member".to_owned(), + )); + } + let ResponsesTool::Namespace(public_namespace) = &mut public_tools[record.public_index] else { + return Err(ToolError::Config( + "namespace identity conflicts with an existing non-namespace definition".to_owned(), + )); + }; + let members = record + .namespace_members + .as_mut() + .ok_or_else(|| ToolError::Config("namespace definition is missing prepared member state".to_owned()))?; + ensure_namespace_members_do_not_resolve_prior_calls( + returned, + returned.tools.iter().filter(|member| match member { + CodexNamespaceMember::Function(function) => !members.indexes.contains_key(function.name.as_str()), + CodexNamespaceMember::Unknown => true, + }), + prior_unknown_namespace_calls, + unqualified_call_positions, + )?; + let mut newly_loaded = Vec::new(); + for member in &returned.tools { + let CodexNamespaceMember::Function(returned_function) = member else { + return Err(ToolError::Config( + "tool_search_output namespaces may contain only function members".to_owned(), + )); + }; + let member_name = returned_function.name.as_str(); + let canonical = canonical_namespace_member(returned_function)?; + if let Some(member_index) = members.indexes.get(member_name).copied() { + let member_record = &mut members.ordered[member_index]; + if member_record.canonical != canonical { + return Err(ToolError::Config(format!( + "loaded namespace member '{}.{member_name}' conflicts with its existing schema, description, or configuration", + returned.name + ))); + } + if !member_record.loaded { + let unloaded_count = members.unloaded_count.checked_sub(1).ok_or_else(|| { + ToolError::Config("namespace member availability state is inconsistent".to_owned()) + })?; + member_record.loaded = true; + members.unloaded_count = unloaded_count; + withheld_function_names + .remove(&super::model_visible_namespace_member_name(&returned.name, member_name)); + newly_loaded.push(public_namespace.tools[member_record.public_member_index].clone()); + } + continue; + } + + let public_member_index = public_namespace.tools.len(); + public_namespace.tools.push(member.clone()); + members.indexes.insert(member_name.to_owned(), members.ordered.len()); + members.ordered.push(NamespaceMemberRecord { + canonical, + public_member_index, + loaded: true, + }); + newly_loaded.push(member.clone()); + } + if !newly_loaded.is_empty() { + let mut loaded_subset = public_namespace.clone(); + loaded_subset.tools = newly_loaded; + loaded_public_tools.push(ResponsesTool::Namespace(loaded_subset)); + } + record.loaded = members.unloaded_count == 0; + Ok(()) +} + +fn loaded_tool_identity(tool: &ResponsesTool) -> Result, ToolError> { + let identity = match tool { + ResponsesTool::Function(function) => LoadedToolIdentity::Function(function.name.as_str().to_owned()), + ResponsesTool::Namespace(namespace) => LoadedToolIdentity::Namespace(namespace.name.clone()), + ResponsesTool::ToolSearch(_) + | ResponsesTool::Mcp(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Custom(_) + | ResponsesTool::Unknown => return Ok(None), + }; + if identity.name().trim().is_empty() { + return Err(ToolError::Config(format!( + "{} definition identity must not be blank", + identity.kind() + ))); + } + if matches!( + &identity, + LoadedToolIdentity::Function(name) | LoadedToolIdentity::Namespace(name) + if name == TOOL_SEARCH_NAME + ) { + return Err(ToolError::Config( + "model-visible tool name 'tool_search' is reserved while tool search is active".to_owned(), + )); + } + Ok(Some(identity)) +} + +fn canonical_definition(tool: &ResponsesTool) -> Result { + let projected = match tool { + ResponsesTool::Namespace(namespace) => { + let mut namespace = namespace.clone(); + namespace.tools.clear(); + ResponsesTool::Namespace(namespace) + } + other => other.clone(), + }; + serialize_to_value(&projected) + .map_err(|_| ToolError::Config("tool-search definition could not be compared safely".to_owned())) +} + +fn canonical_namespace_member(function: &FunctionToolParam) -> Result { + serialize_to_value(function) + .map_err(|_| ToolError::Config("namespace member definition could not be compared safely".to_owned())) +} + +fn build_catalog( + public_tools: &[ResponsesTool], + definitions: &[DefinitionRecord], + definition_indexes: &HashMap, +) -> Vec { + public_tools + .iter() + .filter_map(|tool| { + let identity = loaded_tool_identity(tool).ok().flatten()?; + let record = &definitions[*definition_indexes.get(identity.name())?]; + if record.loaded { + return None; + } + match tool { + ResponsesTool::Function(function) if function.defer_loading == Some(true) => { + Some(CatalogEntry::Function { + name: function.name.as_str().to_owned(), + description: function.description.clone(), + }) + } + ResponsesTool::Namespace(namespace) + if namespace_has_withheld_member(record.namespace_members.as_ref()) => + { + Some(CatalogEntry::Namespace { + name: namespace.name.clone(), + description: namespace.description.clone(), + }) + } + ResponsesTool::Function(_) + | ResponsesTool::Namespace(_) + | ResponsesTool::Mcp(_) + | ResponsesTool::ToolSearch(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Custom(_) + | ResponsesTool::Unknown => None, + } + }) + .collect() +} + +/// Catalog prose deliberately follows the provider-characterization shape: +/// declaration text, then one ordered semicolon-delimited list of `name — +/// description` entries. It never uses schemas or execution configuration. +fn synthetic_description(description: &str, catalog: &[CatalogEntry]) -> String { + if catalog.is_empty() { + return description.to_owned(); + } + let entries = catalog + .iter() + .map(|entry| { + entry.description().map_or_else( + || entry.display_name().to_owned(), + |description| { + let description = description.trim(); + if description.is_empty() { + entry.display_name().to_owned() + } else { + format!("{} — {description}", entry.display_name()) + } + }, + ) + }) + .collect::>() + .join("; "); + let noun = if catalog.len() == 1 { "entry" } else { "entries" }; + format!( + "{}. Available catalog {noun}: {entries}.", + description.trim().trim_end_matches('.') + ) +} + +fn synthetic_tool_search(declaration: &ToolSearchToolParam, catalog: &[CatalogEntry]) -> ToolSearchToolParam { + let mut normalized = ToolSearchHandler::normalized_param(declaration); + let description = normalized.description.as_deref().unwrap_or_default(); + normalized.description = Some(synthetic_description(description, catalog)); + normalized +} + +fn build_private_tools( + public_tools: &[ResponsesTool], + definitions: &[DefinitionRecord], + definition_indexes: &HashMap, + synthetic_tool_search: Option<&ToolSearchToolParam>, +) -> Vec { + public_tools + .iter() + .filter_map(|tool| match tool { + ResponsesTool::ToolSearch(_) => synthetic_tool_search.cloned().map(ResponsesTool::ToolSearch), + ResponsesTool::Function(_) | ResponsesTool::Namespace(_) => { + private_definition(tool, definitions, definition_indexes) + } + ResponsesTool::Mcp(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Custom(_) + | ResponsesTool::Unknown => Some(tool.clone()), + }) + .collect() +} + +fn available_public_tools(public_tools: &[ResponsesTool], loaded_tools: &[ResponsesTool]) -> Vec { + let mut loaded_functions = HashSet::new(); + let mut loaded_namespace_members = HashMap::<&str, HashSet<&str>>::new(); + for tool in loaded_tools { + match tool { + ResponsesTool::Function(function) => { + loaded_functions.insert(function.name.as_str()); + } + ResponsesTool::Namespace(namespace) => { + let members = loaded_namespace_members.entry(namespace.name.as_str()).or_default(); + members.extend(namespace.tools.iter().filter_map(|member| match member { + CodexNamespaceMember::Function(function) => Some(function.name.as_str()), + CodexNamespaceMember::Unknown => None, + })); + } + ResponsesTool::ToolSearch(_) + | ResponsesTool::Mcp(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Custom(_) + | ResponsesTool::Unknown => {} + } + } + + public_tools + .iter() + .filter_map(|tool| match tool { + ResponsesTool::Function(function) => { + let loaded = loaded_functions.contains(function.name.as_str()); + if function.defer_loading == Some(true) && !loaded { + return None; + } + let mut available = function.clone(); + if loaded { + available.defer_loading = None; + } + Some(ResponsesTool::Function(available)) + } + ResponsesTool::Namespace(namespace) => { + let loaded_members = loaded_namespace_members.get(namespace.name.as_str()); + let mut available = namespace.clone(); + available.tools = available + .tools + .into_iter() + .filter_map(|member| match member { + CodexNamespaceMember::Function(mut function) => { + let loaded = loaded_members.is_some_and(|members| members.contains(function.name.as_str())); + if function.defer_loading == Some(true) && !loaded { + return None; + } + if loaded { + function.defer_loading = None; + } + Some(CodexNamespaceMember::Function(function)) + } + CodexNamespaceMember::Unknown => None, + }) + .collect(); + (!available.tools.is_empty()).then_some(ResponsesTool::Namespace(available)) + } + ResponsesTool::Mcp(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Custom(_) + | ResponsesTool::Unknown => Some(tool.clone()), + ResponsesTool::ToolSearch(_) => None, + }) + .collect() +} + +fn private_definition( + tool: &ResponsesTool, + definitions: &[DefinitionRecord], + definition_indexes: &HashMap, +) -> Option { + let identity = loaded_tool_identity(tool).ok().flatten()?; + let loaded = definitions[*definition_indexes.get(identity.name())?].loaded; + match tool { + ResponsesTool::Function(function) if loaded || function.defer_loading != Some(true) => { + let mut function = function.clone(); + function.defer_loading = None; + Some(ResponsesTool::Function(function)) + } + ResponsesTool::Namespace(namespace) => private_namespace( + namespace, + definitions[*definition_indexes.get(identity.name())?] + .namespace_members + .as_ref(), + ) + .map(ResponsesTool::Namespace), + ResponsesTool::Function(_) + | ResponsesTool::Mcp(_) + | ResponsesTool::ToolSearch(_) + | ResponsesTool::WebSearch(_) + | ResponsesTool::FileSearch(_) + | ResponsesTool::CodeInterpreter(_) + | ResponsesTool::Custom(_) + | ResponsesTool::Unknown => None, + } +} + +fn private_namespace( + namespace: &CodexNamespaceToolParam, + member_records: Option<&NamespaceMemberRecords>, +) -> Option { + let member_records = member_records?; + let tools = namespace + .tools + .iter() + .filter_map(|member| match member { + CodexNamespaceMember::Function(function) + if member_records + .indexes + .get(function.name.as_str()) + .is_some_and(|index| member_records.ordered[*index].loaded) => + { + let mut function = function.clone(); + function.defer_loading = None; + Some(CodexNamespaceMember::Function(function)) + } + CodexNamespaceMember::Function(_) => None, + CodexNamespaceMember::Unknown => Some(CodexNamespaceMember::Unknown), + }) + .collect::>(); + (!tools.is_empty()).then(|| CodexNamespaceToolParam { + tools, + ..namespace.clone() + }) +} + +fn namespace_has_withheld_member(member_records: Option<&NamespaceMemberRecords>) -> bool { + member_records.is_some_and(|members| members.unloaded_count != 0) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + use crate::tool::ToolRegistry; + + fn param(value: Value) -> ToolSearchToolParam { + let ResponsesTool::ToolSearch(param) = serde_json::from_value(value).expect("valid tool_search declaration") + else { + panic!("expected tool_search"); + }; + param + } + + #[test] + fn handler_validates_and_normalizes_exactly_one_function() { + let param = param(json!({ + "type": "tool_search", + "execution": "client", + "description": "Find matching tools", + "parameters": {"type": "object", "properties": {"term": {"type": "string"}}} + })); + let value = serde_json::to_value(¶m).unwrap(); + + ToolSearchHandler.validate(&value).unwrap(); + assert_eq!(ToolSearchHandler.tool_type(), ToolType::ToolSearch); + assert_eq!( + serde_json::to_value(ToolSearchHandler.normalize(&value)).unwrap(), + json!([{ + "type": "function", + "name": "tool_search", + "description": "Find matching tools", + "parameters": {"type": "object", "properties": {"term": {"type": "string"}}}, + "strict": true + }]) + ); + } + + #[test] + fn normalization_uses_safe_defaults() { + let param = param(json!({"type": "tool_search", "execution": "client", "description": " "})); + let value = serde_json::to_value(¶m).unwrap(); + assert_eq!( + serde_json::to_value(ToolSearchHandler.normalize(&value)).unwrap(), + json!([{ + "type": "function", + "name": "tool_search", + "description": "Search the client tool catalog", + "parameters": { + "type": "object", + "properties": {"query": { + "type": "string", + "description": "A concise description of the needed capabilities." + }}, + "required": ["query"], + "additionalProperties": false + }, + "strict": true + }]) + ); + } + + #[test] + fn synthetic_public_call_construction_is_validated_in_tool_layer() { + let valid = FunctionToolCall { + id: "fc_search".to_owned(), + call_id: "call_search".to_owned(), + name: TOOL_SEARCH_NAME.to_owned(), + namespace: None, + arguments: r#"{"query":"weather"}"#.to_owned(), + status: MessageStatus::Completed, + }; + let started = started_public_call(&valid).expect("valid started call"); + assert_eq!(started.id, "tsc_search"); + assert_eq!(started.status, ToolSearchStatus::InProgress); + let completed = completed_public_call(&valid).expect("valid completed call"); + assert_eq!(completed.arguments["query"], "weather"); + + for invalid in [ + FunctionToolCall { + name: "ordinary".to_owned(), + ..valid.clone() + }, + FunctionToolCall { + namespace: Some("catalog".to_owned()), + ..valid.clone() + }, + FunctionToolCall { + arguments: "[]".to_owned(), + ..valid.clone() + }, + FunctionToolCall { + status: MessageStatus::InProgress, + ..valid.clone() + }, + ] { + assert!(completed_public_call(&invalid).is_err()); + } + } + + #[test] + fn terminal_projection_rejects_or_discards_unfinished_calls() { + let synthetic = FunctionToolCall { + id: "fc_search".to_owned(), + call_id: "call_search".to_owned(), + name: TOOL_SEARCH_NAME.to_owned(), + namespace: None, + arguments: String::new(), + status: MessageStatus::InProgress, + }; + assert!(project_synthetic_call(&synthetic, false, true).is_err()); + assert!(project_synthetic_call(&synthetic, true, true).unwrap().is_none()); + + let native = ToolSearchCall { + id: "tsc_search".to_owned(), + call_id: "call_search".to_owned(), + execution: crate::types::tools::ToolSearchExecution::Client, + arguments: Map::new(), + status: ToolSearchStatus::Incomplete, + }; + assert!(project_native_call(&native, false).is_err()); + assert!(project_native_call(&native, true).unwrap().is_none()); + } + + #[test] + fn registry_requires_tool_search_preparation_before_upstream_conversion() { + let mut request: RequestPayload = serde_json::from_value(json!({ + "model": "test", + "input": "find weather", + "parallel_tool_calls": false, + "tools": [{"type": "tool_search", "execution": "client"}] + })) + .expect("request shape"); + + assert!(ToolRegistry::default().ensure_request_prepared(&request).is_err()); + let registry = ToolRegistry::prepare_request(&mut request, &[], false).expect("tool-search preparation"); + registry + .ensure_request_prepared(&request) + .expect("prepared request is ready for upstream conversion"); + } + + #[test] + fn ordinary_function_named_tool_search_does_not_require_preparation() { + let request: RequestPayload = serde_json::from_value(json!({ + "model": "test", + "input": "call the ordinary function", + "tools": [{"type": "function", "name": "tool_search"}] + })) + .expect("ordinary function request"); + + ToolRegistry::default() + .ensure_request_prepared(&request) + .expect("the reserved name applies only to active tool search"); + } + + #[test] + fn registry_strictly_validates_blocking_search_without_changing_inactive_functions() { + let mut request: RequestPayload = serde_json::from_value(json!({ + "model": "test", + "input": "find weather", + "parallel_tool_calls": false, + "tools": [{"type": "tool_search", "execution": "client"}] + })) + .expect("request shape"); + let registry = ToolRegistry::prepare_request(&mut request, &[], false).expect("tool-search preparation"); + let native = json!({ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + }); + let synthetic = json!({ + "type": "function_call", + "id": "fc_search", + "call_id": "call_search", + "name": "tool_search", + "arguments": "{\"query\":\"weather\"}", + "status": "completed" + }); + let malformed = [ + ("native missing id", { + let mut item = native.clone(); + item.as_object_mut().unwrap().remove("id"); + item + }), + ("native missing call_id", { + let mut item = native.clone(); + item.as_object_mut().unwrap().remove("call_id"); + item + }), + ("native missing arguments", { + let mut item = native.clone(); + item.as_object_mut().unwrap().remove("arguments"); + item + }), + ("native namespace", { + let mut item = native.clone(); + item["namespace"] = json!("catalog"); + item + }), + ("synthetic missing status", { + let mut item = synthetic.clone(); + item.as_object_mut().unwrap().remove("status"); + item + }), + ("synthetic null status", { + let mut item = synthetic.clone(); + item["status"] = Value::Null; + item + }), + ]; + + for (case, item) in malformed { + let body = json!({"status": "completed", "output": [item]}).to_string(); + assert!( + matches!( + registry.validate_blocking_response(&body), + Err(ToolError::InvalidUpstreamToolSearch) + ), + "{case}" + ); + } + + let partial = json!({ + "status": "incomplete", + "output": [{ + "type": "function_call", + "id": "fc_partial", + "call_id": "call_partial", + "name": "tool_search", + "arguments": "{\"query\":", + "status": "in_progress" + }] + }) + .to_string(); + registry + .validate_blocking_response(&partial) + .expect("unfinished search placeholder is allowed on an incomplete response"); + + let ordinary = json!({ + "status": "completed", + "output": [{"type": "function_call", "name": "tool_search", "arguments": "{}"}] + }) + .to_string(); + ToolRegistry::default() + .validate_blocking_response(&ordinary) + .expect("inactive ordinary function keeps generic compatibility defaults"); + } + + #[test] + fn prepared_response_tools_remove_request_scoped_mcp_secrets_and_discovery() { + let mut request: RequestPayload = serde_json::from_value(json!({ + "model": "test", + "input": "find weather", + "parallel_tool_calls": false, + "tools": [ + {"type": "tool_search", "execution": "client"}, + { + "type": "mcp", + "server_label": "weather", + "server_url": "https://mcp.example.test/mcp", + "headers": {"Authorization": "Bearer header-secret"}, + "authorization": "field-secret", + "_agentic_discovered_tools": [{ + "server_label": "weather", + "tool_name": "forecast", + "internal_name": "mcp__weather__forecast", + "tool": {"name": "forecast", "inputSchema": {"type": "object"}} + }] + } + ] + })) + .expect("request shape"); + + let prepared = ToolRegistry::prepare_request(&mut request, &[], false).expect("tool-search preparation"); + let serialized = serde_json::to_value(prepared.tool_search_response_tools().expect("active public tools")) + .expect("public tools serialize"); + let serialized = serialized.to_string(); + + for secret in [ + "header-secret", + "field-secret", + "mcp__weather__forecast", + "_agentic_discovered_tools", + ] { + assert!(!serialized.contains(secret)); + } + } + + #[test] + fn public_tool_search_item_ids_are_stable_and_domain_separated() { + assert_eq!(public_item_id("tsc_existing"), "tsc_existing"); + assert_eq!(public_item_id("fc_search_1"), "tsc_search_1"); + let first = public_item_id("provider-item-1"); + assert_eq!(first, public_item_id("provider-item-1")); + assert!(first.starts_with("tsc_")); + assert_ne!(first, crate::tool::custom::public_item_id("provider-item-1")); + } + + #[test] + fn response_tools_after_search_keep_immediate_and_loaded_public_availability() { + let request: RequestPayload = serde_json::from_value(serde_json::json!({ + "model": "test", + "store": false, + "tools": [ + { + "type": "tool_search", + "execution": "client", + "description": "Find tools", + "parameters": {"type": "object"} + }, + {"type": "function", "name": "always_ready"}, + {"type": "function", "name": "get_weather", "defer_loading": true}, + {"type": "function", "name": "not_loaded", "defer_loading": true}, + { + "type": "namespace", + "name": "travel", + "tools": [ + {"type": "function", "name": "always_ready_member"}, + {"type": "function", "name": "get_timezone", "defer_loading": true}, + {"type": "function", "name": "not_loaded_member", "defer_loading": true} + ] + } + ], + "input": [ + { + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "arguments": {"query": "weather and timezone"} + }, + { + "type": "tool_search_output", + "call_id": "call_search_1", + "tools": [ + {"type": "function", "name": "get_weather", "defer_loading": true}, + { + "type": "namespace", + "name": "travel", + "tools": [{"type": "function", "name": "get_timezone", "defer_loading": true}] + } + ] + } + ] + })) + .expect("valid mixed-availability tool-search request"); + + let state = ToolSearchState::build(&request).expect("tool-search state"); + let tools = serialize_to_value(&state.public_response_tools()).expect("response tools serialize"); + assert_eq!( + tools, + serde_json::json!([ + {"type": "function", "name": "always_ready"}, + {"type": "function", "name": "get_weather"}, + { + "type": "namespace", + "name": "travel", + "tools": [ + {"type": "function", "name": "always_ready_member"}, + {"type": "function", "name": "get_timezone"} + ] + } + ]) + ); + } + + #[test] + fn tool_search_output_rejects_mcp_definitions() { + let request: RequestPayload = serde_json::from_value(serde_json::json!({ + "model": "test", + "store": false, + "parallel_tool_calls": false, + "tools": [{ + "type": "tool_search", + "execution": "client", + "description": "Find a tool", + "parameters": {"type": "object"} + }], + "input": [ + { + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "arguments": {"query": "weather"} + }, + { + "type": "tool_search_output", + "call_id": "call_search_1", + "tools": [{ + "type": "mcp", + "server_label": "weather", + "server_url": "https://mcp.example.test/mcp" + }] + } + ] + })) + .expect("typed request"); + + let error = ToolSearchState::build(&request).expect_err("MCP is not a client-loaded tool definition"); + + assert!(matches!( + error, + ToolError::Config(message) if message.contains("unsupported tool definition") + )); + } +} diff --git a/crates/agentic-server-core/src/types/io/input.rs b/crates/agentic-server-core/src/types/io/input.rs index 625f41c9..2990e6db 100644 --- a/crates/agentic-server-core/src/types/io/input.rs +++ b/crates/agentic-server-core/src/types/io/input.rs @@ -4,9 +4,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::types::event::MessageStatus; +use crate::types::tools::{ResponsesTool, ToolSearchExecution, ToolSearchStatus}; use crate::utils::common::deserialize_from_value; -use super::output::{CustomToolCall, FunctionToolCall, ReasoningOutput}; +use super::output::{CustomToolCall, FunctionToolCall, ReasoningOutput, ToolSearchCall}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InputTextContent { @@ -167,6 +168,60 @@ impl From for InputFunctionToolCall { } } +pub(super) fn deserialize_non_blank_string<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = String::deserialize(deserializer)?; + if value.trim().is_empty() { + return Err(serde::de::Error::custom("value must not be blank")); + } + Ok(value) +} + +/// A public model-generated tool-search call replayed as Responses input. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InputToolSearchCall { + #[serde(deserialize_with = "deserialize_non_blank_string")] + pub id: String, + #[serde(deserialize_with = "deserialize_non_blank_string")] + pub call_id: String, + #[serde(default)] + pub execution: ToolSearchExecution, + pub arguments: serde_json::Map, + #[serde(default)] + pub status: ToolSearchStatus, +} + +impl TryFrom<&ToolSearchCall> for InputToolSearchCall { + type Error = ToolSearchStatus; + + fn try_from(call: &ToolSearchCall) -> Result { + if call.status != ToolSearchStatus::Completed { + return Err(call.status); + } + Ok(Self { + id: call.id.clone(), + call_id: call.call_id.clone(), + execution: call.execution, + arguments: call.arguments.clone(), + status: ToolSearchStatus::Completed, + }) + } +} + +/// Client-returned declarations resolving a public tool-search call. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolSearchOutputMessage { + #[serde(deserialize_with = "deserialize_non_blank_string")] + pub call_id: String, + #[serde(default)] + pub execution: ToolSearchExecution, + #[serde(default)] + pub status: ToolSearchStatus, + pub tools: Vec, +} + /// An opaque compacted context checkpoint accepted as Responses input. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompactionItem { @@ -204,6 +259,10 @@ pub enum InputItem { FunctionCall(InputFunctionToolCall), #[serde(rename = "function_call_output")] FunctionCallOutput(FunctionToolResultMessage), + #[serde(rename = "tool_search_call")] + ToolSearchCall(InputToolSearchCall), + #[serde(rename = "tool_search_output")] + ToolSearchOutput(ToolSearchOutputMessage), /// The public freeform invocation accepted from a client request. #[serde(rename = "custom_tool_call")] CustomToolCall(CustomToolCall), @@ -232,6 +291,8 @@ impl<'de> Deserialize<'de> for InputItem { None | Some("message") => deserialize_from_value(value).map(Self::Message), Some("function_call") => deserialize_from_value(value).map(Self::FunctionCall), Some("function_call_output") => deserialize_from_value(value).map(Self::FunctionCallOutput), + Some("tool_search_call") => deserialize_from_value(value).map(Self::ToolSearchCall), + Some("tool_search_output") => deserialize_from_value(value).map(Self::ToolSearchOutput), Some("custom_tool_call") => deserialize_from_value(value).map(Self::CustomToolCall), Some("custom_tool_call_output") => deserialize_from_value(value).map(Self::CustomToolCallOutput), Some("reasoning") => deserialize_from_value(value).map(Self::Reasoning), @@ -377,6 +438,125 @@ fn function_call_item_id(item_id: &str) -> Option { mod tests { use super::*; + #[test] + fn tool_search_replay_defaults_are_canonicalized() { + let call: InputItem = serde_json::from_value(serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "arguments": {"query": "weather"} + })) + .expect("valid replayed search call"); + let output: InputItem = serde_json::from_value(serde_json::json!({ + "type": "tool_search_output", + "call_id": "call_search_1", + "tools": [] + })) + .expect("valid empty search result"); + + assert_eq!( + serde_json::to_value(call).expect("call serializes"), + serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + }) + ); + assert_eq!( + serde_json::to_value(output).expect("output serializes"), + serde_json::json!({ + "type": "tool_search_output", + "call_id": "call_search_1", + "execution": "client", + "status": "completed", + "tools": [] + }) + ); + } + + #[test] + fn tool_search_items_accept_documented_statuses() { + for status in ["in_progress", "completed", "incomplete"] { + let call: InputItem = serde_json::from_value(serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "arguments": {"query": "weather"}, + "status": status + })) + .expect("documented tool-search call status"); + let output: InputItem = serde_json::from_value(serde_json::json!({ + "type": "tool_search_output", + "call_id": "call_search_1", + "status": status, + "tools": [] + })) + .expect("documented tool-search output status"); + + assert_eq!(serde_json::to_value(call).expect("call serializes")["status"], status); + assert_eq!( + serde_json::to_value(output).expect("output serializes")["status"], + status + ); + } + } + + #[test] + fn tool_search_replay_rejects_invalid_known_shapes() { + for item in [ + serde_json::json!({ + "type": "tool_search_call", + "call_id": "call_search_1", + "arguments": {"query": "missing required item id"} + }), + serde_json::json!({ + "type": "tool_search_call", + "id": " ", + "call_id": "call_search_1", + "arguments": {"query": "blank item id"} + }), + serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": " ", + "arguments": {"query": "blank call id"} + }), + serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "execution": "server", + "arguments": {"query": "unsupported execution"} + }), + serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "arguments": "not an object", + "status": "completed" + }), + serde_json::json!({ + "type": "tool_search_output", + "call_id": "call_search_1" + }), + ] { + assert!( + serde_json::from_value::(item).is_err(), + "malformed known tool-search item must not become Unknown" + ); + } + + let future: InputItem = serde_json::from_value(serde_json::json!({ + "type": "future_search_item", + "payload": {"opaque": true} + })) + .expect("unrelated future item remains forward-compatible"); + assert!(matches!(future, InputItem::Unknown)); + } + #[test] fn function_call_input_accepts_missing_status() { let item: InputItem = serde_json::from_value(serde_json::json!({ diff --git a/crates/agentic-server-core/src/types/io/mod.rs b/crates/agentic-server-core/src/types/io/mod.rs index 648b9036..216d153c 100644 --- a/crates/agentic-server-core/src/types/io/mod.rs +++ b/crates/agentic-server-core/src/types/io/mod.rs @@ -6,13 +6,13 @@ pub mod usage; pub use input::{ CompactionItem, CustomToolCallOutputMessage, FunctionToolResultMessage, InputContent, InputFileContent, InputFunctionToolCall, InputImageContent, InputItem, InputMessage, InputMessageContent, InputTextContent, - ResponsesInput, ToolCallOutput, ToolOutputContent, + InputToolSearchCall, ResponsesInput, ToolCallOutput, ToolOutputContent, ToolSearchOutputMessage, }; pub use output::{ ApplyDone, CustomToolCall, FunctionToolCall, GatewayCallStatus, McpCall, McpCallError, McpCallStatus, McpToolExecutionError, McpToolExecutionErrorContent, OutputItem, OutputMessage, OutputTextContent, ReasoningOutput, - ReasoningTextContent, WebSearchAction, WebSearchActionFindInPage, WebSearchActionOpenPage, WebSearchActionSearch, - WebSearchCall, WebSearchCallStatus, WebSearchSource, + ReasoningTextContent, ToolSearchCall, WebSearchAction, WebSearchActionFindInPage, WebSearchActionOpenPage, + WebSearchActionSearch, WebSearchCall, WebSearchCallStatus, WebSearchSource, }; pub use tools::{AllowedTool, AllowedToolsMode, FunctionTool, ToolChoice}; pub(crate) use tools::{resolve_tool_choice, resolve_tools}; diff --git a/crates/agentic-server-core/src/types/io/output.rs b/crates/agentic-server-core/src/types/io/output.rs index 85867627..931eec15 100644 --- a/crates/agentic-server-core/src/types/io/output.rs +++ b/crates/agentic-server-core/src/types/io/output.rs @@ -5,11 +5,13 @@ use crate::events::EventPayload; use crate::executor::error::ExecutorError; use crate::tool::ToolRegistry; use crate::types::event::MessageStatus; +use crate::types::tools::{ToolSearchExecution, ToolSearchStatus}; use crate::utils::common::deserialize_from_value_opt; use crate::utils::uuid7_str; use super::input::{ - CompactionItem, InputContent, InputFunctionToolCall, InputItem, InputMessage, InputMessageContent, InputTextContent, + CompactionItem, InputContent, InputFunctionToolCall, InputItem, InputMessage, InputMessageContent, + InputTextContent, InputToolSearchCall, deserialize_non_blank_string, }; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -102,6 +104,21 @@ pub struct FunctionToolCall { pub status: MessageStatus, } +/// A newly emitted public client tool-search call. +/// +/// Unlike replay input, execution and status have no serde defaults: response +/// translation must populate both fields explicitly. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolSearchCall { + #[serde(deserialize_with = "deserialize_non_blank_string")] + pub id: String, + #[serde(deserialize_with = "deserialize_non_blank_string")] + pub call_id: String, + pub execution: ToolSearchExecution, + pub arguments: serde_json::Map, + pub status: ToolSearchStatus, +} + /// A freeform custom tool invocation. /// /// `input` is opaque text and must not be parsed as function-call JSON. @@ -174,6 +191,30 @@ impl TryFrom<&EventPayload> for FunctionToolCall { } } +impl TryFrom<&EventPayload> for ToolSearchCall { + type Error = ExecutorError; + + fn try_from(payload: &EventPayload) -> Result { + let EventPayload::OutputItemAdded { item_id, call_id, .. } = payload else { + return Err(ExecutorError::ParseError("expected OutputItemAdded payload".into())); + }; + let call_id = call_id + .as_deref() + .filter(|call_id| !call_id.trim().is_empty()) + .ok_or_else(|| ExecutorError::ParseError("tool_search_call is missing call_id".into()))?; + if item_id.trim().is_empty() { + return Err(ExecutorError::ParseError("tool_search_call is missing id".into())); + } + Ok(Self { + id: item_id.clone(), + call_id: call_id.to_owned(), + execution: ToolSearchExecution::Client, + arguments: serde_json::Map::new(), + status: ToolSearchStatus::InProgress, + }) + } +} + impl TryFrom<&EventPayload> for CustomToolCall { type Error = ExecutorError; @@ -656,6 +697,17 @@ impl ApplyDone for FunctionToolCall { } } +impl ApplyDone for ToolSearchCall { + fn apply_done(&mut self, payload: &EventPayload, _buffer: &mut String) { + let EventPayload::OutputItemDone { item, .. } = payload else { + return; + }; + if let Some(call) = deserialize_from_value_opt(item.clone()) { + *self = call; + } + } +} + impl ApplyDone for CustomToolCall { fn apply_done(&mut self, payload: &EventPayload, buffer: &mut String) { match payload { @@ -731,6 +783,8 @@ pub enum OutputItem { Message(OutputMessage), #[serde(rename = "function_call")] FunctionCall(FunctionToolCall), + #[serde(rename = "tool_search_call")] + ToolSearchCall(ToolSearchCall), #[serde(rename = "custom_tool_call")] CustomToolCall(CustomToolCall), #[serde(rename = "web_search_call")] @@ -754,7 +808,7 @@ impl OutputItem { Self::FunctionCall(call) => registry .lookup(&call.name) .is_none_or(|entry| !entry.tool_type.is_gateway_owned()), - Self::CustomToolCall(_) => true, + Self::ToolSearchCall(_) | Self::CustomToolCall(_) => true, Self::Message(_) | Self::WebSearchCall(_) | Self::McpCall(_) @@ -771,6 +825,7 @@ impl OutputItem { Self::Message(message) => Some(InputItem::Message(message.clone().into())), Self::Reasoning(reasoning) => Some(InputItem::Reasoning(reasoning.clone())), Self::FunctionCall(call) => Some(InputItem::FunctionCall(InputFunctionToolCall::from(call.clone()))), + Self::ToolSearchCall(call) => InputToolSearchCall::try_from(call).ok().map(InputItem::ToolSearchCall), Self::CustomToolCall(call) => Some(InputItem::FunctionCall(call.clone().into())), Self::Compaction(item) => Some(InputItem::Compaction(item.clone())), Self::WebSearchCall(_) | Self::McpCall(_) | Self::McpListTools(_) | Self::Unknown => None, @@ -783,6 +838,82 @@ mod tests { use super::*; use crate::types::io::InputItem; + #[test] + fn emitted_tool_search_call_is_explicit_and_requires_client_action() { + let wire = serde_json::json!({ + "type": "tool_search_call", + "id": "provider_item_1", + "call_id": "call_search_1", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + }); + let item: OutputItem = serde_json::from_value(wire.clone()).expect("valid emitted search call"); + + assert_eq!(serde_json::to_value(&item).expect("call serializes"), wire); + assert!(item.requires_client_action(&ToolRegistry::default())); + let replay = item.to_input_item().expect("search call must remain public on replay"); + assert_eq!(serde_json::to_value(replay).expect("replay serializes"), wire); + } + + #[test] + fn emitted_tool_search_call_rejects_missing_or_invalid_required_fields() { + for missing in ["execution", "status"] { + let mut wire = serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + }); + wire.as_object_mut().expect("object").remove(missing); + + assert!( + serde_json::from_value::(wire).is_err(), + "newly emitted calls require explicit {missing}" + ); + } + + for (field, value) in [ + ("id", serde_json::json!(" ")), + ("call_id", serde_json::json!(" ")), + ("arguments", serde_json::json!("not an object")), + ] { + let mut wire = serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + }); + wire[field] = value; + + assert!( + serde_json::from_value::(wire).is_err(), + "newly emitted calls reject invalid {field}" + ); + } + } + + #[test] + fn unfinished_tool_search_call_statuses_deserialize_but_are_not_replayable() { + for status in ["in_progress", "incomplete"] { + let item: OutputItem = serde_json::from_value(serde_json::json!({ + "type": "tool_search_call", + "id": "provider_item_1", + "call_id": "call_search_1", + "execution": "client", + "arguments": {}, + "status": status + })) + .unwrap(); + + assert!(item.to_input_item().is_none()); + } + } + #[test] fn compaction_output_item_round_trips_with_type_tag() { let item: OutputItem = serde_json::from_value(serde_json::json!({ diff --git a/crates/agentic-server-core/src/types/mod.rs b/crates/agentic-server-core/src/types/mod.rs index 0c60e157..af992511 100644 --- a/crates/agentic-server-core/src/types/mod.rs +++ b/crates/agentic-server-core/src/types/mod.rs @@ -8,11 +8,11 @@ pub use io::{ AllowedTool, AllowedToolsMode, CompactionItem, CustomToolCall, CustomToolCallOutputMessage, FunctionTool, FunctionToolCall, FunctionToolResultMessage, GatewayCallStatus, InputContent, InputFileContent, InputFunctionToolCall, InputImageContent, InputItem, InputMessage, InputMessageContent, InputTextContent, - InputTokenDetails, McpCall, McpCallError, McpCallStatus, McpToolExecutionError, McpToolExecutionErrorContent, - OutputItem, OutputMessage, OutputTextContent, OutputTokenDetails, ReasoningOutput, ReasoningTextContent, - ResponseUsage, ResponsesInput, ToolCallOutput, ToolChoice, ToolOutputContent, WebSearchAction, - WebSearchActionFindInPage, WebSearchActionOpenPage, WebSearchActionSearch, WebSearchCall, WebSearchCallStatus, - WebSearchSource, + InputTokenDetails, InputToolSearchCall, McpCall, McpCallError, McpCallStatus, McpToolExecutionError, + McpToolExecutionErrorContent, OutputItem, OutputMessage, OutputTextContent, OutputTokenDetails, ReasoningOutput, + ReasoningTextContent, ResponseUsage, ResponsesInput, ToolCallOutput, ToolChoice, ToolOutputContent, ToolSearchCall, + ToolSearchOutputMessage, WebSearchAction, WebSearchActionFindInPage, WebSearchActionOpenPage, + WebSearchActionSearch, WebSearchCall, WebSearchCallStatus, WebSearchSource, }; pub use request_response::{ CompactRequest, CompactedResponse, ContextManagement, IncompleteDetails, RequestPayload, ResponsePayload, @@ -20,6 +20,7 @@ pub use request_response::{ }; pub use tools::{ CodeInterpreterToolParam, CodexNamespaceMember, CodexNamespaceToolParam, CustomToolParam, EmptyToolNameError, - FileSearchToolParam, FunctionToolParam, McpToolParam, NonEmptyToolName, ResponsesTool, WebSearchContextSize, - WebSearchFilters, WebSearchToolParam, WebSearchUserLocation, + FileSearchToolParam, FunctionToolParam, McpToolParam, NonEmptyToolName, ResponsesTool, ToolSearchExecution, + ToolSearchStatus, ToolSearchToolParam, WebSearchContextSize, WebSearchFilters, WebSearchToolParam, + WebSearchUserLocation, }; diff --git a/crates/agentic-server-core/src/types/request_response.rs b/crates/agentic-server-core/src/types/request_response.rs index 72ae5eee..6a5fca11 100644 --- a/crates/agentic-server-core/src/types/request_response.rs +++ b/crates/agentic-server-core/src/types/request_response.rs @@ -145,11 +145,12 @@ impl RequestPayload { }); let tools = tools.filter(|tools| !tools.is_empty()); let namespace_map = CodexNamespaceHandler.build_namespace_map(self.tools.as_deref())?; + let input = CodexNamespaceHandler.resolve_input(namespace_map.as_ref(), self.input.model_input()); let tool_choice = CodexNamespaceHandler.resolve_tool_choice(namespace_map.as_ref(), self.tool_choice.as_ref()); CustomHandler::validate_tool_choice(self.tools.as_deref(), &tool_choice)?; Ok(UpstreamRequest { model: &self.model, - input: self.input.model_input(), + input, stream, instructions: self.instructions.as_deref(), tools, @@ -220,6 +221,10 @@ pub struct ResponsePayload { pub previous_response_id: Option, pub conversation_id: Option, pub instructions: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tools: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_choice: Option, } impl ResponsePayload { @@ -735,6 +740,8 @@ mod tests { previous_response_id: None, conversation_id: None, instructions: None, + tools: None, + tool_choice: None, }; for (status, expected_type) in [ @@ -768,6 +775,8 @@ mod tests { previous_response_id: None, conversation_id: None, instructions: None, + tools: None, + tool_choice: None, }; let chunk = payload.as_created_response_chunk(); diff --git a/crates/agentic-server-core/src/types/tools/mod.rs b/crates/agentic-server-core/src/types/tools/mod.rs index acf36880..a7bf4b76 100644 --- a/crates/agentic-server-core/src/types/tools/mod.rs +++ b/crates/agentic-server-core/src/types/tools/mod.rs @@ -8,5 +8,6 @@ pub mod params; pub use params::{ CodeInterpreterToolParam, CodexNamespaceMember, CodexNamespaceToolParam, CustomToolParam, EmptyToolNameError, FileSearchToolParam, FunctionToolParam, McpDiscoveredToolParam, McpToolParam, NonEmptyToolName, ResponsesTool, - WebSearchContextSize, WebSearchFilters, WebSearchToolParam, WebSearchUserLocation, + ToolSearchExecution, ToolSearchStatus, ToolSearchToolParam, WebSearchContextSize, WebSearchFilters, + WebSearchToolParam, WebSearchUserLocation, }; diff --git a/crates/agentic-server-core/src/types/tools/params.rs b/crates/agentic-server-core/src/types/tools/params.rs index b162507d..59c75edd 100644 --- a/crates/agentic-server-core/src/types/tools/params.rs +++ b/crates/agentic-server-core/src/types/tools/params.rs @@ -80,6 +80,8 @@ impl std::fmt::Display for NonEmptyToolName { pub enum ResponsesTool { #[serde(rename = "function")] Function(FunctionToolParam), + #[serde(rename = "tool_search")] + ToolSearch(ToolSearchToolParam), #[serde(rename = "mcp")] Mcp(McpToolParam), #[serde( @@ -143,6 +145,35 @@ pub struct CustomToolParam { pub extra: HashMap, } +/// Only client-executed tool search is part of the public gateway contract. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ToolSearchExecution { + #[default] + Client, + // TODO: Support `Server` execution type for gateway built-in tool +} + +/// Lifecycle status of a public tool-search call or output item. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ToolSearchStatus { + InProgress, + #[default] + Completed, + Incomplete, +} + +/// Parameters for a client-executed tool-search declaration. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolSearchToolParam { + pub execution: ToolSearchExecution, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parameters: Option>, +} + /// Parameters for a gateway MCP built-in tool declaration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpToolParam { @@ -257,6 +288,7 @@ impl ResponsesTool { pub fn original_type(&self) -> Option<&str> { match self { Self::Function(_) => Some("function"), + Self::ToolSearch(_) => Some("tool_search"), Self::Mcp(_) => Some("mcp"), Self::WebSearch(_) => Some("web_search_preview"), Self::FileSearch(_) => Some("file_search"), @@ -391,6 +423,106 @@ mod tests { assert_eq!(persisted["require_approval"], "never"); } + #[test] + fn responses_tool_search_declaration_round_trips_exactly() { + let declaration = serde_json::json!({ + "type": "tool_search", + "execution": "client", + "description": "Find a tool for the requested task", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"] + } + }); + + let tool: ResponsesTool = serde_json::from_value(declaration.clone()).expect("valid tool-search declaration"); + + assert_eq!(tool.original_type(), Some("tool_search")); + assert_eq!(tool.tool_type(), Some(crate::tool::ToolType::ToolSearch)); + assert!( + !tool.is_gateway_owned(), + "client-executed tool search must bypass gateway dispatch" + ); + assert_eq!( + serde_json::to_value(tool.to_function_tools()).unwrap(), + serde_json::json!([{ + "type": "function", + "name": "tool_search", + "description": "Find a tool for the requested task", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"] + }, + "strict": true + }]), + "the upstream-normalization boundary lowers tool search exactly once" + ); + assert_eq!(serde_json::to_value(tool).expect("tool serializes"), declaration); + } + + #[test] + fn responses_tool_search_declaration_omits_optional_fields() { + let declaration = serde_json::json!({ + "type": "tool_search", + "execution": "client" + }); + + let tool: ResponsesTool = serde_json::from_value(declaration.clone()).expect("valid minimal declaration"); + + tool.validate().expect("omitted optional fields are valid"); + assert_eq!(serde_json::to_value(tool).expect("tool serializes"), declaration); + } + + #[test] + fn responses_tool_search_declaration_rejects_invalid_wire_shapes() { + for declaration in [ + serde_json::json!({ + "type": "tool_search", + "description": "Missing execution", + "parameters": {"type": "object"} + }), + serde_json::json!({ + "type": "tool_search", + "execution": "server", + "description": "Hosted execution is excluded", + "parameters": {"type": "object"} + }), + serde_json::json!({ + "type": "tool_search", + "execution": "client", + "description": "Parameters must be an object", + "parameters": "not an object" + }), + ] { + assert!( + serde_json::from_value::(declaration).is_err(), + "invalid tool-search wire shape must not fall back to an unknown tool" + ); + } + } + + #[test] + fn responses_tool_search_declaration_accepts_model_facing_values_for_private_normalization() { + for (description, parameters) in [ + (" ", serde_json::json!({"type": "object"})), + ("Find a tool", serde_json::json!({})), + ("Find a tool", serde_json::json!({"type": "array"})), + ] { + let tool: ResponsesTool = serde_json::from_value(serde_json::json!({ + "type": "tool_search", + "execution": "client", + "description": description, + "parameters": parameters + })) + .expect("structurally valid declaration"); + + tool.validate() + .expect("typed public values are normalized only when building the private synthetic function"); + } + } + #[test] fn responses_tool_mcp_ignores_unknown_fields() { let tool = serde_json::from_value::(serde_json::json!({ diff --git a/crates/agentic-server-core/tests/cassettes/README.md b/crates/agentic-server-core/tests/cassettes/README.md index 40e96f29..3f59ce64 100644 --- a/crates/agentic-server-core/tests/cassettes/README.md +++ b/crates/agentic-server-core/tests/cassettes/README.md @@ -74,8 +74,9 @@ model requested by Codex 0.149.1. ``` --turns N Number of turns --output PATH Output YAML path ---mode MODE responses | conv | isolation | mixed | store_true_then_store_false (default: conv) +--mode MODE responses | messages | conv | isolation | mixed | store_true_then_store_false (default: conv) --stream / --no-stream Streaming or non-streaming (default: streaming) +--transport TRANSPORT http | websocket (default: http; WebSocket requires responses mode) --model NAME Model name sent in requests --no-store Set store=false --vllm URL vLLM upstream, e.g. http://localhost:8000 (responses mode only) @@ -83,6 +84,14 @@ model requested by Codex 0.149.1. --openai URL OpenAI upstream (default https://api.openai.com) --tools FILE JSON file containing a tools array (responses mode only) --tool-choice VALUE "auto", "none", "required", or JSON e.g. '{"type":"function","name":"foo"}' +--tool-choice-sequence FILE + JSON array with one tool_choice value per linear Responses turn +--tool-outputs FILE JSON object mapping called tool names to output strings +--tool-search-output-tools FILE + JSON array returned for a client tool-search call +--tools-after-search FILE + Effective tools after normalized direct-vLLM search +--manual-item-replay Replay accumulated items with store=false for direct-vLLM or gateway tool search --input-file FILE JSON string or item array for one HTTP Responses turn --max-output-tokens N max_output_tokens for Responses requests (default 1024; use 0 to omit) --proxy-port PORT Local proxy port (default 7070) @@ -199,6 +208,7 @@ turns: | `record_mcp_cassettes.sh` | Native MCP counter tool discovery and calls (streaming + non-streaming) | gateway and OpenAI reference | | `record_web_search_cassettes.sh` | Matching web-search calls (streaming + non-streaming) | gateway and OpenAI reference | | `record_dynamo_cassettes.sh` | Stateful two-turn and client-executed function tool call cassettes (streaming + non-streaming) | NVIDIA Dynamo frontend | +| `record_tool_search_cassettes.sh` | Four-turn mixed function/namespace client tool-search characterization; gateway blocking, HTTP/SSE, and WebSocket acceptance | OpenAI reference, direct vLLM, and gateway | ### Text-only (OpenAI) @@ -234,6 +244,55 @@ hydrated item history the gateway sends upstream), records it, and merges both i DYNAMO_URL=http://127.0.0.1:8000 MODEL=openai/gpt-oss-20b bash tests/cassettes/record_dynamo_cassettes.sh ``` +### Client tool search (OpenAI reference, direct vLLM, and gateway) + +The recorder captures four turns: a search call, a linked search output followed by one loaded ordinary function +call, its linked function call output followed by one loaded namespace-member call, then that call's linked output and +the final message. The initial catalog contains several deferred ordinary functions and a namespace with several +deferred members; the search output loads exactly one ordinary function and exactly one member of that namespace. +OpenAI and gateway use public `tool_search_call`/`tool_search_output` and public `{ namespace, name }` calls; direct +vLLM uses a private synthetic `tool_search` function and the flattened namespace-member name. Direct vLLM and gateway +blocking use `store: false` full-item replay; gateway SSE/WebSocket profiles use stored continuation. The private +projection is not the gateway-to-vLLM envelope. + +Each profile also records a four-entry `tool_choice` sequence so inference cannot repeat a prior call or emit another +call on the final turn. OpenAI uses `required`, selected `get_weather`, `auto`, then `none`: its function selector +cannot identify a function nested in a namespace, and the stored continuation intentionally omits the repeated `tools` +parameter required by `required`, so the third-turn prompt identifies `travel.get_timezone` and the characterization +strictly rejects a wrong or multiple call. Gateway profiles use `required`, selected `get_weather`, +selected public `travel.get_timezone`, then `none`, so the gateway can resolve the namespace member to its flattened +upstream identity. Direct vLLM selects the synthetic `tool_search`, `get_weather`, that flattened namespace member, +then `none`. The first public choice is `required` because the gateway's typed public `tool_choice` currently has no +`type: "tool_search"` selector; deferred declarations leave tool search as the only available choice on that turn. + +The complete set is exactly seven flows: OpenAI blocking/SSE, direct-vLLM blocking/SSE, and gateway blocking/SSE/WS. +HTTP uses the embedded proxy; WebSocket uses bounded direct capture. Recorded vLLM was `0.25.1`; the target must expose +the Responses API with a compatible function-call parser, but `/version` did not expose exact flags. Use a fresh gateway +database. After recording, the script runs `tool_search_characterization_test` as the single semantic validator for the +matrix. + +Start the gateway with this SQLite path absent: + +```bash +GATEWAY_PORT=3099 \ +DATABASE_URL=sqlite:///tmp/agentic_api_tool_search_matrix.db \ +V_API_BASE=http://127.0.0.1:8000 \ +V_API_KEY="" \ +V_MODEL=Qwen/Qwen3.6-35B-A3B-FP8 \ +./scripts/codex-start-gateway.sh +``` + +```bash +OPENAI_API_KEY=sk-... \ +TOOL_SEARCH_RECORD_SET=all \ +OPENAI_MODEL=gpt-5.6 \ +VLLM_URL=http://127.0.0.1:8000 \ +MODEL=Qwen/Qwen3.6-35B-A3B-FP8 \ +GATEWAY_URL=http://127.0.0.1:3099 \ +GATEWAY_MODEL=Qwen/Qwen3.6-35B-A3B-FP8 \ +bash crates/agentic-server-core/tests/cassettes/record_tool_search_cassettes.sh +``` + ### Web search (gateway and OpenAI) The default records both providers. Use `WEB_SEARCH_RECORD_SET=gateway` or diff --git a/crates/agentic-server-core/tests/cassettes/record_cassette.py b/crates/agentic-server-core/tests/cassettes/record_cassette.py index ff00d80c..cf64f585 100644 --- a/crates/agentic-server-core/tests/cassettes/record_cassette.py +++ b/crates/agentic-server-core/tests/cassettes/record_cassette.py @@ -33,6 +33,7 @@ """ import base64 +import copy import hashlib import json import logging @@ -55,7 +56,8 @@ from fastapi import FastAPI, Request, Response from fastapi.responses import JSONResponse, StreamingResponse from httpx import AsyncClient -from yaml import dump as yaml_dump, safe_load as yaml_load +from yaml import dump as yaml_dump +from yaml import safe_load as yaml_load logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("cassette_proxy") @@ -389,6 +391,7 @@ def __init__(self, url: str, headers: dict[str, str]) -> None: self.url = url self.headers = headers self.sock: socket.socket | ssl.SSLSocket | None = None + self._receive_buffer = bytearray() def __enter__(self) -> "WebSocketClient": parsed = urlparse(self.url) @@ -445,6 +448,10 @@ def __exit__(self, _exc_type: object, _exc: object, _tb: object) -> None: def _read_exact(self, size: int) -> bytes: assert self.sock is not None chunks = bytearray() + if self._receive_buffer: + buffered = min(size, len(self._receive_buffer)) + chunks.extend(self._receive_buffer[:buffered]) + del self._receive_buffer[:buffered] while len(chunks) < size: chunk = self.sock.recv(size - len(chunks)) if not chunk: @@ -460,7 +467,9 @@ def _read_http_response(self) -> str: if not chunk: raise EOFError("websocket closed during handshake") data.extend(chunk) - return data.decode("iso-8859-1") + header_end = data.index(b"\r\n\r\n") + 4 + self._receive_buffer.extend(data[header_end:]) + return data[:header_end].decode("iso-8859-1") def send_text(self, text: str) -> None: self._send_frame(0x1, text.encode("utf-8")) @@ -590,12 +599,16 @@ def _send_websocket( except json.JSONDecodeError: continue turn["response"]["sse"].append( + f"event: {event.get('type', '')}\n" f"data: {json.dumps(event, separators=(',', ':'))}\n" ) event_type = event.get("type") if event_type == "response.completed": response_data = event.get("response") break + if event_type == "response.failed": + response_data = event.get("response") + break if event_type == "error": response_data = event break @@ -654,14 +667,15 @@ def _inject_tools(body: dict, tools: list | None, tool_choice: Any) -> None: def _extract_tool_calls(response_data: dict | None) -> list[dict]: - """Extract client-owned function and custom tool calls from a response.""" + """Extract client-owned tool calls from a Responses output.""" if not response_data: return [] output = response_data.get("output", []) return [ item for item in output - if item.get("type") in {"function_call", "custom_tool_call"} + if item.get("type") + in {"function_call", "custom_tool_call", "tool_search_call"} ] @@ -669,42 +683,80 @@ def _build_tool_output_input( tool_calls: list[dict], tool_outputs: dict[str, str], user_prompt: str | None, + tool_search_tools: list[dict] | None = None, ) -> list[dict]: """Build tool output items followed by an optional user message. Args: - tool_calls: function_call or custom_tool_call items from the previous response. + tool_calls: client-owned call items from the previous response. tool_outputs: mapping of tool name -> fake JSON output string. user_prompt: the next user message (None for tool-output-only turns). + tool_search_tools: tools returned for public or synthetic tool search. Returns: A list suitable for the `input` field of the next request. """ input_items: list[dict] = [] for call in tool_calls: - call_id = call.get("call_id", "") + call_id = call.get("call_id") + if not isinstance(call_id, str) or not call_id.strip(): + raise ValueError( + f"{call.get('type', 'tool')} item has no non-empty call_id" + ) + + call_type = call.get("type") name = call.get("name", "") - output = tool_outputs.get( - name, json.dumps({"result": f"mock output for {name}"}) - ) + is_public_search = call_type == "tool_search_call" + is_synthetic_search = call_type == "function_call" and name == "tool_search" + if is_public_search or is_synthetic_search: + if tool_search_tools is None: + raise ValueError("a tool-search call requires --tool-search-output-tools") + if is_public_search: + input_items.append( + { + "type": "tool_search_output", + "call_id": call_id, + "execution": "client", + "status": "completed", + "tools": copy.deepcopy(tool_search_tools), + } + ) + else: + input_items.append( + { + "type": "function_call_output", + "call_id": call_id, + "output": json.dumps( + {"tools": tool_search_tools}, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ), + } + ) + continue + + if tool_search_tools is not None and name not in tool_outputs: + raise ValueError( + f"loaded function {name!r} requires an explicit output fixture" + ) input_items.append( { "type": ( "custom_tool_call_output" - if call.get("type") == "custom_tool_call" + if call_type == "custom_tool_call" else "function_call_output" ), "call_id": call_id, - "output": output, + "output": tool_outputs.get( + name, json.dumps({"result": f"mock output for {name}"}) + ), } ) + if user_prompt: input_items.append( - { - "type": "message", - "role": "user", - "content": user_prompt, - } + {"type": "message", "role": "user", "content": user_prompt} ) return input_items @@ -926,9 +978,13 @@ def run_responses( output_file: Path | None = None, tools: list | None = None, tool_choice: Any = None, + tool_choice_sequence: list[Any] | None = None, tool_outputs: dict[str, str] | None = None, + tool_search_output_tools: list[dict] | None = None, + tools_after_search: list | None = None, max_output_tokens: int | None = None, preset_input: str | list | None = None, + manual_item_replay: bool = False, ) -> None: response_ids: dict[int, str] = {} responses: dict[int, dict] = {} @@ -942,6 +998,8 @@ def run_responses( previous_response_id: str | None = None last_response: dict | None = None + search_tools_loaded = False + manual_history: list[dict] = [] for turn in range(1, turns + 1): if turn in branch_map: branch_from = branch_map[turn] @@ -960,11 +1018,27 @@ def run_responses( else: prompt = _prompt(f"Turn {turn}/{turns} — enter prompt: ") - # Inject matching function/custom output items before the user message. - pending_calls = _extract_tool_calls(last_response) if tool_outputs else [] - if pending_calls and tool_outputs: + # Inject matching client-tool outputs before the user message. + has_output_fixtures = ( + tool_outputs is not None or tool_search_output_tools is not None + ) + pending_calls = ( + _extract_tool_calls(last_response) if has_output_fixtures else [] + ) + if pending_calls: + search_tools_loaded = search_tools_loaded or any( + call.get("type") == "tool_search_call" + or ( + call.get("type") == "function_call" + and call.get("name") == "tool_search" + ) + for call in pending_calls + ) input_value = _build_tool_output_input( - pending_calls, tool_outputs, prompt if prompt else None + pending_calls, + tool_outputs or {}, + prompt if prompt else None, + tool_search_output_tools, ) click.echo( f" [injecting {len(pending_calls)} tool output(s) before user message]" @@ -972,12 +1046,32 @@ def run_responses( else: input_value = prompt + if manual_item_replay: + if isinstance(input_value, str): + new_input_items = [ + { + "type": "message", + "role": "user", + "content": input_value, + } + ] + elif isinstance(input_value, list): + new_input_items = copy.deepcopy(input_value) + else: + raise ValueError("manual item replay input must be a string or item array") + manual_history.extend(new_input_items) + input_value = copy.deepcopy(manual_history) + body: dict = {"model": model, "input": input_value, "stream": stream, "store": store} if max_output_tokens is not None: body["max_output_tokens"] = max_output_tokens if previous_response_id and store: body["previous_response_id"] = previous_response_id - _inject_tools(body, tools, tool_choice) + if tool_search_output_tools is not None: + body["parallel_tool_calls"] = False + effective_tools = tools_after_search if search_tools_loaded else tools + turn_tool_choice = tool_choice_sequence[turn - 1] if tool_choice_sequence is not None else tool_choice + _inject_tools(body, effective_tools, turn_tool_choice) response_data = _send( client, body, @@ -989,6 +1083,13 @@ def run_responses( output_file, ) response_id = response_data.get("id") if response_data else None + if manual_item_replay: + response_output = response_data.get("output") if response_data else None + if not isinstance(response_output, list): + raise ValueError( + "manual item replay requires every response to contain an output array" + ) + manual_history.extend(copy.deepcopy(response_output)) previous_response_id = response_id if store else None last_response = response_data if response_id: @@ -1136,6 +1237,14 @@ def run_responses( default=None, help='tool_choice value: "auto", "none", "required", or JSON e.g. \'{"type":"function","name":"foo"}\'.', ) +@click.option( + "--tool-choice-sequence", + "tool_choice_sequence_file", + metavar="FILE", + default=None, + type=click.Path(exists=True, dir_okay=False), + help="JSON array containing one tool_choice value per linear Responses turn.", +) @click.option( "--tool-outputs", "tool_outputs_file", @@ -1146,6 +1255,28 @@ def run_responses( "When provided, matching function_call_output or custom_tool_call_output items are injected " "between turns (required for OpenAI Responses API).", ) +@click.option( + "--tool-search-output-tools", + "tool_search_output_tools_file", + metavar="FILE", + default=None, + type=click.Path(exists=True, dir_okay=False), + help="JSON array returned by a client tool-search continuation.", +) +@click.option( + "--tools-after-search", + "tools_after_search_file", + metavar="FILE", + default=None, + type=click.Path(exists=True, dir_okay=False), + help="Effective JSON tools array used after search (required for direct vLLM characterization).", +) +@click.option( + "--manual-item-replay", + is_flag=True, + default=False, + help="Replay full accumulated item history with store=false (direct-vLLM or gateway tool-search).", +) @click.option( "--input-file", type=click.Path(exists=True, dir_okay=False), @@ -1174,7 +1305,11 @@ def main( gateway_url: str | None, tools_file: str | None, tool_choice_raw: str | None, + tool_choice_sequence_file: str | None, tool_outputs_file: str | None, + tool_search_output_tools_file: str | None, + tools_after_search_file: str | None, + manual_item_replay: bool, input_file: str | None, max_output_tokens: int, ) -> None: @@ -1190,6 +1325,59 @@ def main( (bf, branch_turn_number[i] if i < len(branch_turn_number) else None) for i, bf in enumerate(branch_from) ] + tool_search_recording = ( + tool_search_output_tools_file is not None + or tools_after_search_file is not None + ) + if tools_after_search_file and not tool_search_output_tools_file: + raise click.UsageError( + "--tools-after-search requires --tool-search-output-tools." + ) + if manual_item_replay and not tool_search_recording: + raise click.UsageError( + "--manual-item-replay requires --tool-search-output-tools." + ) + if tool_search_recording: + if mode != "responses": + raise click.UsageError( + "tool-search recorder fixtures require --mode responses." + ) + if turns != 4: + raise click.UsageError( + "tool-search recorder fixtures require exactly --turns 4." + ) + if branches: + raise click.UsageError( + "tool-search recorder fixtures do not support --branch-from." + ) + if no_store and not manual_item_replay: + raise click.UsageError( + "store=false tool-search characterization requires --manual-item-replay." + ) + if manual_item_replay and not no_store: + raise click.UsageError( + "--manual-item-replay requires --no-store." + ) + if input_file: + raise click.UsageError( + "tool-search recorder fixtures do not support --input-file." + ) + if not tools_file or not tool_outputs_file: + raise click.UsageError( + "tool-search recording requires --tools and --tool-outputs." + ) + if not tool_choice_sequence_file: + raise click.UsageError( + "tool-search recording requires --tool-choice-sequence." + ) + if tool_choice_raw and tool_choice_sequence_file: + raise click.UsageError( + "--tool-choice and --tool-choice-sequence are mutually exclusive." + ) + if tool_choice_sequence_file and (mode != "responses" or branches): + raise click.UsageError( + "--tool-choice-sequence requires linear --mode responses without branches." + ) backend_count = sum(bool(url) for url in (openai_url, vllm_url, gateway_url)) if backend_count > 1: raise click.UsageError("--openai, --vllm, and --gateway are mutually exclusive.") @@ -1224,6 +1412,15 @@ def main( else: tool_choice = stripped + tool_choice_sequence: list[Any] | None = None + if tool_choice_sequence_file: + with open(tool_choice_sequence_file, encoding="utf-8") as f: + tool_choice_sequence = json.load(f) + if not isinstance(tool_choice_sequence, list) or len(tool_choice_sequence) != turns: + raise click.UsageError( + "--tool-choice-sequence must contain one JSON value per turn." + ) + tool_outputs: dict[str, str] | None = None if tool_outputs_file: with open(tool_outputs_file, encoding="utf-8") as f: @@ -1232,6 +1429,43 @@ def main( raise click.UsageError("--tool-outputs file must contain a JSON object (name -> output string).") click.echo(f"Tool outputs: {list(tool_outputs.keys())}") + tool_search_output_tools: list[dict] | None = None + if tool_search_output_tools_file: + with open(tool_search_output_tools_file, encoding="utf-8") as f: + tool_search_output_tools = json.load(f) + if not isinstance(tool_search_output_tools, list) or not all( + isinstance(tool, dict) for tool in tool_search_output_tools + ): + raise click.UsageError( + "--tool-search-output-tools must contain a JSON array of objects." + ) + if not tool_search_output_tools: + raise click.UsageError( + "--tool-search-output-tools must contain at least one tool." + ) + + tools_after_search: list | None = None + if tools_after_search_file: + with open(tools_after_search_file, encoding="utf-8") as f: + tools_after_search = json.load(f) + if not isinstance(tools_after_search, list): + raise click.UsageError( + "--tools-after-search must contain a JSON array." + ) + + if tool_search_recording and vllm_url and tools_after_search is None: + raise click.UsageError( + "direct vLLM tool-search characterization requires --tools-after-search." + ) + if tool_search_recording and vllm_url and not manual_item_replay: + raise click.UsageError( + "direct vLLM tool-search characterization requires --manual-item-replay." + ) + if tool_search_recording and not (vllm_url or gateway_url) and manual_item_replay: + raise click.UsageError( + "--manual-item-replay is reserved for direct vLLM or gateway tool-search recording." + ) + if gateway_url: target = gateway_url.rstrip("/") headers = {} @@ -1281,11 +1515,15 @@ def main( target, headers, output_file, - tools, - tool_choice, - tool_outputs, - response_max_output_tokens, - preset_input, + tools=tools, + tool_choice=tool_choice, + tool_choice_sequence=tool_choice_sequence, + tool_outputs=tool_outputs, + tool_search_output_tools=tool_search_output_tools, + tools_after_search=tools_after_search, + max_output_tokens=response_max_output_tokens, + preset_input=preset_input, + manual_item_replay=manual_item_replay, ) else: click.echo(f"Proxy: {proxy_url} (requests go through here for recording)") @@ -1313,11 +1551,15 @@ def main( target, headers, output_file, - tools, - tool_choice, - tool_outputs, - response_max_output_tokens, - preset_input, + tools=tools, + tool_choice=tool_choice, + tool_choice_sequence=tool_choice_sequence, + tool_outputs=tool_outputs, + tool_search_output_tools=tool_search_output_tools, + tools_after_search=tools_after_search, + max_output_tokens=response_max_output_tokens, + preset_input=preset_input, + manual_item_replay=manual_item_replay, ) elif mode == "messages": run_messages( diff --git a/crates/agentic-server-core/tests/cassettes/record_tool_search_cassettes.sh b/crates/agentic-server-core/tests/cassettes/record_tool_search_cassettes.sh new file mode 100755 index 00000000..36b1cdd4 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/record_tool_search_cassettes.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# Records client tool-search characterization against OpenAI, direct vLLM, or +# the gateway blocking, HTTP/SSE, or WebSocket profiles. +# +# Usage from the repository root: +# TOOL_SEARCH_RECORD_SET=openai-reference OPENAI_API_KEY=sk-... \ +# bash crates/agentic-server-core/tests/cassettes/record_tool_search_cassettes.sh +# TOOL_SEARCH_RECORD_SET=direct-vllm VLLM_URL=http://localhost:8000 \ +# bash crates/agentic-server-core/tests/cassettes/record_tool_search_cassettes.sh +# TOOL_SEARCH_RECORD_SET=gateway-nonstreaming GATEWAY_URL=http://localhost:9000 \ +# bash crates/agentic-server-core/tests/cassettes/record_tool_search_cassettes.sh +# TOOL_SEARCH_RECORD_SET=gateway-streaming GATEWAY_URL=http://localhost:9000 \ +# bash crates/agentic-server-core/tests/cassettes/record_tool_search_cassettes.sh +# TOOL_SEARCH_RECORD_SET=gateway-websocket GATEWAY_URL=http://localhost:9000 \ +# bash crates/agentic-server-core/tests/cassettes/record_tool_search_cassettes.sh + +set -euo pipefail + +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BASE_DIR="$SCRIPTS_DIR/tool_search" +RETURNED_TOOLS="$BASE_DIR/returned_tools.json" +FUNCTION_OUTPUTS="$BASE_DIR/function_outputs.json" +PROMPTS="$BASE_DIR/prompts.txt" +OPENAI_TOOLS="$BASE_DIR/openai_tools.json" +VLLM_INITIAL_TOOLS="$BASE_DIR/vllm_initial_tools.json" +VLLM_NEXT_TOOLS="$BASE_DIR/vllm_tools_after_search.json" +OPENAI_TOOL_CHOICES="$BASE_DIR/openai_tool_choice_sequence.json" +GATEWAY_TOOL_CHOICES="$BASE_DIR/gateway_tool_choice_sequence.json" +VLLM_TOOL_CHOICES="$BASE_DIR/vllm_tool_choice_sequence.json" +OPENAI_MODEL="${OPENAI_MODEL:-gpt-5.6}" +VLLM_MODEL="${MODEL:-Qwen/Qwen3.6-35B-A3B-FP8}" +VLLM_URL="${VLLM_URL:-}" +GATEWAY_MODEL="${GATEWAY_MODEL:-${MODEL:-Qwen/Qwen3.6-35B-A3B-FP8}}" +GATEWAY_URL="${GATEWAY_URL:-}" +TOOL_SEARCH_RECORD_SET="${TOOL_SEARCH_RECORD_SET:-all}" + +model_slug() { + printf '%s' "$1" | tr '/: ' '---' +} + +record_scenario() { + local endpoint_flag="$1" + local endpoint="$2" + local model="$3" + local tools="$4" + local next_tools="$5" + local projection="$6" + local tool_choice_sequence="$7" + local filename="$8" + local recorder_args=("${@:9}") + local temporary_output + local next_tools_args=() + local continuation_args=() + + temporary_output="$(mktemp "$STAGING_DIR/.tool-search-cassette.XXXXXX")" + if [[ -n "$next_tools" ]]; then + next_tools_args=(--tools-after-search "$next_tools") + fi + if [[ "$projection" == "normalized" || "$projection" == "gateway-public" ]]; then + continuation_args=(--no-store --manual-item-replay) + fi + if ! python "$SCRIPTS_DIR/record_cassette.py" \ + --mode responses \ + --turns 4 \ + "${recorder_args[@]}" \ + --model "$model" \ + "$endpoint_flag" "$endpoint" \ + --tools "$tools" \ + --tool-choice-sequence "$tool_choice_sequence" \ + --tool-outputs "$FUNCTION_OUTPUTS" \ + --tool-search-output-tools "$RETURNED_TOOLS" \ + "${next_tools_args[@]}" \ + "${continuation_args[@]}" \ + --max-output-tokens 4096 \ + --output "$temporary_output" < "$PROMPTS" + then + rm -f -- "$temporary_output" + return 1 + fi + + mv -- "$temporary_output" "$STAGING_DIR/$filename" + RECORDED_FILES+=("$filename") + printf 'staged %s\n' "$filename" +} + +record_provider() { + local provider="$1" + local endpoint_flag="$2" + local endpoint="$3" + local model="$4" + local tools="$5" + local next_tools="$6" + local projection="$7" + local tool_choice_sequence="$8" + local prefix="$9" + local slug + + slug="$(model_slug "$model")" + printf 'Recording %s blocking tool-search characterization\n' "$provider" + record_scenario \ + "$endpoint_flag" "$endpoint" "$model" "$tools" "$next_tools" "$projection" \ + "$tool_choice_sequence" \ + "${prefix}-${slug}-nonstreaming.yaml" --no-stream + printf 'Recording %s streaming tool-search characterization\n' "$provider" + record_scenario \ + "$endpoint_flag" "$endpoint" "$model" "$tools" "$next_tools" "$projection" \ + "$tool_choice_sequence" \ + "${prefix}-${slug}-streaming.yaml" --stream +} + +case "$TOOL_SEARCH_RECORD_SET" in + openai-reference|openai|direct-vllm|vllm|gateway-nonstreaming|gateway-streaming|gateway-websocket|gateway|all) ;; + *) + printf 'ERROR: TOOL_SEARCH_RECORD_SET must be openai-reference, direct-vllm, gateway-nonstreaming, gateway-streaming, gateway-websocket, gateway, or all\n' >&2 + exit 1 + ;; +esac + +for required_file in \ + "$RETURNED_TOOLS" \ + "$FUNCTION_OUTPUTS" \ + "$PROMPTS" \ + "$OPENAI_TOOLS" \ + "$VLLM_INITIAL_TOOLS" \ + "$VLLM_NEXT_TOOLS" \ + "$OPENAI_TOOL_CHOICES" \ + "$GATEWAY_TOOL_CHOICES" \ + "$VLLM_TOOL_CHOICES" +do + if [[ ! -f "$required_file" ]]; then + printf 'ERROR: required fixture does not exist: %s\n' "$required_file" >&2 + exit 1 + fi +done + +if [[ "$TOOL_SEARCH_RECORD_SET" =~ ^(openai-reference|openai|all)$ ]] && [[ -z "${OPENAI_API_KEY:-}" ]]; then + printf 'ERROR: OPENAI_API_KEY is required for %s\n' "$TOOL_SEARCH_RECORD_SET" >&2 + exit 1 +fi + +if [[ "$TOOL_SEARCH_RECORD_SET" =~ ^(direct-vllm|vllm|all)$ ]] && [[ -z "$VLLM_URL" ]]; then + printf 'ERROR: VLLM_URL is required for %s\n' "$TOOL_SEARCH_RECORD_SET" >&2 + exit 1 +fi +if [[ "$TOOL_SEARCH_RECORD_SET" =~ ^(gateway-nonstreaming|gateway-streaming|gateway-websocket|gateway|all)$ ]] && [[ -z "$GATEWAY_URL" ]]; then + printf 'ERROR: GATEWAY_URL is required for %s\n' "$TOOL_SEARCH_RECORD_SET" >&2 + exit 1 +fi + +STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/agentic-tool-search-cassettes.XXXXXX")" +trap 'rm -rf -- "$STAGING_DIR"' EXIT +RECORDED_FILES=() +cp -a -- "$BASE_DIR/." "$STAGING_DIR/" + +if [[ "$TOOL_SEARCH_RECORD_SET" =~ ^(openai-reference|openai|all)$ ]]; then + record_provider \ + OpenAI --openai https://api.openai.com "$OPENAI_MODEL" \ + "$OPENAI_TOOLS" "" public-stored "$OPENAI_TOOL_CHOICES" tool-search-openai-reference +fi + +if [[ "$TOOL_SEARCH_RECORD_SET" =~ ^(gateway-nonstreaming|gateway|all)$ ]]; then + slug="$(model_slug "$GATEWAY_MODEL")" + printf 'Recording gateway blocking tool-search flow\n' + record_scenario \ + --gateway "$GATEWAY_URL" "$GATEWAY_MODEL" "$OPENAI_TOOLS" "" gateway-public \ + "$GATEWAY_TOOL_CHOICES" \ + "tool-search-gateway-${slug}-nonstreaming.yaml" --no-stream +fi + +if [[ "$TOOL_SEARCH_RECORD_SET" =~ ^(gateway-streaming|gateway|all)$ ]]; then + slug="$(model_slug "$GATEWAY_MODEL")" + printf 'Recording gateway HTTP/SSE tool-search flow\n' + record_scenario \ + --gateway "$GATEWAY_URL" "$GATEWAY_MODEL" "$OPENAI_TOOLS" "" public-stored \ + "$GATEWAY_TOOL_CHOICES" \ + "tool-search-gateway-${slug}-streaming.yaml" --stream +fi + +if [[ "$TOOL_SEARCH_RECORD_SET" =~ ^(gateway-websocket|gateway|all)$ ]]; then + slug="$(model_slug "$GATEWAY_MODEL")" + printf 'Recording gateway WebSocket tool-search flow\n' + record_scenario \ + --gateway "$GATEWAY_URL" "$GATEWAY_MODEL" "$OPENAI_TOOLS" "" public-stored \ + "$GATEWAY_TOOL_CHOICES" \ + "tool-search-gateway-${slug}-websocket.yaml" --stream --transport websocket +fi + +if [[ "$TOOL_SEARCH_RECORD_SET" =~ ^(direct-vllm|vllm|all)$ ]]; then + record_provider \ + direct-vLLM --vllm "$VLLM_URL" "$VLLM_MODEL" \ + "$VLLM_INITIAL_TOOLS" "$VLLM_NEXT_TOOLS" normalized "$VLLM_TOOL_CHOICES" tool-search-direct-vllm +fi + +printf 'Validating tool-search cassette matrix\n' +TOOL_SEARCH_CASSETTE_DIR="$STAGING_DIR" cargo test --manifest-path "$SCRIPTS_DIR/../../../../Cargo.toml" \ + -p agentic-server-core --test tool_search_characterization_test + +for filename in "${RECORDED_FILES[@]}"; do + chmod 664 "$STAGING_DIR/$filename" + mv -- "$STAGING_DIR/$filename" "$BASE_DIR/$filename" + printf 'recorded %s\n' "$BASE_DIR/$filename" +done diff --git a/crates/agentic-server-core/tests/cassettes/test_record_tool_search.py b/crates/agentic-server-core/tests/cassettes/test_record_tool_search.py new file mode 100644 index 00000000..2b2b32a5 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/test_record_tool_search.py @@ -0,0 +1,751 @@ +"""Focused offline tests for client tool-search cassette recording.""" + +import json +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from click.testing import CliRunner + +import record_cassette + + +RETURNED_TOOLS = [ + { + "type": "function", + "name": "get_weather", + "description": "Get the weather for a city.", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + "additionalProperties": False, + }, + "strict": True, + "defer_loading": True, + }, + { + "type": "namespace", + "name": "travel", + "description": "Travel tools.", + "tools": [ + { + "type": "function", + "name": "get_timezone", + "description": "Get the time zone for a city.", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + "additionalProperties": False, + }, + "strict": True, + "defer_loading": True, + } + ], + }, +] + +FLAT_TIMEZONE_NAME = "agentic_ns__travel__get_timezone" +OPENAI_TOOL_CHOICES = [ + "required", + {"type": "function", "name": "get_weather"}, + "auto", + "none", +] +GATEWAY_TOOL_CHOICES = [ + "required", + {"type": "function", "name": "get_weather"}, + {"type": "function", "namespace": "travel", "name": "get_timezone"}, + "none", +] +NORMALIZED_TOOL_CHOICES = [ + {"type": "function", "name": "tool_search"}, + {"type": "function", "name": "get_weather"}, + {"type": "function", "name": FLAT_TIMEZONE_NAME}, + "none", +] + + +class RecordToolSearchTests(unittest.TestCase): + def test_mixed_catalog_fixture_loads_one_function_and_one_namespace_member(self) -> None: + fixture_dir = Path(__file__).parent / "tool_search" + initial = json.loads((fixture_dir / "openai_tools.json").read_text(encoding="utf-8")) + returned = json.loads((fixture_dir / "returned_tools.json").read_text(encoding="utf-8")) + normalized_initial = json.loads( + (fixture_dir / "vllm_initial_tools.json").read_text(encoding="utf-8") + ) + normalized_loaded = json.loads( + (fixture_dir / "vllm_tools_after_search.json").read_text(encoding="utf-8") + ) + openai_choices = json.loads( + (fixture_dir / "openai_tool_choice_sequence.json").read_text(encoding="utf-8") + ) + gateway_choices = json.loads( + (fixture_dir / "gateway_tool_choice_sequence.json").read_text(encoding="utf-8") + ) + normalized_choices = json.loads( + (fixture_dir / "vllm_tool_choice_sequence.json").read_text(encoding="utf-8") + ) + + ordinary = [tool for tool in initial if tool["type"] == "function"] + namespaces = [tool for tool in initial if tool["type"] == "namespace"] + self.assertEqual( + [tool["name"] for tool in ordinary], + ["get_weather", "get_exchange_rate", "search_hotels"], + ) + self.assertTrue(all(tool["defer_loading"] is True for tool in ordinary)) + self.assertEqual(len(namespaces), 1) + self.assertEqual(namespaces[0]["name"], "travel") + self.assertEqual( + [member["name"] for member in namespaces[0]["tools"]], + ["get_timezone", "get_coordinates", "calculate_distance"], + ) + self.assertTrue( + all(member["defer_loading"] is True for member in namespaces[0]["tools"]) + ) + + self.assertEqual([tool["type"] for tool in returned], ["function", "namespace"]) + self.assertEqual(returned[0]["name"], "get_weather") + self.assertEqual(returned[1]["name"], "travel") + self.assertEqual([member["name"] for member in returned[1]["tools"]], ["get_timezone"]) + + self.assertEqual([tool["name"] for tool in normalized_initial], ["tool_search"]) + self.assertEqual( + [tool["name"] for tool in normalized_loaded], + ["tool_search", "get_weather", FLAT_TIMEZONE_NAME], + ) + normalized_names = {tool["name"] for tool in normalized_loaded} + self.assertTrue( + normalized_names.isdisjoint( + {"get_exchange_rate", "search_hotels", "get_coordinates", "calculate_distance"} + ) + ) + self.assertEqual(openai_choices, OPENAI_TOOL_CHOICES) + self.assertEqual(gateway_choices, GATEWAY_TOOL_CHOICES) + self.assertEqual(normalized_choices, NORMALIZED_TOOL_CHOICES) + + def test_gateway_cli_profile_accepts_public_store_false_manual_replay(self) -> None: + with tempfile.TemporaryDirectory() as directory: + directory_path = Path(directory) + tools = directory_path / "tools.json" + outputs = directory_path / "outputs.json" + returned = directory_path / "returned.json" + choices = directory_path / "choices.json" + capture = directory_path / "capture.yaml" + tools.write_text(json.dumps([{"type": "tool_search", "execution": "client"}]), encoding="utf-8") + outputs.write_text(json.dumps({"get_weather": "sunny"}), encoding="utf-8") + returned.write_text(json.dumps(RETURNED_TOOLS), encoding="utf-8") + choices.write_text(json.dumps(GATEWAY_TOOL_CHOICES), encoding="utf-8") + + with ( + mock.patch.object(record_cassette, "_start_proxy", return_value=object()), + mock.patch.object(record_cassette, "_stop_proxy"), + mock.patch.object(record_cassette, "run_responses") as run_responses, + ): + result = CliRunner().invoke( + record_cassette.main, + [ + "--mode", "responses", + "--turns", "4", + "--gateway", "http://gateway.test", + "--model", "test-model", + "--no-stream", + "--no-store", + "--manual-item-replay", + "--tools", str(tools), + "--tool-outputs", str(outputs), + "--tool-search-output-tools", str(returned), + "--tool-choice-sequence", str(choices), + "--output", str(capture), + ], + ) + + self.assertEqual(result.exit_code, 0, result.output) + self.assertIs(run_responses.call_args.kwargs["manual_item_replay"], True) + self.assertEqual(run_responses.call_args.kwargs["tool_choice_sequence"], GATEWAY_TOOL_CHOICES) + self.assertIs(run_responses.call_args.args[4], False, "gateway profile must set store=false") + + def test_gateway_websocket_cli_profile_accepts_stored_tool_search_flow(self) -> None: + with tempfile.TemporaryDirectory() as directory: + directory_path = Path(directory) + tools = directory_path / "tools.json" + outputs = directory_path / "outputs.json" + returned = directory_path / "returned.json" + choices = directory_path / "choices.json" + capture = directory_path / "capture.yaml" + tools.write_text(json.dumps([{"type": "tool_search", "execution": "client"}]), encoding="utf-8") + outputs.write_text(json.dumps({"get_weather": "sunny"}), encoding="utf-8") + returned.write_text(json.dumps(RETURNED_TOOLS), encoding="utf-8") + choices.write_text(json.dumps(GATEWAY_TOOL_CHOICES), encoding="utf-8") + + with ( + mock.patch.object(record_cassette, "_start_proxy") as start_proxy, + mock.patch.object(record_cassette, "run_responses") as run_responses, + ): + result = CliRunner().invoke( + record_cassette.main, + [ + "--mode", "responses", + "--turns", "4", + "--gateway", "http://gateway.test", + "--transport", "websocket", + "--model", "test-model", + "--stream", + "--tools", str(tools), + "--tool-outputs", str(outputs), + "--tool-search-output-tools", str(returned), + "--tool-choice-sequence", str(choices), + "--output", str(capture), + ], + ) + + self.assertEqual(result.exit_code, 0, result.output) + start_proxy.assert_not_called() + self.assertIs(run_responses.call_args.args[4], True) + self.assertEqual(run_responses.call_args.args[7], "websocket") + self.assertEqual(run_responses.call_args.kwargs["tool_choice_sequence"], GATEWAY_TOOL_CHOICES) + + def test_tool_search_cli_requires_one_tool_choice_per_turn(self) -> None: + with tempfile.TemporaryDirectory() as directory: + directory_path = Path(directory) + tools = directory_path / "tools.json" + outputs = directory_path / "outputs.json" + returned = directory_path / "returned.json" + short_choices = directory_path / "short-choices.json" + capture = directory_path / "capture.yaml" + tools.write_text(json.dumps([{"type": "tool_search", "execution": "client"}]), encoding="utf-8") + outputs.write_text(json.dumps({"get_weather": "sunny"}), encoding="utf-8") + returned.write_text(json.dumps(RETURNED_TOOLS), encoding="utf-8") + short_choices.write_text(json.dumps(GATEWAY_TOOL_CHOICES[:-1]), encoding="utf-8") + common_args = [ + "--mode", "responses", + "--turns", "4", + "--gateway", "http://gateway.test", + "--model", "test-model", + "--tools", str(tools), + "--tool-outputs", str(outputs), + "--tool-search-output-tools", str(returned), + "--output", str(capture), + ] + + missing = CliRunner().invoke(record_cassette.main, common_args) + short = CliRunner().invoke( + record_cassette.main, + [*common_args, "--tool-choice-sequence", str(short_choices)], + ) + + self.assertNotEqual(missing.exit_code, 0) + self.assertIn("requires --tool-choice-sequence", missing.output) + self.assertNotEqual(short.exit_code, 0) + self.assertIn("one JSON value per turn", short.output) + + def test_websocket_handshake_preserves_coalesced_first_frame(self) -> None: + class Socket: + def __init__(self) -> None: + self.chunks = [b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n\x81\x02ok"] + + def recv(self, _size: int) -> bytes: + return self.chunks.pop(0) if self.chunks else b"" + + client = record_cassette.WebSocketClient("ws://gateway.test", {}) + client.sock = Socket() + + response = client._read_http_response() + + self.assertTrue(response.endswith("\r\n\r\n")) + self.assertEqual(client.receive_text(), "ok") + + def test_websocket_recording_stops_on_response_failed(self) -> None: + failed = { + "type": "response.failed", + "response": { + "id": "resp_failed", + "status": "failed", + "error": {"code": "provider_failure", "message": "stopped"}, + }, + } + + class Socket: + def __init__(self) -> None: + self.messages = [json.dumps(failed), None] + + def __enter__(self) -> "Socket": + return self + + def __exit__(self, *_args: object) -> None: + return None + + def send_text(self, _text: str) -> None: + return None + + def receive_text(self) -> str | None: + return self.messages.pop(0) + + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "capture.yaml" + with ( + mock.patch.object(record_cassette, "WebSocketClient", return_value=Socket()), + mock.patch.object(record_cassette, "_append_turn") as append_turn, + ): + response = record_cassette._send_websocket( + {"model": "test", "input": "hello"}, + "http://gateway.test", + {}, + output, + ) + + self.assertEqual(response, failed["response"]) + turn = append_turn.call_args.args[1] + self.assertEqual(turn["response"]["status_code"], 101) + self.assertEqual(json.loads(turn["response"]["websocket"][0]), failed) + self.assertTrue(turn["response"]["sse"][0].startswith("event: response.failed\n")) + + def test_tool_search_outputs_use_public_and_synthetic_wire_shapes(self) -> None: + public = record_cassette._build_tool_output_input( + [{"type": "tool_search_call", "call_id": "call_public"}], + {}, + None, + RETURNED_TOOLS, + ) + synthetic = record_cassette._build_tool_output_input( + [ + { + "type": "function_call", + "name": "tool_search", + "call_id": "call_synthetic", + } + ], + {}, + None, + RETURNED_TOOLS, + ) + + self.assertEqual( + public, + [ + { + "type": "tool_search_output", + "call_id": "call_public", + "execution": "client", + "status": "completed", + "tools": RETURNED_TOOLS, + } + ], + ) + self.assertEqual(synthetic[0]["type"], "function_call_output") + self.assertEqual( + json.loads(synthetic[0]["output"]), {"tools": RETURNED_TOOLS} + ) + + def test_tool_continuations_reject_empty_ids_and_missing_search_tools(self) -> None: + for call in ( + {"type": "tool_search_call", "call_id": ""}, + {"type": "function_call", "name": "tool_search"}, + {"type": "function_call", "name": "get_weather", "call_id": " "}, + ): + with self.subTest(call=call), self.assertRaises(ValueError): + record_cassette._build_tool_output_input( + [call], {}, None, RETURNED_TOOLS + ) + + with self.assertRaises(ValueError): + record_cassette._build_tool_output_input( + [ + { + "type": "tool_search_call", + "call_id": "call_without_tools", + } + ], + {}, + None, + None, + ) + with self.assertRaisesRegex(ValueError, "explicit output fixture"): + record_cassette._build_tool_output_input( + [ + { + "type": "function_call", + "name": "get_weather", + "call_id": "call_without_output", + } + ], + {}, + None, + RETURNED_TOOLS, + ) + + def test_existing_function_and_custom_outputs_remain_supported(self) -> None: + continuation = record_cassette._build_tool_output_input( + [ + { + "type": "function_call", + "call_id": "call_function", + "name": "lookup", + }, + { + "type": "custom_tool_call", + "call_id": "call_custom", + "name": "raw_echo", + }, + ], + {"lookup": "function result", "raw_echo": "custom result"}, + "continue", + None, + ) + self.assertEqual( + [item["type"] for item in continuation], + ["function_call_output", "custom_tool_call_output", "message"], + ) + + def test_namespaced_and_flattened_calls_use_explicit_output_fixtures(self) -> None: + public = record_cassette._build_tool_output_input( + [ + { + "type": "function_call", + "namespace": "travel", + "name": "get_timezone", + "call_id": "call_public_timezone", + } + ], + {"get_timezone": "public time zone"}, + None, + RETURNED_TOOLS, + ) + normalized = record_cassette._build_tool_output_input( + [ + { + "type": "function_call", + "name": FLAT_TIMEZONE_NAME, + "call_id": "call_flat_timezone", + } + ], + {FLAT_TIMEZONE_NAME: "normalized time zone"}, + None, + RETURNED_TOOLS, + ) + + self.assertEqual(public[0]["output"], "public time zone") + self.assertEqual(normalized[0]["output"], "normalized time zone") + + def test_synthetic_manual_replay_switches_to_loaded_tools(self) -> None: + initial_tools = [{"type": "function", "name": "tool_search"}] + next_tools = initial_tools + [ + RETURNED_TOOLS[0], + { + **RETURNED_TOOLS[1]["tools"][0], + "name": FLAT_TIMEZONE_NAME, + "defer_loading": False, + }, + ] + responses = [ + { + "id": "resp_search", + "output": [ + { + "type": "function_call", + "name": "tool_search", + "call_id": "call_search", + "status": "completed", + "arguments": '{"query":"weather tool"}', + } + ], + }, + { + "id": "resp_weather", + "output": [ + { + "type": "function_call", + "name": "get_weather", + "call_id": "call_weather", + "status": "completed", + "arguments": '{"city":"Paris"}', + } + ], + }, + { + "id": "resp_timezone", + "output": [ + { + "type": "function_call", + "name": FLAT_TIMEZONE_NAME, + "call_id": "call_timezone", + "status": "completed", + "arguments": '{"city":"Paris"}', + } + ], + }, + {"id": "resp_final", "output": [{"type": "message"}]}, + ] + sent_bodies: list[dict] = [] + + def fake_send(_client: object, body: dict, *_args: object, **_kwargs: object) -> dict: + sent_bodies.append(body) + return responses[len(sent_bodies) - 1] + + with ( + mock.patch.object( + record_cassette, + "_prompt", + side_effect=["find tools", "call weather", "call timezone", "finish"], + ), + mock.patch.object(record_cassette, "_send", side_effect=fake_send), + ): + record_cassette.run_responses( + client=object(), + turns=4, + model="test-model", + stream=False, + store=False, + branches=[], + proxy_url="http://unused", + tools=initial_tools, + tool_choice_sequence=NORMALIZED_TOOL_CHOICES, + tool_outputs={ + "get_weather": '{"temperature_c":21}', + FLAT_TIMEZONE_NAME: '{"iana_timezone":"Europe/Paris"}', + }, + tool_search_output_tools=RETURNED_TOOLS, + tools_after_search=next_tools, + manual_item_replay=True, + ) + + self.assertEqual( + [body["tools"] for body in sent_bodies], + [initial_tools, next_tools, next_tools, next_tools], + ) + self.assertTrue(all(body["store"] is False for body in sent_bodies)) + self.assertTrue(all("previous_response_id" not in body for body in sent_bodies)) + self.assertEqual([body["tool_choice"] for body in sent_bodies], NORMALIZED_TOOL_CHOICES) + + turn_one_input = sent_bodies[0]["input"] + turn_two_input = sent_bodies[1]["input"] + turn_three_input = sent_bodies[2]["input"] + turn_four_input = sent_bodies[3]["input"] + self.assertEqual( + turn_one_input, + [{"type": "message", "role": "user", "content": "find tools"}], + ) + self.assertEqual(turn_two_input[: len(turn_one_input)], turn_one_input) + self.assertEqual(turn_two_input[1]["type"], "function_call") + self.assertEqual(turn_two_input[1]["call_id"], "call_search") + self.assertEqual(turn_two_input[2]["type"], "function_call_output") + self.assertEqual(turn_two_input[2]["call_id"], "call_search") + self.assertEqual(turn_three_input[: len(turn_two_input)], turn_two_input) + weather_call = next( + item + for item in turn_three_input[len(turn_two_input) :] + if item.get("type") == "function_call" + ) + weather_output = next( + item + for item in turn_three_input[len(turn_two_input) :] + if item.get("type") == "function_call_output" + ) + self.assertEqual(weather_call["call_id"], "call_weather") + self.assertEqual(weather_output["call_id"], "call_weather") + self.assertEqual(turn_four_input[: len(turn_three_input)], turn_three_input) + timezone_call = next( + item + for item in turn_four_input[len(turn_three_input) :] + if item.get("type") == "function_call" + ) + timezone_output = next( + item + for item in turn_four_input[len(turn_three_input) :] + if item.get("type") == "function_call_output" + ) + self.assertEqual(timezone_call["name"], FLAT_TIMEZONE_NAME) + self.assertEqual(timezone_call["call_id"], "call_timezone") + self.assertEqual(timezone_output["call_id"], "call_timezone") + self.assertTrue(all(body["parallel_tool_calls"] is False for body in sent_bodies)) + + def test_public_linear_responses_flow_keeps_public_top_level_tools(self) -> None: + public_tools = [ + {"type": "tool_search", "execution": "client"}, + { + "type": "function", + "name": "get_weather", + "defer_loading": True, + }, + RETURNED_TOOLS[1], + ] + responses = [ + { + "id": "resp_search", + "output": [ + { + "type": "tool_search_call", + "call_id": "call_search", + "execution": "client", + "status": "completed", + "arguments": {"query": "weather tool"}, + } + ], + }, + { + "id": "resp_weather", + "output": [ + { + "type": "function_call", + "name": "get_weather", + "call_id": "call_weather", + "status": "completed", + "arguments": '{"city":"Paris"}', + } + ], + }, + { + "id": "resp_timezone", + "output": [ + { + "type": "function_call", + "namespace": "travel", + "name": "get_timezone", + "call_id": "call_timezone", + "status": "completed", + "arguments": '{"city":"Paris"}', + } + ], + }, + {"id": "resp_final", "output": [{"type": "message"}]}, + ] + sent_bodies: list[dict] = [] + + def fake_send(_client: object, body: dict, *_args: object, **_kwargs: object) -> dict: + sent_bodies.append(body) + return responses[len(sent_bodies) - 1] + + with ( + mock.patch.object( + record_cassette, + "_prompt", + side_effect=["find tools", "call weather", "call timezone", "finish"], + ), + mock.patch.object(record_cassette, "_send", side_effect=fake_send), + ): + record_cassette.run_responses( + client=object(), + turns=4, + model="test-model", + stream=False, + store=True, + branches=[], + proxy_url="http://unused", + tools=public_tools, + tool_choice_sequence=GATEWAY_TOOL_CHOICES, + tool_outputs={ + "get_weather": '{"temperature_c":21}', + "get_timezone": '{"iana_timezone":"Europe/Paris"}', + }, + tool_search_output_tools=RETURNED_TOOLS, + ) + + self.assertEqual(sent_bodies[0]["tools"], public_tools) + self.assertEqual([body["tool_choice"] for body in sent_bodies], GATEWAY_TOOL_CHOICES) + self.assertNotIn("tools", sent_bodies[1]) + self.assertNotIn("tools", sent_bodies[2]) + self.assertNotIn("tools", sent_bodies[3]) + public_output = sent_bodies[1]["input"][0] + self.assertEqual(public_output["type"], "tool_search_output") + self.assertEqual(public_output["call_id"], "call_search") + self.assertEqual(public_output["execution"], "client") + self.assertEqual(public_output["status"], "completed") + self.assertEqual(public_output["tools"], RETURNED_TOOLS) + self.assertEqual(sent_bodies[2]["input"][0]["type"], "function_call_output") + self.assertEqual(sent_bodies[3]["input"][0]["type"], "function_call_output") + self.assertEqual(sent_bodies[3]["input"][0]["call_id"], "call_timezone") + self.assertTrue(all(body["parallel_tool_calls"] is False for body in sent_bodies)) + + def test_gateway_public_manual_replay_is_store_false_and_omits_tools_after_search(self) -> None: + public_tools = [ + {"type": "tool_search", "execution": "client"}, + {"type": "function", "name": "get_weather", "defer_loading": True}, + RETURNED_TOOLS[1], + ] + responses = [ + { + "id": "resp_search", + "output": [{ + "type": "tool_search_call", + "id": "tsc_search", + "call_id": "call_search", + "execution": "client", + "status": "completed", + "arguments": {"query": "weather"}, + }], + }, + { + "id": "resp_weather", + "output": [{ + "type": "function_call", + "id": "fc_weather", + "name": "get_weather", + "call_id": "call_weather", + "status": "completed", + "arguments": '{"city":"Paris"}', + }], + }, + { + "id": "resp_timezone", + "output": [{ + "type": "function_call", + "id": "fc_timezone", + "namespace": "travel", + "name": "get_timezone", + "call_id": "call_timezone", + "status": "completed", + "arguments": '{"city":"Paris"}', + }], + }, + {"id": "resp_final", "output": [{"type": "message"}]}, + ] + sent_bodies: list[dict] = [] + + def fake_send(_client: object, body: dict, *_args: object, **_kwargs: object) -> dict: + sent_bodies.append(body) + return responses[len(sent_bodies) - 1] + + with ( + mock.patch.object( + record_cassette, + "_prompt", + side_effect=["find", "call weather", "call timezone", "finish"], + ), + mock.patch.object(record_cassette, "_send", side_effect=fake_send), + ): + record_cassette.run_responses( + client=object(), + turns=4, + model="test-model", + stream=False, + store=False, + branches=[], + proxy_url="http://unused", + tools=public_tools, + tool_choice_sequence=GATEWAY_TOOL_CHOICES, + tool_outputs={"get_weather": "sunny", "get_timezone": "Europe/Paris"}, + tool_search_output_tools=RETURNED_TOOLS, + manual_item_replay=True, + ) + + self.assertTrue(all(body["store"] is False for body in sent_bodies)) + self.assertTrue(all("previous_response_id" not in body for body in sent_bodies)) + self.assertEqual(sent_bodies[0]["tools"], public_tools) + self.assertEqual([body["tool_choice"] for body in sent_bodies], GATEWAY_TOOL_CHOICES) + self.assertNotIn("tools", sent_bodies[1]) + self.assertNotIn("tools", sent_bodies[2]) + self.assertNotIn("tools", sent_bodies[3]) + self.assertEqual(sent_bodies[1]["input"][1]["type"], "tool_search_call") + self.assertEqual(sent_bodies[1]["input"][2]["type"], "tool_search_output") + self.assertEqual(sent_bodies[2]["input"][3]["type"], "message") + self.assertEqual(sent_bodies[2]["input"][4]["type"], "function_call") + self.assertEqual(sent_bodies[2]["input"][5]["type"], "function_call_output") + self.assertEqual(sent_bodies[3]["input"][7]["namespace"], "travel") + self.assertEqual(sent_bodies[3]["input"][7]["name"], "get_timezone") + self.assertEqual(sent_bodies[3]["input"][8]["type"], "function_call_output") + self.assertEqual(sent_bodies[3]["input"][9]["type"], "message") + +if __name__ == "__main__": + unittest.main() diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/function_outputs.json b/crates/agentic-server-core/tests/cassettes/tool_search/function_outputs.json new file mode 100644 index 00000000..fd66a5fa --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/function_outputs.json @@ -0,0 +1,5 @@ +{ + "get_weather": "{\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}", + "get_timezone": "{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}", + "agentic_ns__travel__get_timezone": "{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}" +} diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/gateway_tool_choice_sequence.json b/crates/agentic-server-core/tests/cassettes/tool_search/gateway_tool_choice_sequence.json new file mode 100644 index 00000000..60c1e1db --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/gateway_tool_choice_sequence.json @@ -0,0 +1,13 @@ +[ + "required", + { + "type": "function", + "name": "get_weather" + }, + { + "type": "function", + "namespace": "travel", + "name": "get_timezone" + }, + "none" +] diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/openai_tool_choice_sequence.json b/crates/agentic-server-core/tests/cassettes/tool_search/openai_tool_choice_sequence.json new file mode 100644 index 00000000..e54df17d --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/openai_tool_choice_sequence.json @@ -0,0 +1,9 @@ +[ + "required", + { + "type": "function", + "name": "get_weather" + }, + "auto", + "none" +] diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/openai_tools.json b/crates/agentic-server-core/tests/cassettes/tool_search/openai_tools.json new file mode 100644 index 00000000..aa15cee3 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/openai_tools.json @@ -0,0 +1,133 @@ +[ + { + "type": "tool_search", + "execution": "client", + "description": "Search the client tool catalog for tools that can satisfy the request.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "A concise description of the needed capabilities." + } + }, + "required": ["query"], + "additionalProperties": false + } + }, + { + "type": "function", + "name": "get_weather", + "description": "Get the current weather for a city", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + }, + { + "type": "function", + "name": "get_exchange_rate", + "description": "Get the exchange rate between two currencies", + "parameters": { + "type": "object", + "properties": { + "base": { + "type": "string" + }, + "quote": { + "type": "string" + } + }, + "required": ["base", "quote"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + }, + { + "type": "function", + "name": "search_hotels", + "description": "Search for hotels in a city", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + }, + { + "type": "namespace", + "name": "travel", + "description": "Travel location tools", + "tools": [ + { + "type": "function", + "name": "get_timezone", + "description": "Get the IANA time zone for a city", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + }, + { + "type": "function", + "name": "get_coordinates", + "description": "Get latitude and longitude for a city", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + }, + { + "type": "function", + "name": "calculate_distance", + "description": "Calculate the distance between two cities", + "parameters": { + "type": "object", + "properties": { + "origin": { + "type": "string" + }, + "destination": { + "type": "string" + } + }, + "required": ["origin", "destination"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + } + ] + } +] diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/prompts.txt b/crates/agentic-server-core/tests/cassettes/tool_search/prompts.txt new file mode 100644 index 00000000..91c71a36 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/prompts.txt @@ -0,0 +1,4 @@ +First call tool_search exactly once to find both a current-weather function and a travel time-zone function. Do not call any other tool yet. +Now call get_weather exactly once with {"city":"Paris"}. Do not call any other tool. +Now call the loaded travel namespace member get_timezone exactly once with {"city":"Paris"}. Do not call any other tool. +Use both function outputs and call no more tools. Reply with exactly PARIS_MIXED_TOOLS_OK. diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/returned_tools.json b/crates/agentic-server-core/tests/cassettes/tool_search/returned_tools.json new file mode 100644 index 00000000..ca847f7a --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/returned_tools.json @@ -0,0 +1,43 @@ +[ + { + "type": "function", + "name": "get_weather", + "description": "Get the current weather for a city", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + }, + { + "type": "namespace", + "name": "travel", + "description": "Travel location tools", + "tools": [ + { + "type": "function", + "name": "get_timezone", + "description": "Get the IANA time zone for a city", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + } + ] + } +] diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml new file mode 100644 index 00000000..59b51c17 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml @@ -0,0 +1,948 @@ +turns: +- filename: t1 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: false + tool_choice: + name: tool_search + type: function + tools: + - description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather — Get the current weather + for a city; get_exchange_rate — Get the exchange rate between two currencies; + search_hotels — Search for hotels in a city; travel — Travel location tools.' + name: tool_search + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + background: false + created_at: 1787714225 + frequency_penalty: 0.0 + id: resp_80050f2a83c18eae + incomplete_details: null + input_messages: null + instructions: null + kv_transfer_params: null + max_output_tokens: 4096 + max_tool_calls: null + metadata: null + model: Qwen/Qwen3.6-35B-A3B-FP8 + object: response + output: + - content: + - text: "The user wants me to call `tool_search` exactly once to find:\n1.\ + \ A current-weather function\n2. A travel time-zone function\n\nI need\ + \ to construct a query that asks for both of these capabilities in a single\ + \ search.\nLooking at the available tools in the description:\n- get_weather\ + \ — Get the current weather for a city\n- get_exchange_rate — Get the\ + \ exchange rate between two currencies\n- search_hotels — Search for hotels\ + \ in a city\n- travel — Travel location tools.\n\nI will formulate the\ + \ query to request a tool for current weather and a tool for travel time\ + \ zone.\nQuery: \"a function to get current weather and a function to\ + \ get travel time zones\"\nThen I will call `tool_search` with this query.\n\ + Let's double check the constraints: \"First call tool_search exactly once...\ + \ Do not call any other tool yet.\"\nProceed. \nI will construct the query\ + \ carefully.\n`tool_search(query=\"a function to get current weather and\ + \ a function for travel time zones\")`\nDone. \nWait, let's verify the\ + \ exact description. \"travel — Travel location tools.\" Time zone might\ + \ be part of travel tools.\nI'll just ask for \"current weather and travel\ + \ time zone capabilities\".\nReady. \nCalling tool_search. \nNote: The\ + \ prompt says \"First call tool_search exactly once...\". So I will output\ + \ the tool call now. \nLet's make sure the parameters match: `{\"query\"\ + : \"string\"}`.\nQuery: \"a current-weather function and a travel time-zone\ + \ function\"\nProceeds. \nOutput matches format.✅\n" + type: reasoning_text + encrypted_content: null + id: rs_9e94d568ce77b3e8 + status: null + summary: [] + type: reasoning + - arguments: '{"query": "a current-weather function and a travel time-zone function"}' + call_id: chatcmpl-tool-bfd01690188cd378 + caller: null + id: fc_a54b71918970eabe + name: tool_search + namespace: null + status: completed + type: function_call + output_messages: null + parallel_tool_calls: false + presence_penalty: 0.0 + previous_response_id: null + prompt: null + reasoning: null + service_tier: auto + status: completed + temperature: 1.0 + text: null + tool_choice: + name: tool_search + type: function + tools: + - allowed_callers: null + defer_loading: null + description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather — Get the current weather + for a city; get_exchange_rate — Get the exchange rate between two currencies; + search_hotels — Search for hotels in a city; travel — Travel location tools.' + name: tool_search + output_schema: null + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + top_logprobs: null + top_p: 0.95 + truncation: disabled + usage: + input_tokens: 394 + input_tokens_details: + cached_tokens: 0 + cached_tokens_per_turn: [] + input_tokens_per_turn: [] + output_tokens: 376 + output_tokens_details: + output_tokens_per_turn: [] + reasoning_tokens: 0 + tool_output_tokens: 0 + tool_output_tokens_per_turn: [] + total_tokens: 770 + user: null + headers: + content-type: application/json + status_code: 200 +- filename: t2 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + - content: + - text: "The user wants me to call `tool_search` exactly once to find:\n1.\ + \ A current-weather function\n2. A travel time-zone function\n\nI need\ + \ to construct a query that asks for both of these capabilities in a single\ + \ search.\nLooking at the available tools in the description:\n- get_weather\ + \ — Get the current weather for a city\n- get_exchange_rate — Get the\ + \ exchange rate between two currencies\n- search_hotels — Search for hotels\ + \ in a city\n- travel — Travel location tools.\n\nI will formulate the\ + \ query to request a tool for current weather and a tool for travel time\ + \ zone.\nQuery: \"a function to get current weather and a function to\ + \ get travel time zones\"\nThen I will call `tool_search` with this query.\n\ + Let's double check the constraints: \"First call tool_search exactly once...\ + \ Do not call any other tool yet.\"\nProceed. \nI will construct the query\ + \ carefully.\n`tool_search(query=\"a function to get current weather and\ + \ a function for travel time zones\")`\nDone. \nWait, let's verify the\ + \ exact description. \"travel — Travel location tools.\" Time zone might\ + \ be part of travel tools.\nI'll just ask for \"current weather and travel\ + \ time zone capabilities\".\nReady. \nCalling tool_search. \nNote: The\ + \ prompt says \"First call tool_search exactly once...\". So I will output\ + \ the tool call now. \nLet's make sure the parameters match: `{\"query\"\ + : \"string\"}`.\nQuery: \"a current-weather function and a travel time-zone\ + \ function\"\nProceeds. \nOutput matches format.✅\n" + type: reasoning_text + encrypted_content: null + id: rs_9e94d568ce77b3e8 + status: null + summary: [] + type: reasoning + - arguments: '{"query": "a current-weather function and a travel time-zone function"}' + call_id: chatcmpl-tool-bfd01690188cd378 + caller: null + id: fc_a54b71918970eabe + name: tool_search + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-bfd01690188cd378 + output: '{"tools":[{"defer_loading":true,"description":"Get the current weather + for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}]}' + type: function_call_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: false + tool_choice: + name: get_weather + type: function + tools: + - description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_exchange_rate — Get the exchange + rate between two currencies; search_hotels — Search for hotels in a city; + travel — Travel location tools.' + name: tool_search + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + - description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Get the IANA time zone for a city + name: agentic_ns__travel__get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + background: false + created_at: 1787714227 + frequency_penalty: 0.0 + id: resp_855b120985e573ea + incomplete_details: null + input_messages: null + instructions: null + kv_transfer_params: null + max_output_tokens: 4096 + max_tool_calls: null + metadata: null + model: Qwen/Qwen3.6-35B-A3B-FP8 + object: response + output: + - content: + - text: 'The user wants me to call the `get_weather` function with the argument + `{"city": "Paris"}`. + + I should not call any other tool. + + I have found the `get_weather` function in the previous step. + + I will proceed with calling `get_weather(city="Paris")`. + + ' + type: reasoning_text + encrypted_content: null + id: rs_8b055887860d4070 + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-b8e069e468543530 + caller: null + id: fc_92d8c37016c26805 + name: get_weather + namespace: null + status: completed + type: function_call + output_messages: null + parallel_tool_calls: false + presence_penalty: 0.0 + previous_response_id: null + prompt: null + reasoning: null + service_tier: auto + status: completed + temperature: 1.0 + text: null + tool_choice: + name: get_weather + type: function + tools: + - allowed_callers: null + defer_loading: null + description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_exchange_rate — Get the exchange + rate between two currencies; search_hotels — Search for hotels in a city; + travel — Travel location tools.' + name: tool_search + output_schema: null + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + - allowed_callers: null + defer_loading: null + description: Get the current weather for a city + name: get_weather + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - allowed_callers: null + defer_loading: null + description: Get the IANA time zone for a city + name: agentic_ns__travel__get_timezone + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + top_logprobs: null + top_p: 0.95 + truncation: disabled + usage: + input_tokens: 778 + input_tokens_details: + cached_tokens: 0 + cached_tokens_per_turn: [] + input_tokens_per_turn: [] + output_tokens: 94 + output_tokens_details: + output_tokens_per_turn: [] + reasoning_tokens: 0 + tool_output_tokens: 0 + tool_output_tokens_per_turn: [] + total_tokens: 872 + user: null + headers: + content-type: application/json + status_code: 200 +- filename: t3 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + - content: + - text: "The user wants me to call `tool_search` exactly once to find:\n1.\ + \ A current-weather function\n2. A travel time-zone function\n\nI need\ + \ to construct a query that asks for both of these capabilities in a single\ + \ search.\nLooking at the available tools in the description:\n- get_weather\ + \ — Get the current weather for a city\n- get_exchange_rate — Get the\ + \ exchange rate between two currencies\n- search_hotels — Search for hotels\ + \ in a city\n- travel — Travel location tools.\n\nI will formulate the\ + \ query to request a tool for current weather and a tool for travel time\ + \ zone.\nQuery: \"a function to get current weather and a function to\ + \ get travel time zones\"\nThen I will call `tool_search` with this query.\n\ + Let's double check the constraints: \"First call tool_search exactly once...\ + \ Do not call any other tool yet.\"\nProceed. \nI will construct the query\ + \ carefully.\n`tool_search(query=\"a function to get current weather and\ + \ a function for travel time zones\")`\nDone. \nWait, let's verify the\ + \ exact description. \"travel — Travel location tools.\" Time zone might\ + \ be part of travel tools.\nI'll just ask for \"current weather and travel\ + \ time zone capabilities\".\nReady. \nCalling tool_search. \nNote: The\ + \ prompt says \"First call tool_search exactly once...\". So I will output\ + \ the tool call now. \nLet's make sure the parameters match: `{\"query\"\ + : \"string\"}`.\nQuery: \"a current-weather function and a travel time-zone\ + \ function\"\nProceeds. \nOutput matches format.✅\n" + type: reasoning_text + encrypted_content: null + id: rs_9e94d568ce77b3e8 + status: null + summary: [] + type: reasoning + - arguments: '{"query": "a current-weather function and a travel time-zone function"}' + call_id: chatcmpl-tool-bfd01690188cd378 + caller: null + id: fc_a54b71918970eabe + name: tool_search + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-bfd01690188cd378 + output: '{"tools":[{"defer_loading":true,"description":"Get the current weather + for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}]}' + type: function_call_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + - content: + - text: 'The user wants me to call the `get_weather` function with the argument + `{"city": "Paris"}`. + + I should not call any other tool. + + I have found the `get_weather` function in the previous step. + + I will proceed with calling `get_weather(city="Paris")`. + + ' + type: reasoning_text + encrypted_content: null + id: rs_8b055887860d4070 + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-b8e069e468543530 + caller: null + id: fc_92d8c37016c26805 + name: get_weather + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-b8e069e468543530 + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: false + tool_choice: + name: agentic_ns__travel__get_timezone + type: function + tools: + - description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_exchange_rate — Get the exchange + rate between two currencies; search_hotels — Search for hotels in a city; + travel — Travel location tools.' + name: tool_search + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + - description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Get the IANA time zone for a city + name: agentic_ns__travel__get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + background: false + created_at: 1787714227 + frequency_penalty: 0.0 + id: resp_a4bbf113c68a2596 + incomplete_details: null + input_messages: null + instructions: null + kv_transfer_params: null + max_output_tokens: 4096 + max_tool_calls: null + metadata: null + model: Qwen/Qwen3.6-35B-A3B-FP8 + object: response + output: + - content: + - text: 'The user wants me to call the `get_timezone` function, which is part + of the `travel` namespace, with the parameter `{"city": "Paris"}`. + + I should use the `agentic_ns__travel__get_timezone` function. + + The parameter is `city: "Paris"`. + + I will make exactly one tool call. + + No other tools should be called. + + ' + type: reasoning_text + encrypted_content: null + id: rs_a5943cde7bd96f68 + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-9f31ea6d5523f168 + caller: null + id: fc_aed60407fd0eaf05 + name: agentic_ns__travel__get_timezone + namespace: null + status: completed + type: function_call + output_messages: null + parallel_tool_calls: false + presence_penalty: 0.0 + previous_response_id: null + prompt: null + reasoning: null + service_tier: auto + status: completed + temperature: 1.0 + text: null + tool_choice: + name: agentic_ns__travel__get_timezone + type: function + tools: + - allowed_callers: null + defer_loading: null + description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_exchange_rate — Get the exchange + rate between two currencies; search_hotels — Search for hotels in a city; + travel — Travel location tools.' + name: tool_search + output_schema: null + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + - allowed_callers: null + defer_loading: null + description: Get the current weather for a city + name: get_weather + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - allowed_callers: null + defer_loading: null + description: Get the IANA time zone for a city + name: agentic_ns__travel__get_timezone + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + top_logprobs: null + top_p: 0.95 + truncation: disabled + usage: + input_tokens: 862 + input_tokens_details: + cached_tokens: 0 + cached_tokens_per_turn: [] + input_tokens_per_turn: [] + output_tokens: 115 + output_tokens_details: + output_tokens_per_turn: [] + reasoning_tokens: 0 + tool_output_tokens: 0 + tool_output_tokens_per_turn: [] + total_tokens: 977 + user: null + headers: + content-type: application/json + status_code: 200 +- filename: t4 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + - content: + - text: "The user wants me to call `tool_search` exactly once to find:\n1.\ + \ A current-weather function\n2. A travel time-zone function\n\nI need\ + \ to construct a query that asks for both of these capabilities in a single\ + \ search.\nLooking at the available tools in the description:\n- get_weather\ + \ — Get the current weather for a city\n- get_exchange_rate — Get the\ + \ exchange rate between two currencies\n- search_hotels — Search for hotels\ + \ in a city\n- travel — Travel location tools.\n\nI will formulate the\ + \ query to request a tool for current weather and a tool for travel time\ + \ zone.\nQuery: \"a function to get current weather and a function to\ + \ get travel time zones\"\nThen I will call `tool_search` with this query.\n\ + Let's double check the constraints: \"First call tool_search exactly once...\ + \ Do not call any other tool yet.\"\nProceed. \nI will construct the query\ + \ carefully.\n`tool_search(query=\"a function to get current weather and\ + \ a function for travel time zones\")`\nDone. \nWait, let's verify the\ + \ exact description. \"travel — Travel location tools.\" Time zone might\ + \ be part of travel tools.\nI'll just ask for \"current weather and travel\ + \ time zone capabilities\".\nReady. \nCalling tool_search. \nNote: The\ + \ prompt says \"First call tool_search exactly once...\". So I will output\ + \ the tool call now. \nLet's make sure the parameters match: `{\"query\"\ + : \"string\"}`.\nQuery: \"a current-weather function and a travel time-zone\ + \ function\"\nProceeds. \nOutput matches format.✅\n" + type: reasoning_text + encrypted_content: null + id: rs_9e94d568ce77b3e8 + status: null + summary: [] + type: reasoning + - arguments: '{"query": "a current-weather function and a travel time-zone function"}' + call_id: chatcmpl-tool-bfd01690188cd378 + caller: null + id: fc_a54b71918970eabe + name: tool_search + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-bfd01690188cd378 + output: '{"tools":[{"defer_loading":true,"description":"Get the current weather + for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}]}' + type: function_call_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + - content: + - text: 'The user wants me to call the `get_weather` function with the argument + `{"city": "Paris"}`. + + I should not call any other tool. + + I have found the `get_weather` function in the previous step. + + I will proceed with calling `get_weather(city="Paris")`. + + ' + type: reasoning_text + encrypted_content: null + id: rs_8b055887860d4070 + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-b8e069e468543530 + caller: null + id: fc_92d8c37016c26805 + name: get_weather + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-b8e069e468543530 + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + - content: + - text: 'The user wants me to call the `get_timezone` function, which is part + of the `travel` namespace, with the parameter `{"city": "Paris"}`. + + I should use the `agentic_ns__travel__get_timezone` function. + + The parameter is `city: "Paris"`. + + I will make exactly one tool call. + + No other tools should be called. + + ' + type: reasoning_text + encrypted_content: null + id: rs_a5943cde7bd96f68 + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-9f31ea6d5523f168 + caller: null + id: fc_aed60407fd0eaf05 + name: agentic_ns__travel__get_timezone + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-9f31ea6d5523f168 + output: '{"city":"Paris","iana_timezone":"Europe/Paris"}' + type: function_call_output + - content: Use both function outputs and call no more tools. Reply with exactly + PARIS_MIXED_TOOLS_OK. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: false + tool_choice: none + tools: + - description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_exchange_rate — Get the exchange + rate between two currencies; search_hotels — Search for hotels in a city; + travel — Travel location tools.' + name: tool_search + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + - description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Get the IANA time zone for a city + name: agentic_ns__travel__get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + background: false + created_at: 1787714228 + frequency_penalty: 0.0 + id: resp_937efeb59f115cd7 + incomplete_details: null + input_messages: null + instructions: null + kv_transfer_params: null + max_output_tokens: 4096 + max_tool_calls: null + metadata: null + model: Qwen/Qwen3.6-35B-A3B-FP8 + object: response + output: + - content: + - text: "The user wants a final output based on the previous tool calls.\n\ + I called `get_weather` for Paris and got the weather.\nI called `get_timezone`\ + \ (via `travel`) for Paris and got the timezone.\nThe instruction is to\ + \ reply with exactly \"PARIS_MIXED_TOOLS_OK\".\nNo more tools should be\ + \ called.\n\nSteps:\n1. Check the outputs from previous steps.\n *\ + \ Weather: Clear, 21C.\n * Timezone: Europe/Paris.\n2. Formulate\ + \ the response.\n3. The response must be exactly \"PARIS_MIXED_TOOLS_OK\"\ + .\n\nConstraint Check:\n* Call no more tools? Yes.\n* Reply with exactly\ + \ PARIS_MIXED_TOOLS_OK? Yes.\n\nFinal Output Generation:\nPARIS_MIXED_TOOLS_OK\n" + type: reasoning_text + encrypted_content: null + id: rs_99e8c06239fd895c + status: null + summary: [] + type: reasoning + - content: + - annotations: [] + logprobs: null + text: ' + + + PARIS_MIXED_TOOLS_OK' + type: output_text + id: msg_93620139bdc851d1 + phase: null + role: assistant + status: completed + type: message + output_messages: null + parallel_tool_calls: false + presence_penalty: 0.0 + previous_response_id: null + prompt: null + reasoning: null + service_tier: auto + status: completed + temperature: 1.0 + text: null + tool_choice: none + tools: + - allowed_callers: null + defer_loading: null + description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_exchange_rate — Get the exchange + rate between two currencies; search_hotels — Search for hotels in a city; + travel — Travel location tools.' + name: tool_search + output_schema: null + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + - allowed_callers: null + defer_loading: null + description: Get the current weather for a city + name: get_weather + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - allowed_callers: null + defer_loading: null + description: Get the IANA time zone for a city + name: agentic_ns__travel__get_timezone + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + top_logprobs: null + top_p: 0.95 + truncation: disabled + usage: + input_tokens: 407 + input_tokens_details: + cached_tokens: 0 + cached_tokens_per_turn: [] + input_tokens_per_turn: [] + output_tokens: 189 + output_tokens_details: + output_tokens_per_turn: [] + reasoning_tokens: 0 + tool_output_tokens: 0 + tool_output_tokens_per_turn: [] + total_tokens: 596 + user: null + headers: + content-type: application/json + status_code: 200 diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml new file mode 100644 index 00000000..3d53cfe4 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml @@ -0,0 +1,5254 @@ +turns: +- filename: t1 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: true + tool_choice: + name: tool_search + type: function + tools: + - description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather — Get the current weather + for a city; get_exchange_rate — Get the exchange rate between two currencies; + search_hotels — Search for hotels in a city; travel — Travel location tools.' + name: tool_search + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"response":{"id":"resp_b003f8b489a32767","created_at":1787714231,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":{"name":"tool_search","type":"function"},"tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_weather — Get the current weather for a city; get_exchange_rate + — Get the exchange rate between two currencies; search_hotels — Search for hotels + in a city; travel — Travel location tools.","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","text":null,"top_logprobs":null,"truncation":"disabled","usage":null,"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":0,"type":"response.created"} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"response":{"id":"resp_b003f8b489a32767","created_at":1787714231,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":{"name":"tool_search","type":"function"},"tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_weather — Get the current weather for a city; get_exchange_rate + — Get the exchange rate between two currencies; search_hotels — Search for hotels + in a city; travel — Travel location tools.","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","text":null,"top_logprobs":null,"truncation":"disabled","usage":null,"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":1,"type":"response.in_progress"} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"item":{"id":"88b2decbae922433","summary":[],"type":"reasoning","content":null,"encrypted_content":null,"status":"in_progress"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} + + ' + - ' + + ' + - 'event: response.reasoning_part.added + + ' + - 'data: {"content_index":0,"item_id":"88b2decbae922433","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"The","item_id":"88b2decbae922433","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" user wants me","item_id":"88b2decbae922433","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" to call","item_id":"88b2decbae922433","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `tool_search","item_id":"88b2decbae922433","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"` exactly once","item_id":"88b2decbae922433","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".","item_id":"88b2decbae922433","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nThe query","item_id":"88b2decbae922433","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" needs","item_id":"88b2decbae922433","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" to find two","item_id":"88b2decbae922433","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" specific","item_id":"88b2decbae922433","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" capabilities:\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"1. A","item_id":"88b2decbae922433","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" current-weather","item_id":"88b2decbae922433","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" function.\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"2. A","item_id":"88b2decbae922433","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" travel time-zone","item_id":"88b2decbae922433","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" function.\n\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"I will formulate","item_id":"88b2decbae922433","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the query","item_id":"88b2decbae922433","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" to include","item_id":"88b2decbae922433","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" both requests","item_id":"88b2decbae922433","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\nQuery","item_id":"88b2decbae922433","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": \"current","item_id":"88b2decbae922433","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" weather function and","item_id":"88b2decbae922433","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" travel time-zone","item_id":"88b2decbae922433","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" function\"\n\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Then","item_id":"88b2decbae922433","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" I will call","item_id":"88b2decbae922433","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `","item_id":"88b2decbae922433","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"tool_search`.","item_id":"88b2decbae922433","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nI","item_id":"88b2decbae922433","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" must","item_id":"88b2decbae922433","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" not call any","item_id":"88b2decbae922433","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" other tool","item_id":"88b2decbae922433","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".","item_id":"88b2decbae922433","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"The available","item_id":"88b2decbae922433","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" tools in","item_id":"88b2decbae922433","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the description are","item_id":"88b2decbae922433","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": get","item_id":"88b2decbae922433","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_weather, get","item_id":"88b2decbae922433","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_exchange_rate,","item_id":"88b2decbae922433","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" search_hotels","item_id":"88b2decbae922433","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":", travel.","item_id":"88b2decbae922433","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n\"","item_id":"88b2decbae922433","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"get_weather\"","item_id":"88b2decbae922433","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" fits","item_id":"88b2decbae922433","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" \"","item_id":"88b2decbae922433","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"current-","item_id":"88b2decbae922433","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"weather function","item_id":"88b2decbae922433","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\".\n\"","item_id":"88b2decbae922433","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"travel\" fits","item_id":"88b2decbae922433","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" \"travel time","item_id":"88b2decbae922433","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"-zone function\"","item_id":"88b2decbae922433","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" (or similar","item_id":"88b2decbae922433","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" travel","item_id":"88b2decbae922433","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" capability","item_id":"88b2decbae922433","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":").\n\nI","item_id":"88b2decbae922433","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" will construct the","item_id":"88b2decbae922433","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" tool call","item_id":"88b2decbae922433","output_index":0,"sequence_number":63,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" now","item_id":"88b2decbae922433","output_index":0,"sequence_number":64,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\nTool","item_id":"88b2decbae922433","output_index":0,"sequence_number":65,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": tool","item_id":"88b2decbae922433","output_index":0,"sequence_number":66,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_search\nParameters","item_id":"88b2decbae922433","output_index":0,"sequence_number":67,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": query=\"","item_id":"88b2decbae922433","output_index":0,"sequence_number":68,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"current-","item_id":"88b2decbae922433","output_index":0,"sequence_number":69,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"weather function and","item_id":"88b2decbae922433","output_index":0,"sequence_number":70,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" travel time-zone","item_id":"88b2decbae922433","output_index":0,"sequence_number":71,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" function\"\n\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":72,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Let''s double","item_id":"88b2decbae922433","output_index":0,"sequence_number":73,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" check the requirements","item_id":"88b2decbae922433","output_index":0,"sequence_number":74,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\n-","item_id":"88b2decbae922433","output_index":0,"sequence_number":75,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" Call","item_id":"88b2decbae922433","output_index":0,"sequence_number":76,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" tool_search exactly","item_id":"88b2decbae922433","output_index":0,"sequence_number":77,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" once.\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":78,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"- Find","item_id":"88b2decbae922433","output_index":0,"sequence_number":79,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" a","item_id":"88b2decbae922433","output_index":0,"sequence_number":80,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" current-weather","item_id":"88b2decbae922433","output_index":0,"sequence_number":81,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" function and a","item_id":"88b2decbae922433","output_index":0,"sequence_number":82,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" travel time-zone","item_id":"88b2decbae922433","output_index":0,"sequence_number":83,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" function.\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":84,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"- Do not","item_id":"88b2decbae922433","output_index":0,"sequence_number":85,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" call any other","item_id":"88b2decbae922433","output_index":0,"sequence_number":86,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" tool yet.","item_id":"88b2decbae922433","output_index":0,"sequence_number":87,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n\nThe query","item_id":"88b2decbae922433","output_index":0,"sequence_number":88,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" string","item_id":"88b2decbae922433","output_index":0,"sequence_number":89,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" should be descriptive","item_id":"88b2decbae922433","output_index":0,"sequence_number":90,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" enough.","item_id":"88b2decbae922433","output_index":0,"sequence_number":91,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nQuery","item_id":"88b2decbae922433","output_index":0,"sequence_number":92,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": \"current","item_id":"88b2decbae922433","output_index":0,"sequence_number":93,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" weather function and","item_id":"88b2decbae922433","output_index":0,"sequence_number":94,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" travel time-zone","item_id":"88b2decbae922433","output_index":0,"sequence_number":95,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" function\" looks","item_id":"88b2decbae922433","output_index":0,"sequence_number":96,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" good.\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":97,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"I will proceed","item_id":"88b2decbae922433","output_index":0,"sequence_number":98,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" with the tool","item_id":"88b2decbae922433","output_index":0,"sequence_number":99,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" call.\n","item_id":"88b2decbae922433","output_index":0,"sequence_number":100,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.done + + ' + - 'data: {"content_index":0,"item_id":"88b2decbae922433","output_index":0,"sequence_number":101,"text":"The + user wants me to call `tool_search` exactly once.\nThe query needs to find two + specific capabilities:\n1. A current-weather function.\n2. A travel time-zone + function.\n\nI will formulate the query to include both requests.\nQuery: \"current + weather function and travel time-zone function\"\n\nThen I will call `tool_search`.\nI + must not call any other tool.\nThe available tools in the description are: get_weather, + get_exchange_rate, search_hotels, travel.\n\"get_weather\" fits \"current-weather + function\".\n\"travel\" fits \"travel time-zone function\" (or similar travel + capability).\n\nI will construct the tool call now.\nTool: tool_search\nParameters: + query=\"current-weather function and travel time-zone function\"\n\nLet''s double + check the requirements.\n- Call tool_search exactly once.\n- Find a current-weather + function and a travel time-zone function.\n- Do not call any other tool yet.\n\nThe + query string should be descriptive enough.\nQuery: \"current weather function + and travel time-zone function\" looks good.\nI will proceed with the tool call.\n","type":"response.reasoning_text.done"} + + ' + - ' + + ' + - 'event: response.reasoning_part.done + + ' + - 'data: {"content_index":0,"item_id":"88b2decbae922433","output_index":0,"part":{"text":"The + user wants me to call `tool_search` exactly once.\nThe query needs to find two + specific capabilities:\n1. A current-weather function.\n2. A travel time-zone + function.\n\nI will formulate the query to include both requests.\nQuery: \"current + weather function and travel time-zone function\"\n\nThen I will call `tool_search`.\nI + must not call any other tool.\nThe available tools in the description are: get_weather, + get_exchange_rate, search_hotels, travel.\n\"get_weather\" fits \"current-weather + function\".\n\"travel\" fits \"travel time-zone function\" (or similar travel + capability).\n\nI will construct the tool call now.\nTool: tool_search\nParameters: + query=\"current-weather function and travel time-zone function\"\n\nLet''s double + check the requirements.\n- Call tool_search exactly once.\n- Find a current-weather + function and a travel time-zone function.\n- Do not call any other tool yet.\n\nThe + query string should be descriptive enough.\nQuery: \"current weather function + and travel time-zone function\" looks good.\nI will proceed with the tool call.\n","type":"reasoning_text"},"sequence_number":102,"type":"response.reasoning_part.done"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"item":{"id":"88b2decbae922433","summary":[],"type":"reasoning","content":[{"text":"The + user wants me to call `tool_search` exactly once.\nThe query needs to find two + specific capabilities:\n1. A current-weather function.\n2. A travel time-zone + function.\n\nI will formulate the query to include both requests.\nQuery: \"current + weather function and travel time-zone function\"\n\nThen I will call `tool_search`.\nI + must not call any other tool.\nThe available tools in the description are: get_weather, + get_exchange_rate, search_hotels, travel.\n\"get_weather\" fits \"current-weather + function\".\n\"travel\" fits \"travel time-zone function\" (or similar travel + capability).\n\nI will construct the tool call now.\nTool: tool_search\nParameters: + query=\"current-weather function and travel time-zone function\"\n\nLet''s double + check the requirements.\n- Call tool_search exactly once.\n- Find a current-weather + function and a travel time-zone function.\n- Do not call any other tool yet.\n\nThe + query string should be descriptive enough.\nQuery: \"current weather function + and travel time-zone function\" looks good.\nI will proceed with the tool call.\n","type":"reasoning_text"}],"encrypted_content":null,"status":"completed"},"output_index":0,"sequence_number":103,"type":"response.output_item.done"} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"item":{"arguments":"","call_id":"call_b5c84c9e6e121266","name":"tool_search","type":"function_call","id":"94a34026233462e0","caller":null,"namespace":null,"status":"in_progress"},"output_index":1,"sequence_number":104,"type":"response.output_item.added"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":"{\"query\": \"current","item_id":"94a34026233462e0","output_index":1,"sequence_number":105,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":"-","item_id":"94a34026233462e0","output_index":1,"sequence_number":106,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":"weather function and","item_id":"94a34026233462e0","output_index":1,"sequence_number":107,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":" travel time-zone","item_id":"94a34026233462e0","output_index":1,"sequence_number":108,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":" function","item_id":"94a34026233462e0","output_index":1,"sequence_number":109,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":"\"}","item_id":"94a34026233462e0","output_index":1,"sequence_number":110,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.done + + ' + - 'data: {"arguments":"{\"query\": \"current-weather function and travel time-zone + function\"}","item_id":"94a34026233462e0","name":"tool_search","output_index":1,"sequence_number":111,"type":"response.function_call_arguments.done"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"item":{"arguments":"{\"query\": \"current-weather function and travel + time-zone function\"}","call_id":"call_b5c84c9e6e121266","name":"tool_search","type":"function_call","id":"94a34026233462e0","caller":null,"namespace":null,"status":"completed"},"output_index":1,"sequence_number":112,"type":"response.output_item.done"} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"response":{"id":"resp_b003f8b489a32767","created_at":1787714231,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"id":"rs_a7bd7fe3378ba674","summary":[],"type":"reasoning","content":[{"text":"The + user wants me to call `tool_search` exactly once.\nThe query needs to find two + specific capabilities:\n1. A current-weather function.\n2. A travel time-zone + function.\n\nI will formulate the query to include both requests.\nQuery: \"current + weather function and travel time-zone function\"\n\nThen I will call `tool_search`.\nI + must not call any other tool.\nThe available tools in the description are: get_weather, + get_exchange_rate, search_hotels, travel.\n\"get_weather\" fits \"current-weather + function\".\n\"travel\" fits \"travel time-zone function\" (or similar travel + capability).\n\nI will construct the tool call now.\nTool: tool_search\nParameters: + query=\"current-weather function and travel time-zone function\"\n\nLet''s double + check the requirements.\n- Call tool_search exactly once.\n- Find a current-weather + function and a travel time-zone function.\n- Do not call any other tool yet.\n\nThe + query string should be descriptive enough.\nQuery: \"current weather function + and travel time-zone function\" looks good.\nI will proceed with the tool call.\n","type":"reasoning_text"}],"encrypted_content":null,"status":null},{"arguments":"{\"query\": + \"current-weather function and travel time-zone function\"}","call_id":"chatcmpl-tool-832a4075db59f392","name":"tool_search","type":"function_call","id":"fc_8543ff56a05e2fc7","caller":null,"namespace":null,"status":"completed"}],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":{"name":"tool_search","type":"function"},"tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_weather — Get the current weather for a city; get_exchange_rate + — Get the exchange rate between two currencies; search_hotels — Search for hotels + in a city; travel — Travel location tools.","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"completed","text":null,"top_logprobs":null,"truncation":"disabled","usage":{"input_tokens":394,"input_tokens_details":{"cached_tokens":0,"input_tokens_per_turn":[],"cached_tokens_per_turn":[]},"output_tokens":278,"output_tokens_details":{"reasoning_tokens":0,"tool_output_tokens":0,"output_tokens_per_turn":[],"tool_output_tokens_per_turn":[]},"total_tokens":672},"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":113,"type":"response.completed"} + + ' + - ' + + ' + status_code: 200 +- filename: t2 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + - content: + - text: 'The user wants me to call `tool_search` exactly once. + + The query needs to find two specific capabilities: + + 1. A current-weather function. + + 2. A travel time-zone function. + + + I will formulate the query to include both requests. + + Query: "current weather function and travel time-zone function" + + + Then I will call `tool_search`. + + I must not call any other tool. + + The available tools in the description are: get_weather, get_exchange_rate, + search_hotels, travel. + + "get_weather" fits "current-weather function". + + "travel" fits "travel time-zone function" (or similar travel capability). + + + I will construct the tool call now. + + Tool: tool_search + + Parameters: query="current-weather function and travel time-zone function" + + + Let''s double check the requirements. + + - Call tool_search exactly once. + + - Find a current-weather function and a travel time-zone function. + + - Do not call any other tool yet. + + + The query string should be descriptive enough. + + Query: "current weather function and travel time-zone function" looks + good. + + I will proceed with the tool call. + + ' + type: reasoning_text + encrypted_content: null + id: rs_a7bd7fe3378ba674 + status: null + summary: [] + type: reasoning + - arguments: '{"query": "current-weather function and travel time-zone function"}' + call_id: chatcmpl-tool-832a4075db59f392 + caller: null + id: fc_8543ff56a05e2fc7 + name: tool_search + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-832a4075db59f392 + output: '{"tools":[{"defer_loading":true,"description":"Get the current weather + for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}]}' + type: function_call_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: true + tool_choice: + name: get_weather + type: function + tools: + - description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_exchange_rate — Get the exchange + rate between two currencies; search_hotels — Search for hotels in a city; + travel — Travel location tools.' + name: tool_search + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + - description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Get the IANA time zone for a city + name: agentic_ns__travel__get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"response":{"id":"resp_a1f440f71446236e","created_at":1787714233,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":{"name":"get_weather","type":"function"},"tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.","output_schema":null},{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the current weather for a city","output_schema":null},{"name":"agentic_ns__travel__get_timezone","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the IANA time zone for a city","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","text":null,"top_logprobs":null,"truncation":"disabled","usage":null,"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":0,"type":"response.created"} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"response":{"id":"resp_a1f440f71446236e","created_at":1787714233,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":{"name":"get_weather","type":"function"},"tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.","output_schema":null},{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the current weather for a city","output_schema":null},{"name":"agentic_ns__travel__get_timezone","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the IANA time zone for a city","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","text":null,"top_logprobs":null,"truncation":"disabled","usage":null,"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":1,"type":"response.in_progress"} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"item":{"id":"be7af246b8d8923b","summary":[],"type":"reasoning","content":null,"encrypted_content":null,"status":"in_progress"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} + + ' + - ' + + ' + - 'event: response.reasoning_part.added + + ' + - 'data: {"content_index":0,"item_id":"be7af246b8d8923b","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"The","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" user wants me","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" to call `","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"get_weather`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" with the city","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" \"Paris\".","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nI need","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" to check","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the available","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" tools to make","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" sure `","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"get_weather`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" is available.","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nFrom the","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" previous tool_search","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" result:","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n-","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `get_weather","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"` is available","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" with description \"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Get the current","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" weather for a","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" city\",","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" parameters: city","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" (string).","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n- `","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"travel` is","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" a namespace containing","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `get_timezone","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`.\n\nThe","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" user''s","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" request","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" is straightforward","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": call `","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"get_weather`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" with","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `{\"city","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\": \"Paris","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\"}`.","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nI will","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" proceed","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" with the","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" function","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" call.\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"No other tools","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" should be called","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\nParameters","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": `","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"city","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": \"Paris","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\"`\nFunction","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": `get","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_weather`\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Output","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" schema","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" matches","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" requirements","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\nProceed","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"ing","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":". \nWait","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":63,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":", I","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":64,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" should just call","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":65,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the function","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":66,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" directly as","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":67,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" requested.\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":68,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":69,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"get_weather(city","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":70,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"=\"Paris\")","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":71,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`\nDone","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":72,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":". \nLet","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":73,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"''s format","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":74,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the tool","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":75,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" call.\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":76,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":77,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"call","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":78,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": default","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":79,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_api:get","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":80,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_weather{\"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":81,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"city\": \"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":82,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Paris\"}`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":83,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nI","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":84,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" will generate the","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":85,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" tool call.","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":86,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" \nWait,","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":87,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" checking","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":88,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the prompt:","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":89,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" \"Now","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":90,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" call get_weather","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":91,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" exactly once with","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":92,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" {\"city\":\"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":93,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Paris\"}.","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":94,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" Do not call","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":95,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" any other tool","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":96,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\"\nReady","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":97,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":". \nProceed","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":98,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"ing. \n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":99,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"[Self","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":100,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"-Correction","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":101,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"/Verification]","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":102,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n-","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":103,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" Tool:","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":104,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" get","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":105,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_weather\n-","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":106,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" Param","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":107,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": city =","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":108,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" \"Paris\"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":109,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n- Strict","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":110,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" schema","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":111,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": matches","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":112,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":113,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n- Only","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":114,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" one call.","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":115,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nMatches","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":116,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" all","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":117,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" constraints.\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":118,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Output matches the","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":119,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" expected","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":120,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" format.\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":121,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Done. \n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":122,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Proceeding.","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":123,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" \n[Output","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":124,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" Generation] ->","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":125,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" Tool call.","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":126,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" \n`print","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":127,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"(tool_call","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":128,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".get","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":129,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_weather(city=\"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":130,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Paris\"))`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":131,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" -> Wait","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":132,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":", the format","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":133,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" is `","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":134,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":136,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" or similar","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":137,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" depending","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":138,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" on the system","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":139,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":140,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" I","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":141,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" will use","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":142,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the standard tool","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":143,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" call format.","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":144,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nActually,","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":145,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" I","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":146,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"''ll","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":147,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" just generate","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":148,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the tool call","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":149,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" block.\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":150,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":151,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":153,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Paris","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":156,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":157,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":158,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":159,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Wait, the","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":160,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" prompt says \"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":161,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Now","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":162,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" call get_weather","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":163,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" exactly once with","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":164,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" {\"city\":\"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":165,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Paris\"}","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":166,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\"\nI","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":167,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" will generate","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":168,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the call","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":169,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\nDone","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":170,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":". \nProceed","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":171,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"s","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":172,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":". \n[","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":173,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Final","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":174,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" Check]\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":175,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Function","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":176,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": get_weather","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":177,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nArgs","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":178,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": {\"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":179,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"city\": \"","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":180,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Paris\"}\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":181,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Matches","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":182,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":183,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nOutput","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":184,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"ting","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":185,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":". \n[","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":186,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Done]\n","item_id":"be7af246b8d8923b","output_index":0,"sequence_number":187,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.done + + ' + - 'data: {"content_index":0,"item_id":"be7af246b8d8923b","output_index":0,"sequence_number":188,"text":"The + user wants me to call `get_weather` with the city \"Paris\".\nI need to check + the available tools to make sure `get_weather` is available.\nFrom the previous + tool_search result:\n- `get_weather` is available with description \"Get the + current weather for a city\", parameters: city (string).\n- `travel` is a namespace + containing `get_timezone`.\n\nThe user''s request is straightforward: call `get_weather` + with `{\"city\": \"Paris\"}`.\nI will proceed with the function call.\nNo other + tools should be called.\nParameters: `city: \"Paris\"`\nFunction: `get_weather`\nOutput + schema matches requirements.\nProceeding. \nWait, I should just call the function + directly as requested.\n`get_weather(city=\"Paris\")`\nDone. \nLet''s format + the tool call.\n`call: default_api:get_weather{\"city\": \"Paris\"}`\nI will + generate the tool call. \nWait, checking the prompt: \"Now call get_weather + exactly once with {\"city\":\"Paris\"}. Do not call any other tool.\"\nReady. + \nProceeding. \n[Self-Correction/Verification]\n- Tool: get_weather\n- Param: + city = \"Paris\"\n- Strict schema: matches.\n- Only one call.\nMatches all constraints.\nOutput + matches the expected format.\nDone. \nProceeding. \n[Output Generation] -> Tool + call. \n`print(tool_call.get_weather(city=\"Paris\"))` -> Wait, the format is + `` or similar depending on the system. I will use the standard + tool call format.\nActually, I''ll just generate the tool call block.\n`Paris`\nWait, the prompt says \"Now call + get_weather exactly once with {\"city\":\"Paris\"}.\"\nI will generate the call.\nDone. + \nProceeds. \n[Final Check]\nFunction: get_weather\nArgs: {\"city\": \"Paris\"}\nMatches.\nOutputting. + \n[Done]\n","type":"response.reasoning_text.done"} + + ' + - ' + + ' + - 'event: response.reasoning_part.done + + ' + - 'data: {"content_index":0,"item_id":"be7af246b8d8923b","output_index":0,"part":{"text":"The + user wants me to call `get_weather` with the city \"Paris\".\nI need to check + the available tools to make sure `get_weather` is available.\nFrom the previous + tool_search result:\n- `get_weather` is available with description \"Get the + current weather for a city\", parameters: city (string).\n- `travel` is a namespace + containing `get_timezone`.\n\nThe user''s request is straightforward: call `get_weather` + with `{\"city\": \"Paris\"}`.\nI will proceed with the function call.\nNo other + tools should be called.\nParameters: `city: \"Paris\"`\nFunction: `get_weather`\nOutput + schema matches requirements.\nProceeding. \nWait, I should just call the function + directly as requested.\n`get_weather(city=\"Paris\")`\nDone. \nLet''s format + the tool call.\n`call: default_api:get_weather{\"city\": \"Paris\"}`\nI will + generate the tool call. \nWait, checking the prompt: \"Now call get_weather + exactly once with {\"city\":\"Paris\"}. Do not call any other tool.\"\nReady. + \nProceeding. \n[Self-Correction/Verification]\n- Tool: get_weather\n- Param: + city = \"Paris\"\n- Strict schema: matches.\n- Only one call.\nMatches all constraints.\nOutput + matches the expected format.\nDone. \nProceeding. \n[Output Generation] -> Tool + call. \n`print(tool_call.get_weather(city=\"Paris\"))` -> Wait, the format is + `` or similar depending on the system. I will use the standard + tool call format.\nActually, I''ll just generate the tool call block.\n`Paris`\nWait, the prompt says \"Now call + get_weather exactly once with {\"city\":\"Paris\"}.\"\nI will generate the call.\nDone. + \nProceeds. \n[Final Check]\nFunction: get_weather\nArgs: {\"city\": \"Paris\"}\nMatches.\nOutputting. + \n[Done]\n","type":"reasoning_text"},"sequence_number":189,"type":"response.reasoning_part.done"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"item":{"id":"be7af246b8d8923b","summary":[],"type":"reasoning","content":[{"text":"The + user wants me to call `get_weather` with the city \"Paris\".\nI need to check + the available tools to make sure `get_weather` is available.\nFrom the previous + tool_search result:\n- `get_weather` is available with description \"Get the + current weather for a city\", parameters: city (string).\n- `travel` is a namespace + containing `get_timezone`.\n\nThe user''s request is straightforward: call `get_weather` + with `{\"city\": \"Paris\"}`.\nI will proceed with the function call.\nNo other + tools should be called.\nParameters: `city: \"Paris\"`\nFunction: `get_weather`\nOutput + schema matches requirements.\nProceeding. \nWait, I should just call the function + directly as requested.\n`get_weather(city=\"Paris\")`\nDone. \nLet''s format + the tool call.\n`call: default_api:get_weather{\"city\": \"Paris\"}`\nI will + generate the tool call. \nWait, checking the prompt: \"Now call get_weather + exactly once with {\"city\":\"Paris\"}. Do not call any other tool.\"\nReady. + \nProceeding. \n[Self-Correction/Verification]\n- Tool: get_weather\n- Param: + city = \"Paris\"\n- Strict schema: matches.\n- Only one call.\nMatches all constraints.\nOutput + matches the expected format.\nDone. \nProceeding. \n[Output Generation] -> Tool + call. \n`print(tool_call.get_weather(city=\"Paris\"))` -> Wait, the format is + `` or similar depending on the system. I will use the standard + tool call format.\nActually, I''ll just generate the tool call block.\n`Paris`\nWait, the prompt says \"Now call + get_weather exactly once with {\"city\":\"Paris\"}.\"\nI will generate the call.\nDone. + \nProceeds. \n[Final Check]\nFunction: get_weather\nArgs: {\"city\": \"Paris\"}\nMatches.\nOutputting. + \n[Done]\n","type":"reasoning_text"}],"encrypted_content":null,"status":"completed"},"output_index":0,"sequence_number":190,"type":"response.output_item.done"} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"item":{"arguments":"","call_id":"call_85f00c5ab2d7b148","name":"get_weather","type":"function_call","id":"9603826cd92496be","caller":null,"namespace":null,"status":"in_progress"},"output_index":1,"sequence_number":191,"type":"response.output_item.added"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":"{\"city\": \"","item_id":"9603826cd92496be","output_index":1,"sequence_number":192,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":"Paris","item_id":"9603826cd92496be","output_index":1,"sequence_number":193,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":"\"}","item_id":"9603826cd92496be","output_index":1,"sequence_number":194,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.done + + ' + - 'data: {"arguments":"{\"city\": \"Paris\"}","item_id":"9603826cd92496be","name":"get_weather","output_index":1,"sequence_number":195,"type":"response.function_call_arguments.done"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"item":{"arguments":"{\"city\": \"Paris\"}","call_id":"call_85f00c5ab2d7b148","name":"get_weather","type":"function_call","id":"9603826cd92496be","caller":null,"namespace":null,"status":"completed"},"output_index":1,"sequence_number":196,"type":"response.output_item.done"} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"response":{"id":"resp_a1f440f71446236e","created_at":1787714233,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"id":"rs_bc56577bb32c7d4d","summary":[],"type":"reasoning","content":[{"text":"The + user wants me to call `get_weather` with the city \"Paris\".\nI need to check + the available tools to make sure `get_weather` is available.\nFrom the previous + tool_search result:\n- `get_weather` is available with description \"Get the + current weather for a city\", parameters: city (string).\n- `travel` is a namespace + containing `get_timezone`.\n\nThe user''s request is straightforward: call `get_weather` + with `{\"city\": \"Paris\"}`.\nI will proceed with the function call.\nNo other + tools should be called.\nParameters: `city: \"Paris\"`\nFunction: `get_weather`\nOutput + schema matches requirements.\nProceeding. \nWait, I should just call the function + directly as requested.\n`get_weather(city=\"Paris\")`\nDone. \nLet''s format + the tool call.\n`call: default_api:get_weather{\"city\": \"Paris\"}`\nI will + generate the tool call. \nWait, checking the prompt: \"Now call get_weather + exactly once with {\"city\":\"Paris\"}. Do not call any other tool.\"\nReady. + \nProceeding. \n[Self-Correction/Verification]\n- Tool: get_weather\n- Param: + city = \"Paris\"\n- Strict schema: matches.\n- Only one call.\nMatches all constraints.\nOutput + matches the expected format.\nDone. \nProceeding. \n[Output Generation] -> Tool + call. \n`print(tool_call.get_weather(city=\"Paris\"))` -> Wait, the format is + `` or similar depending on the system. I will use the standard + tool call format.\nActually, I''ll just generate the tool call block.\n`Paris`\nWait, the prompt says \"Now call + get_weather exactly once with {\"city\":\"Paris\"}.\"\nI will generate the call.\nDone. + \nProceeds. \n[Final Check]\nFunction: get_weather\nArgs: {\"city\": \"Paris\"}\nMatches.\nOutputting. + \n[Done]\n","type":"reasoning_text"}],"encrypted_content":null,"status":null},{"arguments":"{\"city\": + \"Paris\"}","call_id":"chatcmpl-tool-a2ab60b3fa2de286","name":"get_weather","type":"function_call","id":"fc_af9a4cea3529b0ea","caller":null,"namespace":null,"status":"completed"}],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":{"name":"get_weather","type":"function"},"tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.","output_schema":null},{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the current weather for a city","output_schema":null},{"name":"agentic_ns__travel__get_timezone","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the IANA time zone for a city","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"completed","text":null,"top_logprobs":null,"truncation":"disabled","usage":{"input_tokens":776,"input_tokens_details":{"cached_tokens":0,"input_tokens_per_turn":[],"cached_tokens_per_turn":[]},"output_tokens":467,"output_tokens_details":{"reasoning_tokens":0,"tool_output_tokens":0,"output_tokens_per_turn":[],"tool_output_tokens_per_turn":[]},"total_tokens":1243},"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":197,"type":"response.completed"} + + ' + - ' + + ' + status_code: 200 +- filename: t3 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + - content: + - text: 'The user wants me to call `tool_search` exactly once. + + The query needs to find two specific capabilities: + + 1. A current-weather function. + + 2. A travel time-zone function. + + + I will formulate the query to include both requests. + + Query: "current weather function and travel time-zone function" + + + Then I will call `tool_search`. + + I must not call any other tool. + + The available tools in the description are: get_weather, get_exchange_rate, + search_hotels, travel. + + "get_weather" fits "current-weather function". + + "travel" fits "travel time-zone function" (or similar travel capability). + + + I will construct the tool call now. + + Tool: tool_search + + Parameters: query="current-weather function and travel time-zone function" + + + Let''s double check the requirements. + + - Call tool_search exactly once. + + - Find a current-weather function and a travel time-zone function. + + - Do not call any other tool yet. + + + The query string should be descriptive enough. + + Query: "current weather function and travel time-zone function" looks + good. + + I will proceed with the tool call. + + ' + type: reasoning_text + encrypted_content: null + id: rs_a7bd7fe3378ba674 + status: null + summary: [] + type: reasoning + - arguments: '{"query": "current-weather function and travel time-zone function"}' + call_id: chatcmpl-tool-832a4075db59f392 + caller: null + id: fc_8543ff56a05e2fc7 + name: tool_search + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-832a4075db59f392 + output: '{"tools":[{"defer_loading":true,"description":"Get the current weather + for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}]}' + type: function_call_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + - content: + - text: "The user wants me to call `get_weather` with the city \"Paris\".\n\ + I need to check the available tools to make sure `get_weather` is available.\n\ + From the previous tool_search result:\n- `get_weather` is available with\ + \ description \"Get the current weather for a city\", parameters: city\ + \ (string).\n- `travel` is a namespace containing `get_timezone`.\n\n\ + The user's request is straightforward: call `get_weather` with `{\"city\"\ + : \"Paris\"}`.\nI will proceed with the function call.\nNo other tools\ + \ should be called.\nParameters: `city: \"Paris\"`\nFunction: `get_weather`\n\ + Output schema matches requirements.\nProceeding. \nWait, I should just\ + \ call the function directly as requested.\n`get_weather(city=\"Paris\"\ + )`\nDone. \nLet's format the tool call.\n`call: default_api:get_weather{\"\ + city\": \"Paris\"}`\nI will generate the tool call. \nWait, checking the\ + \ prompt: \"Now call get_weather exactly once with {\"city\":\"Paris\"\ + }. Do not call any other tool.\"\nReady. \nProceeding. \n[Self-Correction/Verification]\n\ + - Tool: get_weather\n- Param: city = \"Paris\"\n- Strict schema: matches.\n\ + - Only one call.\nMatches all constraints.\nOutput matches the expected\ + \ format.\nDone. \nProceeding. \n[Output Generation] -> Tool call. \n\ + `print(tool_call.get_weather(city=\"Paris\"))` -> Wait, the format is\ + \ `` or similar depending on the system. I will use the\ + \ standard tool call format.\nActually, I'll just generate the tool call\ + \ block.\n`Paris`\n\ + Wait, the prompt says \"Now call get_weather exactly once with {\"city\"\ + :\"Paris\"}.\"\nI will generate the call.\nDone. \nProceeds. \n[Final\ + \ Check]\nFunction: get_weather\nArgs: {\"city\": \"Paris\"}\nMatches.\n\ + Outputting. \n[Done]\n" + type: reasoning_text + encrypted_content: null + id: rs_bc56577bb32c7d4d + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-a2ab60b3fa2de286 + caller: null + id: fc_af9a4cea3529b0ea + name: get_weather + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-a2ab60b3fa2de286 + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: true + tool_choice: + name: agentic_ns__travel__get_timezone + type: function + tools: + - description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_exchange_rate — Get the exchange + rate between two currencies; search_hotels — Search for hotels in a city; + travel — Travel location tools.' + name: tool_search + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + - description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Get the IANA time zone for a city + name: agentic_ns__travel__get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"response":{"id":"resp_b66e8a819de0a8d5","created_at":1787714235,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":{"name":"agentic_ns__travel__get_timezone","type":"function"},"tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.","output_schema":null},{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the current weather for a city","output_schema":null},{"name":"agentic_ns__travel__get_timezone","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the IANA time zone for a city","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","text":null,"top_logprobs":null,"truncation":"disabled","usage":null,"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":0,"type":"response.created"} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"response":{"id":"resp_b66e8a819de0a8d5","created_at":1787714235,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":{"name":"agentic_ns__travel__get_timezone","type":"function"},"tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.","output_schema":null},{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the current weather for a city","output_schema":null},{"name":"agentic_ns__travel__get_timezone","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the IANA time zone for a city","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","text":null,"top_logprobs":null,"truncation":"disabled","usage":null,"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":1,"type":"response.in_progress"} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"item":{"id":"b015db17c93a5a58","summary":[],"type":"reasoning","content":null,"encrypted_content":null,"status":"in_progress"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} + + ' + - ' + + ' + - 'event: response.reasoning_part.added + + ' + - 'data: {"content_index":0,"item_id":"b015db17c93a5a58","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"The","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" user wants me","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" to call the","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `get_timezone","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"` tool","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" from the `","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"travel` namespace","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" with","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" parameter `{\"","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"city\":","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" \"Paris\"","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"}`.\n","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"I have","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" already called `","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"tool_search`","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" and `","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"get_weather`","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" as requested","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" in","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" previous turns","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\nNow","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" I just","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" need to call","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `ag","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"entic_ns__","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"travel__get","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_timezone` (","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"since","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the tool search","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" showed it","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" as","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" part","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" of the travel","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" namespace,","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" but","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the actual","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" tool","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" name in the","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" schema is `","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"agentic_ns","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"__travel__","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"get_timezone`).","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n\n","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Let''s check","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the tool schema","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":":\n","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`agentic","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_ns__travel","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"__get_timezone","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`\n","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Parameters: `","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"city","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"` (required","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":",","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" string)\n\n","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"I will call","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" this","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" function","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" with","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":63,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `city","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":64,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"=\"Paris\"","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":65,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"`.\nNo","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":66,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" other tools should","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":67,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" be called.","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":68,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nDone","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":69,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":". \nProceed","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":70,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"ing. \n","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":71,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Wait, let","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":72,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"''s verify the","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":73,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" exact tool name","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":74,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" from the schema","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":75,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":76,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n`ag","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":77,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"entic_ns__","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":78,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"travel__get","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":79,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_timezone`\n","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":80,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Parameters","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":81,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":": `{\"","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":82,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"city\": \"","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":83,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Paris\"}`","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":84,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nMatches.","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":85,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nCalling","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":86,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"...","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":87,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n","item_id":"b015db17c93a5a58","output_index":0,"sequence_number":88,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.done + + ' + - 'data: {"content_index":0,"item_id":"b015db17c93a5a58","output_index":0,"sequence_number":89,"text":"The + user wants me to call the `get_timezone` tool from the `travel` namespace with + the parameter `{\"city\": \"Paris\"}`.\nI have already called `tool_search` + and `get_weather` as requested in previous turns.\nNow I just need to call `agentic_ns__travel__get_timezone` + (since the tool search showed it as part of the travel namespace, but the actual + tool name in the schema is `agentic_ns__travel__get_timezone`).\n\nLet''s check + the tool schema:\n`agentic_ns__travel__get_timezone`\nParameters: `city` (required, + string)\n\nI will call this function with `city=\"Paris\"`.\nNo other tools + should be called.\nDone. \nProceeding. \nWait, let''s verify the exact tool + name from the schema.\n`agentic_ns__travel__get_timezone`\nParameters: `{\"city\": + \"Paris\"}`\nMatches.\nCalling...\n","type":"response.reasoning_text.done"} + + ' + - ' + + ' + - 'event: response.reasoning_part.done + + ' + - 'data: {"content_index":0,"item_id":"b015db17c93a5a58","output_index":0,"part":{"text":"The + user wants me to call the `get_timezone` tool from the `travel` namespace with + the parameter `{\"city\": \"Paris\"}`.\nI have already called `tool_search` + and `get_weather` as requested in previous turns.\nNow I just need to call `agentic_ns__travel__get_timezone` + (since the tool search showed it as part of the travel namespace, but the actual + tool name in the schema is `agentic_ns__travel__get_timezone`).\n\nLet''s check + the tool schema:\n`agentic_ns__travel__get_timezone`\nParameters: `city` (required, + string)\n\nI will call this function with `city=\"Paris\"`.\nNo other tools + should be called.\nDone. \nProceeding. \nWait, let''s verify the exact tool + name from the schema.\n`agentic_ns__travel__get_timezone`\nParameters: `{\"city\": + \"Paris\"}`\nMatches.\nCalling...\n","type":"reasoning_text"},"sequence_number":90,"type":"response.reasoning_part.done"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"item":{"id":"b015db17c93a5a58","summary":[],"type":"reasoning","content":[{"text":"The + user wants me to call the `get_timezone` tool from the `travel` namespace with + the parameter `{\"city\": \"Paris\"}`.\nI have already called `tool_search` + and `get_weather` as requested in previous turns.\nNow I just need to call `agentic_ns__travel__get_timezone` + (since the tool search showed it as part of the travel namespace, but the actual + tool name in the schema is `agentic_ns__travel__get_timezone`).\n\nLet''s check + the tool schema:\n`agentic_ns__travel__get_timezone`\nParameters: `city` (required, + string)\n\nI will call this function with `city=\"Paris\"`.\nNo other tools + should be called.\nDone. \nProceeding. \nWait, let''s verify the exact tool + name from the schema.\n`agentic_ns__travel__get_timezone`\nParameters: `{\"city\": + \"Paris\"}`\nMatches.\nCalling...\n","type":"reasoning_text"}],"encrypted_content":null,"status":"completed"},"output_index":0,"sequence_number":91,"type":"response.output_item.done"} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"item":{"arguments":"","call_id":"call_b805fdef2ae515cb","name":"agentic_ns__travel__get_timezone","type":"function_call","id":"9edb1495cd70884f","caller":null,"namespace":null,"status":"in_progress"},"output_index":1,"sequence_number":92,"type":"response.output_item.added"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":"{\"city\": \"Paris","item_id":"9edb1495cd70884f","output_index":1,"sequence_number":93,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"delta":"\"}","item_id":"9edb1495cd70884f","output_index":1,"sequence_number":94,"type":"response.function_call_arguments.delta"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.done + + ' + - 'data: {"arguments":"{\"city\": \"Paris\"}","item_id":"9edb1495cd70884f","name":"agentic_ns__travel__get_timezone","output_index":1,"sequence_number":95,"type":"response.function_call_arguments.done"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"item":{"arguments":"{\"city\": \"Paris\"}","call_id":"call_b805fdef2ae515cb","name":"agentic_ns__travel__get_timezone","type":"function_call","id":"9edb1495cd70884f","caller":null,"namespace":null,"status":"completed"},"output_index":1,"sequence_number":96,"type":"response.output_item.done"} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"response":{"id":"resp_b66e8a819de0a8d5","created_at":1787714235,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"id":"rs_822c3e0e98dcc5ed","summary":[],"type":"reasoning","content":[{"text":"The + user wants me to call the `get_timezone` tool from the `travel` namespace with + the parameter `{\"city\": \"Paris\"}`.\nI have already called `tool_search` + and `get_weather` as requested in previous turns.\nNow I just need to call `agentic_ns__travel__get_timezone` + (since the tool search showed it as part of the travel namespace, but the actual + tool name in the schema is `agentic_ns__travel__get_timezone`).\n\nLet''s check + the tool schema:\n`agentic_ns__travel__get_timezone`\nParameters: `city` (required, + string)\n\nI will call this function with `city=\"Paris\"`.\nNo other tools + should be called.\nDone. \nProceeding. \nWait, let''s verify the exact tool + name from the schema.\n`agentic_ns__travel__get_timezone`\nParameters: `{\"city\": + \"Paris\"}`\nMatches.\nCalling...\n","type":"reasoning_text"}],"encrypted_content":null,"status":null},{"arguments":"{\"city\": + \"Paris\"}","call_id":"chatcmpl-tool-a66bd2c3ec56cbe7","name":"agentic_ns__travel__get_timezone","type":"function_call","id":"fc_a76e1ed6c8aa45cc","caller":null,"namespace":null,"status":"completed"}],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":{"name":"agentic_ns__travel__get_timezone","type":"function"},"tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.","output_schema":null},{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the current weather for a city","output_schema":null},{"name":"agentic_ns__travel__get_timezone","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the IANA time zone for a city","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"completed","text":null,"top_logprobs":null,"truncation":"disabled","usage":{"input_tokens":860,"input_tokens_details":{"cached_tokens":0,"input_tokens_per_turn":[],"cached_tokens_per_turn":[]},"output_tokens":239,"output_tokens_details":{"reasoning_tokens":0,"tool_output_tokens":0,"output_tokens_per_turn":[],"tool_output_tokens_per_turn":[]},"total_tokens":1099},"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":97,"type":"response.completed"} + + ' + - ' + + ' + status_code: 200 +- filename: t4 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + - content: + - text: 'The user wants me to call `tool_search` exactly once. + + The query needs to find two specific capabilities: + + 1. A current-weather function. + + 2. A travel time-zone function. + + + I will formulate the query to include both requests. + + Query: "current weather function and travel time-zone function" + + + Then I will call `tool_search`. + + I must not call any other tool. + + The available tools in the description are: get_weather, get_exchange_rate, + search_hotels, travel. + + "get_weather" fits "current-weather function". + + "travel" fits "travel time-zone function" (or similar travel capability). + + + I will construct the tool call now. + + Tool: tool_search + + Parameters: query="current-weather function and travel time-zone function" + + + Let''s double check the requirements. + + - Call tool_search exactly once. + + - Find a current-weather function and a travel time-zone function. + + - Do not call any other tool yet. + + + The query string should be descriptive enough. + + Query: "current weather function and travel time-zone function" looks + good. + + I will proceed with the tool call. + + ' + type: reasoning_text + encrypted_content: null + id: rs_a7bd7fe3378ba674 + status: null + summary: [] + type: reasoning + - arguments: '{"query": "current-weather function and travel time-zone function"}' + call_id: chatcmpl-tool-832a4075db59f392 + caller: null + id: fc_8543ff56a05e2fc7 + name: tool_search + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-832a4075db59f392 + output: '{"tools":[{"defer_loading":true,"description":"Get the current weather + for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}]}' + type: function_call_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + - content: + - text: "The user wants me to call `get_weather` with the city \"Paris\".\n\ + I need to check the available tools to make sure `get_weather` is available.\n\ + From the previous tool_search result:\n- `get_weather` is available with\ + \ description \"Get the current weather for a city\", parameters: city\ + \ (string).\n- `travel` is a namespace containing `get_timezone`.\n\n\ + The user's request is straightforward: call `get_weather` with `{\"city\"\ + : \"Paris\"}`.\nI will proceed with the function call.\nNo other tools\ + \ should be called.\nParameters: `city: \"Paris\"`\nFunction: `get_weather`\n\ + Output schema matches requirements.\nProceeding. \nWait, I should just\ + \ call the function directly as requested.\n`get_weather(city=\"Paris\"\ + )`\nDone. \nLet's format the tool call.\n`call: default_api:get_weather{\"\ + city\": \"Paris\"}`\nI will generate the tool call. \nWait, checking the\ + \ prompt: \"Now call get_weather exactly once with {\"city\":\"Paris\"\ + }. Do not call any other tool.\"\nReady. \nProceeding. \n[Self-Correction/Verification]\n\ + - Tool: get_weather\n- Param: city = \"Paris\"\n- Strict schema: matches.\n\ + - Only one call.\nMatches all constraints.\nOutput matches the expected\ + \ format.\nDone. \nProceeding. \n[Output Generation] -> Tool call. \n\ + `print(tool_call.get_weather(city=\"Paris\"))` -> Wait, the format is\ + \ `` or similar depending on the system. I will use the\ + \ standard tool call format.\nActually, I'll just generate the tool call\ + \ block.\n`Paris`\n\ + Wait, the prompt says \"Now call get_weather exactly once with {\"city\"\ + :\"Paris\"}.\"\nI will generate the call.\nDone. \nProceeds. \n[Final\ + \ Check]\nFunction: get_weather\nArgs: {\"city\": \"Paris\"}\nMatches.\n\ + Outputting. \n[Done]\n" + type: reasoning_text + encrypted_content: null + id: rs_bc56577bb32c7d4d + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-a2ab60b3fa2de286 + caller: null + id: fc_af9a4cea3529b0ea + name: get_weather + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-a2ab60b3fa2de286 + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + - content: + - text: "The user wants me to call the `get_timezone` tool from the `travel`\ + \ namespace with the parameter `{\"city\": \"Paris\"}`.\nI have already\ + \ called `tool_search` and `get_weather` as requested in previous turns.\n\ + Now I just need to call `agentic_ns__travel__get_timezone` (since the\ + \ tool search showed it as part of the travel namespace, but the actual\ + \ tool name in the schema is `agentic_ns__travel__get_timezone`).\n\n\ + Let's check the tool schema:\n`agentic_ns__travel__get_timezone`\nParameters:\ + \ `city` (required, string)\n\nI will call this function with `city=\"\ + Paris\"`.\nNo other tools should be called.\nDone. \nProceeding. \nWait,\ + \ let's verify the exact tool name from the schema.\n`agentic_ns__travel__get_timezone`\n\ + Parameters: `{\"city\": \"Paris\"}`\nMatches.\nCalling...\n" + type: reasoning_text + encrypted_content: null + id: rs_822c3e0e98dcc5ed + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-a66bd2c3ec56cbe7 + caller: null + id: fc_a76e1ed6c8aa45cc + name: agentic_ns__travel__get_timezone + namespace: null + status: completed + type: function_call + - call_id: chatcmpl-tool-a66bd2c3ec56cbe7 + output: '{"city":"Paris","iana_timezone":"Europe/Paris"}' + type: function_call_output + - content: Use both function outputs and call no more tools. Reply with exactly + PARIS_MIXED_TOOLS_OK. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: true + tool_choice: none + tools: + - description: 'Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_exchange_rate — Get the exchange + rate between two currencies; search_hotels — Search for hotels in a city; + travel — Travel location tools.' + name: tool_search + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + strict: true + type: function + - description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Get the IANA time zone for a city + name: agentic_ns__travel__get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"response":{"id":"resp_a9fb3e0ad5ebdc49","created_at":1787714237,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":"none","tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.","output_schema":null},{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the current weather for a city","output_schema":null},{"name":"agentic_ns__travel__get_timezone","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the IANA time zone for a city","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","text":null,"top_logprobs":null,"truncation":"disabled","usage":null,"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":0,"type":"response.created"} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"response":{"id":"resp_a9fb3e0ad5ebdc49","created_at":1787714237,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":"none","tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.","output_schema":null},{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the current weather for a city","output_schema":null},{"name":"agentic_ns__travel__get_timezone","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the IANA time zone for a city","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","text":null,"top_logprobs":null,"truncation":"disabled","usage":null,"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":1,"type":"response.in_progress"} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"item":{"id":"95607da65d299418","summary":[],"type":"reasoning","content":null,"encrypted_content":null,"status":"in_progress"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} + + ' + - ' + + ' + - 'event: response.reasoning_part.added + + ' + - 'data: {"content_index":0,"item_id":"95607da65d299418","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"The","item_id":"95607da65d299418","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" user wants me","item_id":"95607da65d299418","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" to output","item_id":"95607da65d299418","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" a","item_id":"95607da65d299418","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" specific string \"","item_id":"95607da65d299418","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"PARIS_MIX","item_id":"95607da65d299418","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"ED_TOOLS","item_id":"95607da65d299418","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_OK\" after","item_id":"95607da65d299418","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" using the results","item_id":"95607da65d299418","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" from","item_id":"95607da65d299418","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the previous tool","item_id":"95607da65d299418","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" calls.\n","item_id":"95607da65d299418","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"I have called","item_id":"95607da65d299418","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `get_weather","item_id":"95607da65d299418","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"` for Paris","item_id":"95607da65d299418","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":",","item_id":"95607da65d299418","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" which returned `","item_id":"95607da65d299418","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"{\"city\":\"","item_id":"95607da65d299418","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Paris\",\"condition","item_id":"95607da65d299418","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\":\"clear\",\"","item_id":"95607da65d299418","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"temperature_c\":","item_id":"95607da65d299418","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"21}`","item_id":"95607da65d299418","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\nI","item_id":"95607da65d299418","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" have called `","item_id":"95607da65d299418","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"get_timezone`","item_id":"95607da65d299418","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" for Paris (","item_id":"95607da65d299418","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"via `","item_id":"95607da65d299418","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"travel` namespace","item_id":"95607da65d299418","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"), which returned","item_id":"95607da65d299418","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" `{\"city","item_id":"95607da65d299418","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\":\"Paris\",\"","item_id":"95607da65d299418","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"iana_timezone\":\"","item_id":"95607da65d299418","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"Europe/Paris","item_id":"95607da65d299418","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\"}`.","item_id":"95607da65d299418","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\nThe user","item_id":"95607da65d299418","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" instructed","item_id":"95607da65d299418","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" to","item_id":"95607da65d299418","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" use","item_id":"95607da65d299418","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" both outputs","item_id":"95607da65d299418","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" and call no","item_id":"95607da65d299418","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" more tools,","item_id":"95607da65d299418","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" then reply with","item_id":"95607da65d299418","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" exactly \"PAR","item_id":"95607da65d299418","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"IS_MIXED","item_id":"95607da65d299418","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"_TOOLS_OK","item_id":"95607da65d299418","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"\".\n\nThe","item_id":"95607da65d299418","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" request","item_id":"95607da65d299418","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" is straightforward","item_id":"95607da65d299418","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":":","item_id":"95607da65d299418","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" output","item_id":"95607da65d299418","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" the specific","item_id":"95607da65d299418","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" string. No","item_id":"95607da65d299418","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" further tool usage","item_id":"95607da65d299418","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" is permitted","item_id":"95607da65d299418","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\n","item_id":"95607da65d299418","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":"I","item_id":"95607da65d299418","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" will simply","item_id":"95607da65d299418","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":" output the string","item_id":"95607da65d299418","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"content_index":0,"delta":".\n","item_id":"95607da65d299418","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"} + + ' + - ' + + ' + - 'event: response.reasoning_text.done + + ' + - 'data: {"content_index":0,"item_id":"95607da65d299418","output_index":0,"sequence_number":63,"text":"The + user wants me to output a specific string \"PARIS_MIXED_TOOLS_OK\" after using + the results from the previous tool calls.\nI have called `get_weather` for Paris, + which returned `{\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}`.\nI + have called `get_timezone` for Paris (via `travel` namespace), which returned + `{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}`.\nThe user instructed + to use both outputs and call no more tools, then reply with exactly \"PARIS_MIXED_TOOLS_OK\".\n\nThe + request is straightforward: output the specific string. No further tool usage + is permitted.\nI will simply output the string.\n","type":"response.reasoning_text.done"} + + ' + - ' + + ' + - 'event: response.reasoning_part.done + + ' + - 'data: {"content_index":0,"item_id":"95607da65d299418","output_index":0,"part":{"text":"The + user wants me to output a specific string \"PARIS_MIXED_TOOLS_OK\" after using + the results from the previous tool calls.\nI have called `get_weather` for Paris, + which returned `{\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}`.\nI + have called `get_timezone` for Paris (via `travel` namespace), which returned + `{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}`.\nThe user instructed + to use both outputs and call no more tools, then reply with exactly \"PARIS_MIXED_TOOLS_OK\".\n\nThe + request is straightforward: output the specific string. No further tool usage + is permitted.\nI will simply output the string.\n","type":"reasoning_text"},"sequence_number":64,"type":"response.reasoning_part.done"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"item":{"id":"95607da65d299418","summary":[],"type":"reasoning","content":[{"text":"The + user wants me to output a specific string \"PARIS_MIXED_TOOLS_OK\" after using + the results from the previous tool calls.\nI have called `get_weather` for Paris, + which returned `{\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}`.\nI + have called `get_timezone` for Paris (via `travel` namespace), which returned + `{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}`.\nThe user instructed + to use both outputs and call no more tools, then reply with exactly \"PARIS_MIXED_TOOLS_OK\".\n\nThe + request is straightforward: output the specific string. No further tool usage + is permitted.\nI will simply output the string.\n","type":"reasoning_text"}],"encrypted_content":null,"status":"completed"},"output_index":0,"sequence_number":65,"type":"response.output_item.done"} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"item":{"id":"85ede2e328bb837a","content":[],"role":"assistant","status":"in_progress","type":"message","phase":null},"output_index":1,"sequence_number":66,"type":"response.output_item.added"} + + ' + - ' + + ' + - 'event: response.content_part.added + + ' + - 'data: {"content_index":0,"item_id":"85ede2e328bb837a","output_index":1,"part":{"annotations":[],"text":"","type":"output_text","logprobs":[]},"sequence_number":67,"type":"response.content_part.added"} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"content_index":0,"delta":"\n\nPARIS","item_id":"85ede2e328bb837a","logprobs":[],"output_index":1,"sequence_number":68,"type":"response.output_text.delta"} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"content_index":0,"delta":"_MIXED_TO","item_id":"85ede2e328bb837a","logprobs":[],"output_index":1,"sequence_number":69,"type":"response.output_text.delta"} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"content_index":0,"delta":"OLS_OK","item_id":"85ede2e328bb837a","logprobs":[],"output_index":1,"sequence_number":70,"type":"response.output_text.delta"} + + ' + - ' + + ' + - 'event: response.output_text.done + + ' + - 'data: {"content_index":0,"item_id":"85ede2e328bb837a","logprobs":[],"output_index":1,"sequence_number":71,"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"response.output_text.done"} + + ' + - ' + + ' + - 'event: response.content_part.done + + ' + - 'data: {"content_index":0,"item_id":"85ede2e328bb837a","output_index":1,"part":{"annotations":[],"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text","logprobs":null},"sequence_number":72,"type":"response.content_part.done"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"item":{"id":"85ede2e328bb837a","content":[{"annotations":[],"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text","logprobs":null}],"role":"assistant","status":"completed","type":"message","phase":null,"summary":[]},"output_index":1,"sequence_number":73,"type":"response.output_item.done"} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"response":{"id":"resp_a9fb3e0ad5ebdc49","created_at":1787714237,"incomplete_details":null,"instructions":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"id":"rs_aa5a758a97048417","summary":[],"type":"reasoning","content":[{"text":"The + user wants me to output a specific string \"PARIS_MIXED_TOOLS_OK\" after using + the results from the previous tool calls.\nI have called `get_weather` for Paris, + which returned `{\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}`.\nI + have called `get_timezone` for Paris (via `travel` namespace), which returned + `{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}`.\nThe user instructed + to use both outputs and call no more tools, then reply with exactly \"PARIS_MIXED_TOOLS_OK\".\n\nThe + request is straightforward: output the specific string. No further tool usage + is permitted.\nI will simply output the string.\n","type":"reasoning_text"}],"encrypted_content":null,"status":null},{"id":"msg_90bf446e8768746d","content":[{"annotations":[],"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text","logprobs":null}],"role":"assistant","status":"completed","type":"message","phase":null}],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":"none","tools":[{"name":"tool_search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Search + the client tool catalog for tools that can satisfy the request. Available catalog + entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.","output_schema":null},{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the current weather for a city","output_schema":null},{"name":"agentic_ns__travel__get_timezone","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true,"type":"function","allowed_callers":null,"defer_loading":null,"description":"Get + the IANA time zone for a city","output_schema":null}],"top_p":0.95,"background":false,"max_output_tokens":4096,"max_tool_calls":null,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"completed","text":null,"top_logprobs":null,"truncation":"disabled","usage":{"input_tokens":405,"input_tokens_details":{"cached_tokens":0,"input_tokens_per_turn":[],"cached_tokens_per_turn":[]},"output_tokens":156,"output_tokens_details":{"reasoning_tokens":0,"tool_output_tokens":0,"output_tokens_per_turn":[],"tool_output_tokens_per_turn":[]},"total_tokens":561},"user":null,"presence_penalty":0.0,"frequency_penalty":0.0,"kv_transfer_params":null,"input_messages":null,"output_messages":null},"sequence_number":74,"type":"response.completed"} + + ' + - ' + + ' + status_code: 200 diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml new file mode 100644 index 00000000..573afda8 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml @@ -0,0 +1,949 @@ +turns: +- filename: t1 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: false + tool_choice: required + tools: + - description: Search the client tool catalog for tools that can satisfy the + request. + execution: client + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + type: tool_search + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get the exchange rate between two currencies + name: get_exchange_rate + parameters: + additionalProperties: false + properties: + base: + type: string + quote: + type: string + required: + - base + - quote + type: object + strict: true + type: function + - defer_loading: true + description: Search for hotels in a city + name: search_hotels + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get latitude and longitude for a city + name: get_coordinates + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Calculate the distance between two cities + name: calculate_distance + parameters: + additionalProperties: false + properties: + destination: + type: string + origin: + type: string + required: + - origin + - destination + type: object + strict: true + type: function + type: namespace + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + conversation_id: null + created_at: 1787715672 + error: null + id: resp_01a03c28-5740-7b72-8265-d1eb9060a4d3 + incomplete_details: null + instructions: null + model: Qwen/Qwen3.6-35B-A3B-FP8 + object: response + output: + - content: + - text: 'The user wants me to call `tool_search` exactly once to find two + specific functions: + + 1. A current-weather function + + 2. A travel time-zone function + + + I need to construct a query that searches for both of these capabilities. + The available tools are: + + - get_weather — Get the current weather for a city + + - get_exchange_rate — Get the exchange rate between two currencies + + - search_hotels — Search for hotels in a city + + - travel — Travel location tools + + + The description for `tool_search` is "A concise description of the needed + capabilities." + + + I will search for "current weather and travel time zone functions". + + + Let''s call `tool_search`. + + ' + type: reasoning_text + encrypted_content: null + id: rs_b4aa8121e219577a + status: null + summary: [] + type: reasoning + - arguments: + query: current weather and travel time zone functions + call_id: chatcmpl-tool-b47ff224c79fca2e + execution: client + id: tsc_b6e20137e8f66e99 + status: completed + type: tool_search_call + previous_response_id: null + status: completed + tool_choice: required + tools: + - description: Search the client tool catalog for tools that can satisfy the + request. + execution: client + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + type: tool_search + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get the exchange rate between two currencies + name: get_exchange_rate + parameters: + additionalProperties: false + properties: + base: + type: string + quote: + type: string + required: + - base + - quote + type: object + strict: true + type: function + - defer_loading: true + description: Search for hotels in a city + name: search_hotels + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get latitude and longitude for a city + name: get_coordinates + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Calculate the distance between two cities + name: calculate_distance + parameters: + additionalProperties: false + properties: + destination: + type: string + origin: + type: string + required: + - origin + - destination + type: object + strict: true + type: function + type: namespace + usage: + input_tokens: 394 + input_tokens_details: + cached_tokens: 0 + output_tokens: 176 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 570 + headers: + content-type: application/json + status_code: 200 +- filename: t2 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + - content: + - text: 'The user wants me to call `tool_search` exactly once to find two + specific functions: + + 1. A current-weather function + + 2. A travel time-zone function + + + I need to construct a query that searches for both of these capabilities. + The available tools are: + + - get_weather — Get the current weather for a city + + - get_exchange_rate — Get the exchange rate between two currencies + + - search_hotels — Search for hotels in a city + + - travel — Travel location tools + + + The description for `tool_search` is "A concise description of the needed + capabilities." + + + I will search for "current weather and travel time zone functions". + + + Let''s call `tool_search`. + + ' + type: reasoning_text + encrypted_content: null + id: rs_b4aa8121e219577a + status: null + summary: [] + type: reasoning + - arguments: + query: current weather and travel time zone functions + call_id: chatcmpl-tool-b47ff224c79fca2e + execution: client + id: tsc_b6e20137e8f66e99 + status: completed + type: tool_search_call + - call_id: chatcmpl-tool-b47ff224c79fca2e + execution: client + status: completed + tools: + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + type: tool_search_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: false + tool_choice: + name: get_weather + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + conversation_id: null + created_at: 1787715691 + error: null + id: resp_01a03c28-a046-7840-bb97-d07564b3d6fb + incomplete_details: null + instructions: null + model: Qwen/Qwen3.6-35B-A3B-FP8 + object: response + output: + - content: + - text: 'The user wants me to call the `get_weather` function with the parameter + `{"city": "Paris"}`. + + I have already identified the `get_weather` tool in the previous turn. + + I will now execute the tool call. + + ' + type: reasoning_text + encrypted_content: null + id: rs_9c0f99a8880fe9b1 + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-a88ee0fcceb5cf7f + id: fc_966095bd4ccd6d4f + name: get_weather + status: completed + type: function_call + previous_response_id: null + status: completed + tool_choice: + name: get_weather + type: function + tools: + - description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + usage: + input_tokens: 574 + input_tokens_details: + cached_tokens: 0 + output_tokens: 77 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 651 + headers: + content-type: application/json + status_code: 200 +- filename: t3 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + - content: + - text: 'The user wants me to call `tool_search` exactly once to find two + specific functions: + + 1. A current-weather function + + 2. A travel time-zone function + + + I need to construct a query that searches for both of these capabilities. + The available tools are: + + - get_weather — Get the current weather for a city + + - get_exchange_rate — Get the exchange rate between two currencies + + - search_hotels — Search for hotels in a city + + - travel — Travel location tools + + + The description for `tool_search` is "A concise description of the needed + capabilities." + + + I will search for "current weather and travel time zone functions". + + + Let''s call `tool_search`. + + ' + type: reasoning_text + encrypted_content: null + id: rs_b4aa8121e219577a + status: null + summary: [] + type: reasoning + - arguments: + query: current weather and travel time zone functions + call_id: chatcmpl-tool-b47ff224c79fca2e + execution: client + id: tsc_b6e20137e8f66e99 + status: completed + type: tool_search_call + - call_id: chatcmpl-tool-b47ff224c79fca2e + execution: client + status: completed + tools: + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + type: tool_search_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + - content: + - text: 'The user wants me to call the `get_weather` function with the parameter + `{"city": "Paris"}`. + + I have already identified the `get_weather` tool in the previous turn. + + I will now execute the tool call. + + ' + type: reasoning_text + encrypted_content: null + id: rs_9c0f99a8880fe9b1 + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-a88ee0fcceb5cf7f + id: fc_966095bd4ccd6d4f + name: get_weather + status: completed + type: function_call + - call_id: chatcmpl-tool-a88ee0fcceb5cf7f + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: false + tool_choice: + name: get_timezone + namespace: travel + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + conversation_id: null + created_at: 1787715691 + error: null + id: resp_01a03c28-a30c-7792-b6c5-17d8adab07de + incomplete_details: null + instructions: null + model: Qwen/Qwen3.6-35B-A3B-FP8 + object: response + output: + - content: + - text: 'The user wants me to call the `travel` namespace member `get_timezone` + with the city "Paris". + + Looking at the available tools, I see `agentic_ns__travel__get_timezone` + which matches the description "Get the IANA time zone for a city". + + I need to call it exactly once with `{"city": "Paris"}`. + + Parameters: `city` -> "Paris" + + I will make the tool call. + + ' + type: reasoning_text + encrypted_content: null + id: rs_936b2d8d2c03b5cf + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-a7f43fe38859c312 + id: fc_8239241de8662055 + name: get_timezone + namespace: travel + status: completed + type: function_call + previous_response_id: null + status: completed + tool_choice: + name: get_timezone + namespace: travel + type: function + tools: + - description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + usage: + input_tokens: 658 + input_tokens_details: + cached_tokens: 0 + output_tokens: 128 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 786 + headers: + content-type: application/json + status_code: 200 +- filename: t4 + request: + body: + input: + - content: First call tool_search exactly once to find both a current-weather + function and a travel time-zone function. Do not call any other tool yet. + role: user + type: message + - content: + - text: 'The user wants me to call `tool_search` exactly once to find two + specific functions: + + 1. A current-weather function + + 2. A travel time-zone function + + + I need to construct a query that searches for both of these capabilities. + The available tools are: + + - get_weather — Get the current weather for a city + + - get_exchange_rate — Get the exchange rate between two currencies + + - search_hotels — Search for hotels in a city + + - travel — Travel location tools + + + The description for `tool_search` is "A concise description of the needed + capabilities." + + + I will search for "current weather and travel time zone functions". + + + Let''s call `tool_search`. + + ' + type: reasoning_text + encrypted_content: null + id: rs_b4aa8121e219577a + status: null + summary: [] + type: reasoning + - arguments: + query: current weather and travel time zone functions + call_id: chatcmpl-tool-b47ff224c79fca2e + execution: client + id: tsc_b6e20137e8f66e99 + status: completed + type: tool_search_call + - call_id: chatcmpl-tool-b47ff224c79fca2e + execution: client + status: completed + tools: + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + type: tool_search_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + - content: + - text: 'The user wants me to call the `get_weather` function with the parameter + `{"city": "Paris"}`. + + I have already identified the `get_weather` tool in the previous turn. + + I will now execute the tool call. + + ' + type: reasoning_text + encrypted_content: null + id: rs_9c0f99a8880fe9b1 + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-a88ee0fcceb5cf7f + id: fc_966095bd4ccd6d4f + name: get_weather + status: completed + type: function_call + - call_id: chatcmpl-tool-a88ee0fcceb5cf7f + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + - content: + - text: 'The user wants me to call the `travel` namespace member `get_timezone` + with the city "Paris". + + Looking at the available tools, I see `agentic_ns__travel__get_timezone` + which matches the description "Get the IANA time zone for a city". + + I need to call it exactly once with `{"city": "Paris"}`. + + Parameters: `city` -> "Paris" + + I will make the tool call. + + ' + type: reasoning_text + encrypted_content: null + id: rs_936b2d8d2c03b5cf + status: null + summary: [] + type: reasoning + - arguments: '{"city": "Paris"}' + call_id: chatcmpl-tool-a7f43fe38859c312 + id: fc_8239241de8662055 + name: get_timezone + namespace: travel + status: completed + type: function_call + - call_id: chatcmpl-tool-a7f43fe38859c312 + output: '{"city":"Paris","iana_timezone":"Europe/Paris"}' + type: function_call_output + - content: Use both function outputs and call no more tools. Reply with exactly + PARIS_MIXED_TOOLS_OK. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: false + stream: false + tool_choice: none + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + conversation_id: null + created_at: 1787715692 + error: null + id: resp_01a03c28-a6fc-7303-b399-2272540c0c0b + incomplete_details: null + instructions: null + model: Qwen/Qwen3.6-35B-A3B-FP8 + object: response + output: + - content: + - text: 'The user wants me to acknowledge the function outputs and reply with + a specific string, without calling any more tools. + + I have received outputs from `get_weather` (Paris, 21C, clear) and `travel` + (timezone Europe/Paris). + + The instruction is to reply with "PARIS_MIXED_TOOLS_OK". + + + Plan: + + 1. Output "PARIS_MIXED_TOOLS_OK". + + ' + type: reasoning_text + encrypted_content: null + id: rs_9f78d7593151017c + status: null + summary: [] + type: reasoning + - content: + - annotations: [] + text: ' + + + PARIS_MIXED_TOOLS_OK' + type: output_text + id: msg_bb50d05c0887d441 + role: assistant + status: completed + type: message + previous_response_id: null + status: completed + tool_choice: none + tools: + - description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + usage: + input_tokens: 346 + input_tokens_details: + cached_tokens: 0 + output_tokens: 95 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 441 + headers: + content-type: application/json + status_code: 200 diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml new file mode 100644 index 00000000..0755768c --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml @@ -0,0 +1,3553 @@ +turns: +- filename: t1 + request: + body: + input: First call tool_search exactly once to find both a current-weather function + and a travel time-zone function. Do not call any other tool yet. + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: true + stream: true + tool_choice: required + tools: + - description: Search the client tool catalog for tools that can satisfy the + request. + execution: client + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + type: tool_search + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get the exchange rate between two currencies + name: get_exchange_rate + parameters: + additionalProperties: false + properties: + base: + type: string + quote: + type: string + required: + - base + - quote + type: object + strict: true + type: function + - defer_loading: true + description: Search for hotels in a city + name: search_hotels + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get latitude and longitude for a city + name: get_coordinates + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Calculate the distance between two cities + name: calculate_distance + parameters: + additionalProperties: false + properties: + destination: + type: string + origin: + type: string + required: + - origin + - destination + type: object + strict: true + type: function + type: namespace + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"type":"response.created","sequence_number":0,"response":{"background":false,"created_at":1787715694,"frequency_penalty":0.0,"id":"resp_01a03c28-b034-7bd1-b5d2-5243c66a0a14","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"required","tools":[{"description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"additionalProperties":false,"properties":{"query":{"description":"A + concise description of the needed capabilities.","type":"string"}},"required":["query"],"type":"object"},"type":"tool_search"},{"defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","parameters":{"additionalProperties":false,"properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","parameters":{"additionalProperties":false,"properties":{"destination":{"type":"string"},"origin":{"type":"string"}},"required":["origin","destination"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null}} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"type":"response.in_progress","sequence_number":1,"response":{"background":false,"created_at":1787715694,"frequency_penalty":0.0,"id":"resp_01a03c28-b034-7bd1-b5d2-5243c66a0a14","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"required","tools":[{"description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"additionalProperties":false,"properties":{"query":{"description":"A + concise description of the needed capabilities.","type":"string"}},"required":["query"],"type":"object"},"type":"tool_search"},{"defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","parameters":{"additionalProperties":false,"properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","parameters":{"additionalProperties":false,"properties":{"destination":{"type":"string"},"origin":{"type":"string"}},"required":["origin","destination"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null}} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"content":null,"encrypted_content":null,"id":"a715a17044c59c6f","status":"in_progress","summary":[],"type":"reasoning"}} + + ' + - ' + + ' + - 'event: response.reasoning_part.added + + ' + - 'data: {"type":"response.reasoning_part.added","sequence_number":3,"output_index":0,"content_index":0,"item_id":"a715a17044c59c6f","part":{"text":"","type":"reasoning_text"}} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":4,"output_index":0,"content_index":0,"delta":"The","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":5,"output_index":0,"content_index":0,"delta":" + user wants me","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":6,"output_index":0,"content_index":0,"delta":" + to search for","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":7,"output_index":0,"content_index":0,"delta":" + two","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":8,"output_index":0,"content_index":0,"delta":" + specific tools:","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":9,"output_index":0,"content_index":0,"delta":"\n1.","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":10,"output_index":0,"content_index":0,"delta":" + A current-","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":11,"output_index":0,"content_index":0,"delta":"weather + function\n","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":12,"output_index":0,"content_index":0,"delta":"2. + A","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":13,"output_index":0,"content_index":0,"delta":" + travel time-zone","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":14,"output_index":0,"content_index":0,"delta":" + function\n\nI","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":15,"output_index":0,"content_index":0,"delta":" + need to use","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":16,"output_index":0,"content_index":0,"delta":" + the `tool","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":17,"output_index":0,"content_index":0,"delta":"_search` + function","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":18,"output_index":0,"content_index":0,"delta":" + exactly once with","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":19,"output_index":0,"content_index":0,"delta":" + a query that","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":20,"output_index":0,"content_index":0,"delta":" + describes","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":21,"output_index":0,"content_index":0,"delta":" + both capabilities.","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":22,"output_index":0,"content_index":0,"delta":"\n\nLooking + at","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":23,"output_index":0,"content_index":0,"delta":" + the available tool","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":24,"output_index":0,"content_index":0,"delta":" + catalog","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":25,"output_index":0,"content_index":0,"delta":" + descriptions","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":26,"output_index":0,"content_index":0,"delta":":\n-","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":27,"output_index":0,"content_index":0,"delta":" + `","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":28,"output_index":0,"content_index":0,"delta":"get_weather`","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":29,"output_index":0,"content_index":0,"delta":" + — Get the","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":30,"output_index":0,"content_index":0,"delta":" + current weather for","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":31,"output_index":0,"content_index":0,"delta":" + a city\n","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":32,"output_index":0,"content_index":0,"delta":"- + `get","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":33,"output_index":0,"content_index":0,"delta":"_exchange_rate`","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":34,"output_index":0,"content_index":0,"delta":" + — Get the","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":35,"output_index":0,"content_index":0,"delta":" + exchange rate between","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":36,"output_index":0,"content_index":0,"delta":" + two currencies\n","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":37,"output_index":0,"content_index":0,"delta":"- + `search","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":38,"output_index":0,"content_index":0,"delta":"_hotels`","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":39,"output_index":0,"content_index":0,"delta":" + — Search for","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":40,"output_index":0,"content_index":0,"delta":" + hotels in a","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":41,"output_index":0,"content_index":0,"delta":" + city\n-","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":42,"output_index":0,"content_index":0,"delta":" + `travel`","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":43,"output_index":0,"content_index":0,"delta":" + — Travel location","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":44,"output_index":0,"content_index":0,"delta":" + tools.","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":45,"output_index":0,"content_index":0,"delta":"\n\nThe + `","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":46,"output_index":0,"content_index":0,"delta":"get","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":47,"output_index":0,"content_index":0,"delta":"_weather` + tool","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":48,"output_index":0,"content_index":0,"delta":" + matches \"","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":49,"output_index":0,"content_index":0,"delta":"current-weather","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":50,"output_index":0,"content_index":0,"delta":" + function\".\n","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":51,"output_index":0,"content_index":0,"delta":"The + `travel","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":52,"output_index":0,"content_index":0,"delta":"` + tool likely","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":53,"output_index":0,"content_index":0,"delta":" + contains","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":54,"output_index":0,"content_index":0,"delta":" + or","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":55,"output_index":0,"content_index":0,"delta":" + is","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":56,"output_index":0,"content_index":0,"delta":" + related to \"","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":57,"output_index":0,"content_index":0,"delta":"travel + time-zone","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":58,"output_index":0,"content_index":0,"delta":" + function\" based","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":59,"output_index":0,"content_index":0,"delta":" + on the description","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":60,"output_index":0,"content_index":0,"delta":" + \"","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":61,"output_index":0,"content_index":0,"delta":"Travel + location tools","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":62,"output_index":0,"content_index":0,"delta":"\".\n\nI","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":63,"output_index":0,"content_index":0,"delta":" + will formulate a","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":64,"output_index":0,"content_index":0,"delta":" + query that searches","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":65,"output_index":0,"content_index":0,"delta":" + for both.","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":66,"output_index":0,"content_index":0,"delta":" + \"","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":67,"output_index":0,"content_index":0,"delta":"current","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":68,"output_index":0,"content_index":0,"delta":" + weather function and","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":69,"output_index":0,"content_index":0,"delta":" + travel time-zone","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":70,"output_index":0,"content_index":0,"delta":" + function\".","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":71,"output_index":0,"content_index":0,"delta":"\n\nLet''s","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":72,"output_index":0,"content_index":0,"delta":" + call `","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":73,"output_index":0,"content_index":0,"delta":"tool_search`","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":74,"output_index":0,"content_index":0,"delta":" + with this query","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":75,"output_index":0,"content_index":0,"delta":".\n","item_id":"a715a17044c59c6f"} + + ' + - ' + + ' + - 'event: response.reasoning_text.done + + ' + - 'data: {"type":"response.reasoning_text.done","sequence_number":76,"output_index":0,"content_index":0,"item_id":"a715a17044c59c6f","text":"The + user wants me to search for two specific tools:\n1. A current-weather function\n2. + A travel time-zone function\n\nI need to use the `tool_search` function exactly + once with a query that describes both capabilities.\n\nLooking at the available + tool catalog descriptions:\n- `get_weather` — Get the current weather for a + city\n- `get_exchange_rate` — Get the exchange rate between two currencies\n- + `search_hotels` — Search for hotels in a city\n- `travel` — Travel location + tools.\n\nThe `get_weather` tool matches \"current-weather function\".\nThe + `travel` tool likely contains or is related to \"travel time-zone function\" + based on the description \"Travel location tools\".\n\nI will formulate a query + that searches for both. \"current weather function and travel time-zone function\".\n\nLet''s + call `tool_search` with this query.\n"} + + ' + - ' + + ' + - 'event: response.reasoning_part.done + + ' + - 'data: {"type":"response.reasoning_part.done","sequence_number":77,"output_index":0,"content_index":0,"item_id":"a715a17044c59c6f","part":{"text":"The + user wants me to search for two specific tools:\n1. A current-weather function\n2. + A travel time-zone function\n\nI need to use the `tool_search` function exactly + once with a query that describes both capabilities.\n\nLooking at the available + tool catalog descriptions:\n- `get_weather` — Get the current weather for a + city\n- `get_exchange_rate` — Get the exchange rate between two currencies\n- + `search_hotels` — Search for hotels in a city\n- `travel` — Travel location + tools.\n\nThe `get_weather` tool matches \"current-weather function\".\nThe + `travel` tool likely contains or is related to \"travel time-zone function\" + based on the description \"Travel location tools\".\n\nI will formulate a query + that searches for both. \"current weather function and travel time-zone function\".\n\nLet''s + call `tool_search` with this query.\n","type":"reasoning_text"}} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","sequence_number":78,"output_index":0,"item":{"content":[{"text":"The + user wants me to search for two specific tools:\n1. A current-weather function\n2. + A travel time-zone function\n\nI need to use the `tool_search` function exactly + once with a query that describes both capabilities.\n\nLooking at the available + tool catalog descriptions:\n- `get_weather` — Get the current weather for a + city\n- `get_exchange_rate` — Get the exchange rate between two currencies\n- + `search_hotels` — Search for hotels in a city\n- `travel` — Travel location + tools.\n\nThe `get_weather` tool matches \"current-weather function\".\nThe + `travel` tool likely contains or is related to \"travel time-zone function\" + based on the description \"Travel location tools\".\n\nI will formulate a query + that searches for both. \"current weather function and travel time-zone function\".\n\nLet''s + call `tool_search` with this query.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a715a17044c59c6f","status":"completed","summary":[],"type":"reasoning"}} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","sequence_number":79,"output_index":1,"item":{"arguments":{},"call_id":"call_9dbedda8b73d4737","execution":"client","id":"tsc_0e6b70f150f01d44","status":"in_progress","type":"tool_search_call"}} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","sequence_number":80,"output_index":1,"item":{"arguments":{"query":"current + weather function and travel time-zone function"},"call_id":"call_9dbedda8b73d4737","execution":"client","id":"tsc_0e6b70f150f01d44","status":"completed","type":"tool_search_call"}} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"type":"response.completed","sequence_number":81,"response":{"conversation_id":null,"created_at":1787715695,"error":null,"id":"resp_01a03c28-b034-7bd1-b5d2-5243c66a0a14","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to search for two specific tools:\n1. A current-weather function\n2. + A travel time-zone function\n\nI need to use the `tool_search` function exactly + once with a query that describes both capabilities.\n\nLooking at the available + tool catalog descriptions:\n- `get_weather` — Get the current weather for a + city\n- `get_exchange_rate` — Get the exchange rate between two currencies\n- + `search_hotels` — Search for hotels in a city\n- `travel` — Travel location + tools.\n\nThe `get_weather` tool matches \"current-weather function\".\nThe + `travel` tool likely contains or is related to \"travel time-zone function\" + based on the description \"Travel location tools\".\n\nI will formulate a query + that searches for both. \"current weather function and travel time-zone function\".\n\nLet''s + call `tool_search` with this query.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a715a17044c59c6f","status":null,"summary":[],"type":"reasoning"},{"arguments":{"query":"current + weather function and travel time-zone function"},"call_id":"call_9dbedda8b73d4737","execution":"client","id":"tsc_0e6b70f150f01d44","status":"completed","type":"tool_search_call"}],"previous_response_id":null,"status":"completed","tool_choice":"required","tools":[{"description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"additionalProperties":false,"properties":{"query":{"description":"A + concise description of the needed capabilities.","type":"string"}},"required":["query"],"type":"object"},"type":"tool_search"},{"defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","parameters":{"additionalProperties":false,"properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","parameters":{"additionalProperties":false,"properties":{"destination":{"type":"string"},"origin":{"type":"string"}},"required":["origin","destination"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":394,"input_tokens_details":{"cached_tokens":0},"output_tokens":219,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":613}}} + + ' + - ' + + ' + - 'data: [DONE] + + ' + - ' + + ' + status_code: 200 +- filename: t2 + request: + body: + input: + - call_id: call_9dbedda8b73d4737 + execution: client + status: completed + tools: + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + type: tool_search_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + previous_response_id: resp_01a03c28-b034-7bd1-b5d2-5243c66a0a14 + store: true + stream: true + tool_choice: + name: get_weather + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"type":"response.created","sequence_number":0,"response":{"background":false,"created_at":1787715696,"frequency_penalty":0.0,"id":"resp_01a03c28-b7b8-76d1-93ce-33557fafbad7","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-b034-7bd1-b5d2-5243c66a0a14","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_weather","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null}} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"type":"response.in_progress","sequence_number":1,"response":{"background":false,"created_at":1787715696,"frequency_penalty":0.0,"id":"resp_01a03c28-b7b8-76d1-93ce-33557fafbad7","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-b034-7bd1-b5d2-5243c66a0a14","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_weather","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null}} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"content":null,"encrypted_content":null,"id":"b75ff4c3553a0416","status":"in_progress","summary":[],"type":"reasoning"}} + + ' + - ' + + ' + - 'event: response.reasoning_part.added + + ' + - 'data: {"type":"response.reasoning_part.added","sequence_number":3,"output_index":0,"content_index":0,"item_id":"b75ff4c3553a0416","part":{"text":"","type":"reasoning_text"}} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":4,"output_index":0,"content_index":0,"delta":"The","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":5,"output_index":0,"content_index":0,"delta":" + user wants me","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":6,"output_index":0,"content_index":0,"delta":" + to call `","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":7,"output_index":0,"content_index":0,"delta":"get_weather`","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":8,"output_index":0,"content_index":0,"delta":" + with the city","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":9,"output_index":0,"content_index":0,"delta":" + \"Paris\".","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":10,"output_index":0,"content_index":0,"delta":" + I","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":11,"output_index":0,"content_index":0,"delta":" + need","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":12,"output_index":0,"content_index":0,"delta":" + to make","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":13,"output_index":0,"content_index":0,"delta":" + sure I use","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":14,"output_index":0,"content_index":0,"delta":" + the","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":15,"output_index":0,"content_index":0,"delta":" + exact tool","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":16,"output_index":0,"content_index":0,"delta":" + and","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":17,"output_index":0,"content_index":0,"delta":" + parameters specified","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":18,"output_index":0,"content_index":0,"delta":".\nTool","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":19,"output_index":0,"content_index":0,"delta":": + `get","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":20,"output_index":0,"content_index":0,"delta":"_weather`\n","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":21,"output_index":0,"content_index":0,"delta":"Parameters: + `","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":22,"output_index":0,"content_index":0,"delta":"{\"city\":","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":23,"output_index":0,"content_index":0,"delta":" + \"Paris\"","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":24,"output_index":0,"content_index":0,"delta":"}`\n\n","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":25,"output_index":0,"content_index":0,"delta":"I + will call","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":26,"output_index":0,"content_index":0,"delta":" + the tool now","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":27,"output_index":0,"content_index":0,"delta":".\n","item_id":"b75ff4c3553a0416"} + + ' + - ' + + ' + - 'event: response.reasoning_text.done + + ' + - 'data: {"type":"response.reasoning_text.done","sequence_number":28,"output_index":0,"content_index":0,"item_id":"b75ff4c3553a0416","text":"The + user wants me to call `get_weather` with the city \"Paris\". I need to make + sure I use the exact tool and parameters specified.\nTool: `get_weather`\nParameters: + `{\"city\": \"Paris\"}`\n\nI will call the tool now.\n"} + + ' + - ' + + ' + - 'event: response.reasoning_part.done + + ' + - 'data: {"type":"response.reasoning_part.done","sequence_number":29,"output_index":0,"content_index":0,"item_id":"b75ff4c3553a0416","part":{"text":"The + user wants me to call `get_weather` with the city \"Paris\". I need to make + sure I use the exact tool and parameters specified.\nTool: `get_weather`\nParameters: + `{\"city\": \"Paris\"}`\n\nI will call the tool now.\n","type":"reasoning_text"}} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","sequence_number":30,"output_index":0,"item":{"content":[{"text":"The + user wants me to call `get_weather` with the city \"Paris\". I need to make + sure I use the exact tool and parameters specified.\nTool: `get_weather`\nParameters: + `{\"city\": \"Paris\"}`\n\nI will call the tool now.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"b75ff4c3553a0416","status":"completed","summary":[],"type":"reasoning"}} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","sequence_number":31,"output_index":1,"item":{"arguments":"","call_id":"call_86fcb582d200885e","caller":null,"id":"993aaf1391b2c681","name":"get_weather","namespace":null,"status":"in_progress","type":"function_call"}} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","sequence_number":32,"output_index":1,"delta":"{\"city\": + \"","item_id":"993aaf1391b2c681"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","sequence_number":33,"output_index":1,"delta":"Paris","item_id":"993aaf1391b2c681"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","sequence_number":34,"output_index":1,"delta":"\"}","item_id":"993aaf1391b2c681"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.done + + ' + - 'data: {"type":"response.function_call_arguments.done","sequence_number":35,"output_index":1,"arguments":"{\"city\": + \"Paris\"}","item_id":"993aaf1391b2c681","name":"get_weather"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","sequence_number":36,"output_index":1,"item":{"arguments":"{\"city\": + \"Paris\"}","call_id":"call_86fcb582d200885e","caller":null,"id":"993aaf1391b2c681","name":"get_weather","namespace":null,"status":"completed","type":"function_call"}} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"type":"response.completed","sequence_number":37,"response":{"conversation_id":null,"created_at":1787715697,"error":null,"id":"resp_01a03c28-b7b8-76d1-93ce-33557fafbad7","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to call `get_weather` with the city \"Paris\". I need to make + sure I use the exact tool and parameters specified.\nTool: `get_weather`\nParameters: + `{\"city\": \"Paris\"}`\n\nI will call the tool now.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"b75ff4c3553a0416","status":null,"summary":[],"type":"reasoning"},{"arguments":"{\"city\": + \"Paris\"}","call_id":"call_86fcb582d200885e","id":"993aaf1391b2c681","name":"get_weather","status":"completed","type":"function_call"}],"previous_response_id":"resp_01a03c28-b034-7bd1-b5d2-5243c66a0a14","status":"completed","tool_choice":{"name":"get_weather","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":718,"input_tokens_details":{"cached_tokens":0},"output_tokens":84,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":802}}} + + ' + - ' + + ' + - 'data: [DONE] + + ' + - ' + + ' + status_code: 200 +- filename: t3 + request: + body: + input: + - call_id: call_86fcb582d200885e + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + previous_response_id: resp_01a03c28-b7b8-76d1-93ce-33557fafbad7 + store: true + stream: true + tool_choice: + name: get_timezone + namespace: travel + type: function + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"type":"response.created","sequence_number":0,"response":{"background":false,"created_at":1787715698,"frequency_penalty":0.0,"id":"resp_01a03c28-bdd0-7c82-9e95-42037516c3de","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-b7b8-76d1-93ce-33557fafbad7","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_timezone","namespace":"travel","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null}} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"type":"response.in_progress","sequence_number":1,"response":{"background":false,"created_at":1787715698,"frequency_penalty":0.0,"id":"resp_01a03c28-bdd0-7c82-9e95-42037516c3de","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-b7b8-76d1-93ce-33557fafbad7","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_timezone","namespace":"travel","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null}} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"content":null,"encrypted_content":null,"id":"a52b36823d861ad5","status":"in_progress","summary":[],"type":"reasoning"}} + + ' + - ' + + ' + - 'event: response.reasoning_part.added + + ' + - 'data: {"type":"response.reasoning_part.added","sequence_number":3,"output_index":0,"content_index":0,"item_id":"a52b36823d861ad5","part":{"text":"","type":"reasoning_text"}} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":4,"output_index":0,"content_index":0,"delta":"The","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":5,"output_index":0,"content_index":0,"delta":" + user wants me","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":6,"output_index":0,"content_index":0,"delta":" + to call the","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":7,"output_index":0,"content_index":0,"delta":" + `get_timezone","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":8,"output_index":0,"content_index":0,"delta":"` + function from","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":9,"output_index":0,"content_index":0,"delta":" + the `travel","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":10,"output_index":0,"content_index":0,"delta":"` + namespace,","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":11,"output_index":0,"content_index":0,"delta":" + which was","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":12,"output_index":0,"content_index":0,"delta":" + loaded in the","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":13,"output_index":0,"content_index":0,"delta":" + previous tool","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":14,"output_index":0,"content_index":0,"delta":" + search result","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":15,"output_index":0,"content_index":0,"delta":". + The","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":16,"output_index":0,"content_index":0,"delta":" + parameters","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":17,"output_index":0,"content_index":0,"delta":" + should","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":18,"output_index":0,"content_index":0,"delta":" + be `{\"","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":19,"output_index":0,"content_index":0,"delta":"city\": + \"","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":20,"output_index":0,"content_index":0,"delta":"Paris\"}`","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":21,"output_index":0,"content_index":0,"delta":".\n","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":22,"output_index":0,"content_index":0,"delta":"I + need to","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":23,"output_index":0,"content_index":0,"delta":" + make","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":24,"output_index":0,"content_index":0,"delta":" + sure I call","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":25,"output_index":0,"content_index":0,"delta":" + exactly","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":26,"output_index":0,"content_index":0,"delta":" + this","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":27,"output_index":0,"content_index":0,"delta":" + tool and no","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":28,"output_index":0,"content_index":0,"delta":" + others","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":29,"output_index":0,"content_index":0,"delta":".\nThe","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":30,"output_index":0,"content_index":0,"delta":" + tool","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":31,"output_index":0,"content_index":0,"delta":" + is","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":32,"output_index":0,"content_index":0,"delta":" + `agentic","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":33,"output_index":0,"content_index":0,"delta":"_ns__travel","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":34,"output_index":0,"content_index":0,"delta":"__get_timezone","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":35,"output_index":0,"content_index":0,"delta":"`","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":36,"output_index":0,"content_index":0,"delta":" + based on the","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":37,"output_index":0,"content_index":0,"delta":" + schema provided","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":38,"output_index":0,"content_index":0,"delta":" + in","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":39,"output_index":0,"content_index":0,"delta":" + the system prompt","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":40,"output_index":0,"content_index":0,"delta":", + but usually","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":41,"output_index":0,"content_index":0,"delta":" + I","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":42,"output_index":0,"content_index":0,"delta":" + just call `","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":43,"output_index":0,"content_index":0,"delta":"travel","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":44,"output_index":0,"content_index":0,"delta":".get","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":45,"output_index":0,"content_index":0,"delta":"_timezone` + or","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":46,"output_index":0,"content_index":0,"delta":" + use","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":47,"output_index":0,"content_index":0,"delta":" + the full","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":48,"output_index":0,"content_index":0,"delta":" + name. Wait","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":49,"output_index":0,"content_index":0,"delta":", + looking","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":50,"output_index":0,"content_index":0,"delta":" + at the system","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":51,"output_index":0,"content_index":0,"delta":" + prompt, the","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":52,"output_index":0,"content_index":0,"delta":" + available tools are","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":53,"output_index":0,"content_index":0,"delta":":\n-","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":54,"output_index":0,"content_index":0,"delta":" + `get_exchange","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":55,"output_index":0,"content_index":0,"delta":"_rate`\n","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":56,"output_index":0,"content_index":0,"delta":"- + `search","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":57,"output_index":0,"content_index":0,"delta":"_hotels`","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":58,"output_index":0,"content_index":0,"delta":"\n- + `","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":59,"output_index":0,"content_index":0,"delta":"travel` + (","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":60,"output_index":0,"content_index":0,"delta":"namespace)\n","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":61,"output_index":0,"content_index":0,"delta":"And","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":62,"output_index":0,"content_index":0,"delta":" + the tool","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":63,"output_index":0,"content_index":0,"delta":" + description for","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":64,"output_index":0,"content_index":0,"delta":" + `get_timezone","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":65,"output_index":0,"content_index":0,"delta":"` + says `","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":66,"output_index":0,"content_index":0,"delta":"agentic_ns","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":67,"output_index":0,"content_index":0,"delta":"__travel__","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":68,"output_index":0,"content_index":0,"delta":"get_timezone`","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":69,"output_index":0,"content_index":0,"delta":" + in the schema","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":70,"output_index":0,"content_index":0,"delta":".","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":71,"output_index":0,"content_index":0,"delta":"\nLet","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":72,"output_index":0,"content_index":0,"delta":"''s + check the","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":73,"output_index":0,"content_index":0,"delta":" + exact name to","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":74,"output_index":0,"content_index":0,"delta":" + use.","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":75,"output_index":0,"content_index":0,"delta":" + The prompt","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":76,"output_index":0,"content_index":0,"delta":" + says: `","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":77,"output_index":0,"content_index":0,"delta":"agentic_ns","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":78,"output_index":0,"content_index":0,"delta":"__travel__","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":79,"output_index":0,"content_index":0,"delta":"get_timezone`","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":80,"output_index":0,"content_index":0,"delta":" + is","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":81,"output_index":0,"content_index":0,"delta":" + the","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":82,"output_index":0,"content_index":0,"delta":" + function name in","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":83,"output_index":0,"content_index":0,"delta":" + the schema.","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":84,"output_index":0,"content_index":0,"delta":"\nParameters","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":85,"output_index":0,"content_index":0,"delta":": + `{\"","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":86,"output_index":0,"content_index":0,"delta":"city\": + \"","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":87,"output_index":0,"content_index":0,"delta":"Paris\"}`","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":88,"output_index":0,"content_index":0,"delta":".\nI","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":89,"output_index":0,"content_index":0,"delta":" + will call it","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":90,"output_index":0,"content_index":0,"delta":" + now.\n","item_id":"a52b36823d861ad5"} + + ' + - ' + + ' + - 'event: response.reasoning_text.done + + ' + - 'data: {"type":"response.reasoning_text.done","sequence_number":91,"output_index":0,"content_index":0,"item_id":"a52b36823d861ad5","text":"The + user wants me to call the `get_timezone` function from the `travel` namespace, + which was loaded in the previous tool search result. The parameters should be + `{\"city\": \"Paris\"}`.\nI need to make sure I call exactly this tool and no + others.\nThe tool is `agentic_ns__travel__get_timezone` based on the schema + provided in the system prompt, but usually I just call `travel.get_timezone` + or use the full name. Wait, looking at the system prompt, the available tools + are:\n- `get_exchange_rate`\n- `search_hotels`\n- `travel` (namespace)\nAnd + the tool description for `get_timezone` says `agentic_ns__travel__get_timezone` + in the schema.\nLet''s check the exact name to use. The prompt says: `agentic_ns__travel__get_timezone` + is the function name in the schema.\nParameters: `{\"city\": \"Paris\"}`.\nI + will call it now.\n"} + + ' + - ' + + ' + - 'event: response.reasoning_part.done + + ' + - 'data: {"type":"response.reasoning_part.done","sequence_number":92,"output_index":0,"content_index":0,"item_id":"a52b36823d861ad5","part":{"text":"The + user wants me to call the `get_timezone` function from the `travel` namespace, + which was loaded in the previous tool search result. The parameters should be + `{\"city\": \"Paris\"}`.\nI need to make sure I call exactly this tool and no + others.\nThe tool is `agentic_ns__travel__get_timezone` based on the schema + provided in the system prompt, but usually I just call `travel.get_timezone` + or use the full name. Wait, looking at the system prompt, the available tools + are:\n- `get_exchange_rate`\n- `search_hotels`\n- `travel` (namespace)\nAnd + the tool description for `get_timezone` says `agentic_ns__travel__get_timezone` + in the schema.\nLet''s check the exact name to use. The prompt says: `agentic_ns__travel__get_timezone` + is the function name in the schema.\nParameters: `{\"city\": \"Paris\"}`.\nI + will call it now.\n","type":"reasoning_text"}} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","sequence_number":93,"output_index":0,"item":{"content":[{"text":"The + user wants me to call the `get_timezone` function from the `travel` namespace, + which was loaded in the previous tool search result. The parameters should be + `{\"city\": \"Paris\"}`.\nI need to make sure I call exactly this tool and no + others.\nThe tool is `agentic_ns__travel__get_timezone` based on the schema + provided in the system prompt, but usually I just call `travel.get_timezone` + or use the full name. Wait, looking at the system prompt, the available tools + are:\n- `get_exchange_rate`\n- `search_hotels`\n- `travel` (namespace)\nAnd + the tool description for `get_timezone` says `agentic_ns__travel__get_timezone` + in the schema.\nLet''s check the exact name to use. The prompt says: `agentic_ns__travel__get_timezone` + is the function name in the schema.\nParameters: `{\"city\": \"Paris\"}`.\nI + will call it now.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a52b36823d861ad5","status":"completed","summary":[],"type":"reasoning"}} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","sequence_number":94,"output_index":1,"item":{"arguments":"","call_id":"call_a84a81ee9e4ff247","caller":null,"id":"a38fa81e31f2efa8","name":"get_timezone","namespace":"travel","status":"in_progress","type":"function_call"}} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","sequence_number":95,"output_index":1,"delta":"{\"city\": + \"Paris","item_id":"a38fa81e31f2efa8"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","sequence_number":96,"output_index":1,"delta":"\"}","item_id":"a38fa81e31f2efa8"} + + ' + - ' + + ' + - 'event: response.function_call_arguments.done + + ' + - 'data: {"type":"response.function_call_arguments.done","sequence_number":97,"output_index":1,"arguments":"{\"city\": + \"Paris\"}","item_id":"a38fa81e31f2efa8","name":"agentic_ns__travel__get_timezone"} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","sequence_number":98,"output_index":1,"item":{"arguments":"{\"city\": + \"Paris\"}","call_id":"call_a84a81ee9e4ff247","caller":null,"id":"a38fa81e31f2efa8","name":"get_timezone","namespace":"travel","status":"completed","type":"function_call"}} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"type":"response.completed","sequence_number":99,"response":{"conversation_id":null,"created_at":1787715699,"error":null,"id":"resp_01a03c28-bdd0-7c82-9e95-42037516c3de","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to call the `get_timezone` function from the `travel` namespace, + which was loaded in the previous tool search result. The parameters should be + `{\"city\": \"Paris\"}`.\nI need to make sure I call exactly this tool and no + others.\nThe tool is `agentic_ns__travel__get_timezone` based on the schema + provided in the system prompt, but usually I just call `travel.get_timezone` + or use the full name. Wait, looking at the system prompt, the available tools + are:\n- `get_exchange_rate`\n- `search_hotels`\n- `travel` (namespace)\nAnd + the tool description for `get_timezone` says `agentic_ns__travel__get_timezone` + in the schema.\nLet''s check the exact name to use. The prompt says: `agentic_ns__travel__get_timezone` + is the function name in the schema.\nParameters: `{\"city\": \"Paris\"}`.\nI + will call it now.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a52b36823d861ad5","status":null,"summary":[],"type":"reasoning"},{"arguments":"{\"city\": + \"Paris\"}","call_id":"call_a84a81ee9e4ff247","id":"a38fa81e31f2efa8","name":"get_timezone","namespace":"travel","status":"completed","type":"function_call"}],"previous_response_id":"resp_01a03c28-b7b8-76d1-93ce-33557fafbad7","status":"completed","tool_choice":{"name":"get_timezone","namespace":"travel","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":802,"input_tokens_details":{"cached_tokens":0},"output_tokens":243,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1045}}} + + ' + - ' + + ' + - 'data: [DONE] + + ' + - ' + + ' + status_code: 200 +- filename: t4 + request: + body: + input: + - call_id: call_a84a81ee9e4ff247 + output: '{"city":"Paris","iana_timezone":"Europe/Paris"}' + type: function_call_output + - content: Use both function outputs and call no more tools. Reply with exactly + PARIS_MIXED_TOOLS_OK. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + previous_response_id: resp_01a03c28-bdd0-7c82-9e95-42037516c3de + store: true + stream: true + tool_choice: none + headers: + accept: '*/*' + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"type":"response.created","sequence_number":0,"response":{"background":false,"created_at":1787715699,"frequency_penalty":0.0,"id":"resp_01a03c28-c49d-7d72-9538-73724b7ad60f","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-bdd0-7c82-9e95-42037516c3de","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"none","tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null}} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"type":"response.in_progress","sequence_number":1,"response":{"background":false,"created_at":1787715699,"frequency_penalty":0.0,"id":"resp_01a03c28-c49d-7d72-9538-73724b7ad60f","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-bdd0-7c82-9e95-42037516c3de","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"none","tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null}} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"content":null,"encrypted_content":null,"id":"be6b0d01ce538c64","status":"in_progress","summary":[],"type":"reasoning"}} + + ' + - ' + + ' + - 'event: response.reasoning_part.added + + ' + - 'data: {"type":"response.reasoning_part.added","sequence_number":3,"output_index":0,"content_index":0,"item_id":"be6b0d01ce538c64","part":{"text":"","type":"reasoning_text"}} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":4,"output_index":0,"content_index":0,"delta":"The","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":5,"output_index":0,"content_index":0,"delta":" + user wants me","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":6,"output_index":0,"content_index":0,"delta":" + to combine","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":7,"output_index":0,"content_index":0,"delta":" + the information","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":8,"output_index":0,"content_index":0,"delta":" + from the","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":9,"output_index":0,"content_index":0,"delta":" + previous tool calls","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":10,"output_index":0,"content_index":0,"delta":" + and output a","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":11,"output_index":0,"content_index":0,"delta":" + specific string.","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":12,"output_index":0,"content_index":0,"delta":"\n\n","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":13,"output_index":0,"content_index":0,"delta":"1. + ","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":14,"output_index":0,"content_index":0,"delta":" + **Review","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":15,"output_index":0,"content_index":0,"delta":" + previous","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":16,"output_index":0,"content_index":0,"delta":" + tool outputs:**","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":17,"output_index":0,"content_index":0,"delta":"\n *","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":18,"output_index":0,"content_index":0,"delta":" `get","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":19,"output_index":0,"content_index":0,"delta":"_weather`","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":20,"output_index":0,"content_index":0,"delta":" + for Paris","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":21,"output_index":0,"content_index":0,"delta":" + returned: `","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":22,"output_index":0,"content_index":0,"delta":"{\"city\":\"","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":23,"output_index":0,"content_index":0,"delta":"Paris\",\"condition","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":24,"output_index":0,"content_index":0,"delta":"\":\"clear\",\"","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":25,"output_index":0,"content_index":0,"delta":"temperature_c\":","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":26,"output_index":0,"content_index":0,"delta":"21}`","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":27,"output_index":0,"content_index":0,"delta":".\n ","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":28,"output_index":0,"content_index":0,"delta":" + * `","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":29,"output_index":0,"content_index":0,"delta":"travel","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":30,"output_index":0,"content_index":0,"delta":".get","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":31,"output_index":0,"content_index":0,"delta":"_timezone` + for","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":32,"output_index":0,"content_index":0,"delta":" + Paris returned:","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":33,"output_index":0,"content_index":0,"delta":" + `{\"city","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":34,"output_index":0,"content_index":0,"delta":"\":\"Paris\",\"","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":35,"output_index":0,"content_index":0,"delta":"iana_timezone\":\"","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":36,"output_index":0,"content_index":0,"delta":"Europe/Paris","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":37,"output_index":0,"content_index":0,"delta":"\"}`.","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":38,"output_index":0,"content_index":0,"delta":"\n\n2.","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":39,"output_index":0,"content_index":0,"delta":" **Instruction","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":40,"output_index":0,"content_index":0,"delta":":** + \"Use","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":41,"output_index":0,"content_index":0,"delta":" + both function outputs","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":42,"output_index":0,"content_index":0,"delta":" + and call no","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":43,"output_index":0,"content_index":0,"delta":" + more tools.","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":44,"output_index":0,"content_index":0,"delta":" + Reply with exactly","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":45,"output_index":0,"content_index":0,"delta":" + PARIS_MIXED","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":46,"output_index":0,"content_index":0,"delta":"_TOOLS_OK","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":47,"output_index":0,"content_index":0,"delta":".\"\n\n3","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":48,"output_index":0,"content_index":0,"delta":". **","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":49,"output_index":0,"content_index":0,"delta":"Action:** + The","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":50,"output_index":0,"content_index":0,"delta":" + specific","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":51,"output_index":0,"content_index":0,"delta":" + instruction regarding","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":52,"output_index":0,"content_index":0,"delta":" + the content","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":53,"output_index":0,"content_index":0,"delta":" + (\"","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":54,"output_index":0,"content_index":0,"delta":"Use + both function","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":55,"output_index":0,"content_index":0,"delta":" + outputs\") seems","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":56,"output_index":0,"content_index":0,"delta":" + to be context","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":57,"output_index":0,"content_index":0,"delta":" + for the final","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":58,"output_index":0,"content_index":0,"delta":" + verification","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":59,"output_index":0,"content_index":0,"delta":", + but the","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":60,"output_index":0,"content_index":0,"delta":" + explicit output","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":61,"output_index":0,"content_index":0,"delta":" + instruction","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":62,"output_index":0,"content_index":0,"delta":" + is \"Reply","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":63,"output_index":0,"content_index":0,"delta":" + with exactly PARIS","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":64,"output_index":0,"content_index":0,"delta":"_MIXED_TO","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":65,"output_index":0,"content_index":0,"delta":"OLS_OK.\"","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":66,"output_index":0,"content_index":0,"delta":"\n\n4.","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":67,"output_index":0,"content_index":0,"delta":" **Constraint","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":68,"output_index":0,"content_index":0,"delta":" + Check:** \"","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":69,"output_index":0,"content_index":0,"delta":"call + no more","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":70,"output_index":0,"content_index":0,"delta":" + tools\"","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":71,"output_index":0,"content_index":0,"delta":" + ->","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":72,"output_index":0,"content_index":0,"delta":" + I will","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":73,"output_index":0,"content_index":0,"delta":" + not call any","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":74,"output_index":0,"content_index":0,"delta":" + tool","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":75,"output_index":0,"content_index":0,"delta":". + \"","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":76,"output_index":0,"content_index":0,"delta":"Reply + with exactly","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":77,"output_index":0,"content_index":0,"delta":"...\" + -> I","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":78,"output_index":0,"content_index":0,"delta":" + will print","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":79,"output_index":0,"content_index":0,"delta":" + the","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":80,"output_index":0,"content_index":0,"delta":" + specific","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":81,"output_index":0,"content_index":0,"delta":" + text","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":82,"output_index":0,"content_index":0,"delta":".\n\n5","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":83,"output_index":0,"content_index":0,"delta":". **","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":84,"output_index":0,"content_index":0,"delta":"Final + Output:**","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":85,"output_index":0,"content_index":0,"delta":" + PARIS_MIXED","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":86,"output_index":0,"content_index":0,"delta":"_TOOLS_OK","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.delta + + ' + - 'data: {"type":"response.reasoning_text.delta","sequence_number":87,"output_index":0,"content_index":0,"delta":"\n","item_id":"be6b0d01ce538c64"} + + ' + - ' + + ' + - 'event: response.reasoning_text.done + + ' + - 'data: {"type":"response.reasoning_text.done","sequence_number":88,"output_index":0,"content_index":0,"item_id":"be6b0d01ce538c64","text":"The + user wants me to combine the information from the previous tool calls and output + a specific string.\n\n1. **Review previous tool outputs:**\n * `get_weather` + for Paris returned: `{\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}`.\n * `travel.get_timezone` + for Paris returned: `{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}`.\n\n2. **Instruction:** + \"Use both function outputs and call no more tools. Reply with exactly PARIS_MIXED_TOOLS_OK.\"\n\n3. **Action:** + The specific instruction regarding the content (\"Use both function outputs\") + seems to be context for the final verification, but the explicit output instruction + is \"Reply with exactly PARIS_MIXED_TOOLS_OK.\"\n\n4. **Constraint Check:** + \"call no more tools\" -> I will not call any tool. \"Reply with exactly...\" + -> I will print the specific text.\n\n5. **Final Output:** PARIS_MIXED_TOOLS_OK\n"} + + ' + - ' + + ' + - 'event: response.reasoning_part.done + + ' + - 'data: {"type":"response.reasoning_part.done","sequence_number":89,"output_index":0,"content_index":0,"item_id":"be6b0d01ce538c64","part":{"text":"The + user wants me to combine the information from the previous tool calls and output + a specific string.\n\n1. **Review previous tool outputs:**\n * `get_weather` + for Paris returned: `{\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}`.\n * `travel.get_timezone` + for Paris returned: `{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}`.\n\n2. **Instruction:** + \"Use both function outputs and call no more tools. Reply with exactly PARIS_MIXED_TOOLS_OK.\"\n\n3. **Action:** + The specific instruction regarding the content (\"Use both function outputs\") + seems to be context for the final verification, but the explicit output instruction + is \"Reply with exactly PARIS_MIXED_TOOLS_OK.\"\n\n4. **Constraint Check:** + \"call no more tools\" -> I will not call any tool. \"Reply with exactly...\" + -> I will print the specific text.\n\n5. **Final Output:** PARIS_MIXED_TOOLS_OK\n","type":"reasoning_text"}} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","sequence_number":90,"output_index":0,"item":{"content":[{"text":"The + user wants me to combine the information from the previous tool calls and output + a specific string.\n\n1. **Review previous tool outputs:**\n * `get_weather` + for Paris returned: `{\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}`.\n * `travel.get_timezone` + for Paris returned: `{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}`.\n\n2. **Instruction:** + \"Use both function outputs and call no more tools. Reply with exactly PARIS_MIXED_TOOLS_OK.\"\n\n3. **Action:** + The specific instruction regarding the content (\"Use both function outputs\") + seems to be context for the final verification, but the explicit output instruction + is \"Reply with exactly PARIS_MIXED_TOOLS_OK.\"\n\n4. **Constraint Check:** + \"call no more tools\" -> I will not call any tool. \"Reply with exactly...\" + -> I will print the specific text.\n\n5. **Final Output:** PARIS_MIXED_TOOLS_OK\n","type":"reasoning_text"}],"encrypted_content":null,"id":"be6b0d01ce538c64","status":"completed","summary":[],"type":"reasoning"}} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","sequence_number":91,"output_index":1,"item":{"content":[],"id":"bb25dfecba62be6b","phase":null,"role":"assistant","status":"in_progress","type":"message"}} + + ' + - ' + + ' + - 'event: response.content_part.added + + ' + - 'data: {"type":"response.content_part.added","sequence_number":92,"output_index":1,"content_index":0,"item_id":"bb25dfecba62be6b","part":{"annotations":[],"logprobs":[],"text":"","type":"output_text"}} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","sequence_number":93,"output_index":1,"content_index":0,"delta":"\n\nPAR","item_id":"bb25dfecba62be6b","logprobs":[]} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","sequence_number":94,"output_index":1,"content_index":0,"delta":"IS_MIXED","item_id":"bb25dfecba62be6b","logprobs":[]} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","sequence_number":95,"output_index":1,"content_index":0,"delta":"_TOOLS_OK","item_id":"bb25dfecba62be6b","logprobs":[]} + + ' + - ' + + ' + - 'event: response.output_text.done + + ' + - 'data: {"type":"response.output_text.done","sequence_number":96,"output_index":1,"content_index":0,"item_id":"bb25dfecba62be6b","logprobs":[],"text":"\n\nPARIS_MIXED_TOOLS_OK"} + + ' + - ' + + ' + - 'event: response.content_part.done + + ' + - 'data: {"type":"response.content_part.done","sequence_number":97,"output_index":1,"content_index":0,"item_id":"bb25dfecba62be6b","part":{"annotations":[],"logprobs":null,"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text"}} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","sequence_number":98,"output_index":1,"item":{"content":[{"annotations":[],"logprobs":null,"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text"}],"id":"bb25dfecba62be6b","phase":null,"role":"assistant","status":"completed","summary":[],"type":"message"}} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"type":"response.completed","sequence_number":99,"response":{"conversation_id":null,"created_at":1787715700,"error":null,"id":"resp_01a03c28-c49d-7d72-9538-73724b7ad60f","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to combine the information from the previous tool calls and output + a specific string.\n\n1. **Review previous tool outputs:**\n * `get_weather` + for Paris returned: `{\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}`.\n * `travel.get_timezone` + for Paris returned: `{\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}`.\n\n2. **Instruction:** + \"Use both function outputs and call no more tools. Reply with exactly PARIS_MIXED_TOOLS_OK.\"\n\n3. **Action:** + The specific instruction regarding the content (\"Use both function outputs\") + seems to be context for the final verification, but the explicit output instruction + is \"Reply with exactly PARIS_MIXED_TOOLS_OK.\"\n\n4. **Constraint Check:** + \"call no more tools\" -> I will not call any tool. \"Reply with exactly...\" + -> I will print the specific text.\n\n5. **Final Output:** PARIS_MIXED_TOOLS_OK\n","type":"reasoning_text"}],"encrypted_content":null,"id":"be6b0d01ce538c64","status":null,"summary":[],"type":"reasoning"},{"content":[{"annotations":[],"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text"}],"id":"bb25dfecba62be6b","role":"assistant","status":"completed","type":"message"}],"previous_response_id":"resp_01a03c28-bdd0-7c82-9e95-42037516c3de","status":"completed","tool_choice":"none","tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":347,"input_tokens_details":{"cached_tokens":0},"output_tokens":219,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":566}}} + + ' + - ' + + ' + - 'data: [DONE] + + ' + - ' + + ' + status_code: 200 diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-websocket.yaml b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-websocket.yaml new file mode 100644 index 00000000..e8ef7e80 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-websocket.yaml @@ -0,0 +1,5401 @@ +turns: +- filename: t1 + request: + body: + input: First call tool_search exactly once to find both a current-weather function + and a travel time-zone function. Do not call any other tool yet. + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + store: true + tool_choice: required + tools: + - description: Search the client tool catalog for tools that can satisfy the + request. + execution: client + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + type: tool_search + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get the exchange rate between two currencies + name: get_exchange_rate + parameters: + additionalProperties: false + properties: + base: + type: string + quote: + type: string + required: + - base + - quote + type: object + strict: true + type: function + - defer_loading: true + description: Search for hotels in a city + name: search_hotels + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get latitude and longitude for a city + name: get_coordinates + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Calculate the distance between two cities + name: calculate_distance + parameters: + additionalProperties: false + properties: + destination: + type: string + origin: + type: string + required: + - origin + - destination + type: object + strict: true + type: function + type: namespace + type: response.create + headers: {} + method: WEBSOCKET + path: /v1/responses + query_params: {} + transport: websocket + response: + headers: + transport: websocket + sse: + - 'event: response.created + + data: {"response":{"background":false,"created_at":1787715702,"frequency_penalty":0.0,"id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"required","tools":[{"description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"additionalProperties":false,"properties":{"query":{"description":"A + concise description of the needed capabilities.","type":"string"}},"required":["query"],"type":"object"},"type":"tool_search"},{"defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","parameters":{"additionalProperties":false,"properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","parameters":{"additionalProperties":false,"properties":{"destination":{"type":"string"},"origin":{"type":"string"}},"required":["origin","destination"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"} + + ' + - 'event: response.in_progress + + data: {"response":{"background":false,"created_at":1787715702,"frequency_penalty":0.0,"id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"required","tools":[{"description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"additionalProperties":false,"properties":{"query":{"description":"A + concise description of the needed capabilities.","type":"string"}},"required":["query"],"type":"object"},"type":"tool_search"},{"defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","parameters":{"additionalProperties":false,"properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","parameters":{"additionalProperties":false,"properties":{"destination":{"type":"string"},"origin":{"type":"string"}},"required":["origin","destination"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"} + + ' + - 'event: response.output_item.added + + data: {"item":{"content":null,"encrypted_content":null,"id":"a7b8cae9f47f4dc2","status":"in_progress","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} + + ' + - 'event: response.reasoning_part.added + + data: {"content_index":0,"item_id":"a7b8cae9f47f4dc2","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" user wants me","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" to find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" two","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" specific tools using","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"tool_search`:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\n1.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" A current-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"weather function\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"2. A","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" travel time-zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" function\n\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" need to call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `tool_search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"` exactly once","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" I","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" should construct","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" a query that","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" captures","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" description","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" of these","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" capabilities.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" available tools listed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" in the system","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" prompt are:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"get_weather`,","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `get_exchange","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_rate`, `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"search_hotels","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`, `travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`.\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"get_weather`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" seems to match","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"current-weather","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" function\".\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`travel`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" seems to match","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \"travel time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"-zone function\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" (or perhaps","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" I","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" should","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" search for \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\" specifically","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":").\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Let''s formulate","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" query that searches","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for both or","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" asks","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":63,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" to return","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":64,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" results","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":65,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for both types","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":66,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" of capabilities.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":67,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":68,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" prompt says \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":69,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":70,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the client tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":71,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" catalog for tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":72,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" that can satisfy","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":73,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the request.\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":74,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":75,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":76,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" idea","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":77,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":": \"tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":78,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for current weather","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":79,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and travel time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":80,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" zones\"\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":81,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Let''s check","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":82,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the descriptions","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":83,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" in","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":84,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the prompt:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":85,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\n`get","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":86,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_weather` \u2014","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":87,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Get the current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":88,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" weather for a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":89,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" city\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":90,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"travel` \u2014","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":91,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Travel location tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":92,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":93,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" will construct","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":94,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":95,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":96,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" that","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":97,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" asks","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":98,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":99,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" weather and travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":100,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":101,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tools.\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":102,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":103,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":": \"current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":104,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" weather and travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":105,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" time zone tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":106,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\"\n\nWait","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":107,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":", the prompt","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":108,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" says \"find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":109,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" both a current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":110,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"-weather function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":111,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and a travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":112,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" time-zone function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":113,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\"\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":114,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" will call `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":115,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"tool_search`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":116,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" with this","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":117,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" query.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":118,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"I","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":119,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" must","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":120,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" ensure I only","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":121,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" call it once","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":122,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":123,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Parameters","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":124,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":":\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":125,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":126,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":": \"current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":127,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" weather and travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":128,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" time zone tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":129,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\"\n\nLet","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":130,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"''s do","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":131,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" it. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":132,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Actually","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":133,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":", the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":134,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" description","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":135,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for `travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":136,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"` says","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":137,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \"Travel location","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":138,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tools.\" It","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":139,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" might not","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":140,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" explicitly say","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":141,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" time zone,","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":142,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" but I","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":143,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"''ll search for","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":144,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" it. The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":145,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" user''s","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":146,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" instruction is to","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":147,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":148,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `tool_search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":149,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"` exactly once","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":150,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" to find them","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":151,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":152,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" will proceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":153,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \nCall","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":154,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":": `tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":155,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_search(query=\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":156,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"current weather and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":157,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" travel time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":158,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tools\")`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":159,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nCheck","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":160,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" constraints: \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":161,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"First call tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":162,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_search exactly once","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":163,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"... Do not","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":164,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" call any other","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":165,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tool yet.\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":166,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" -> S","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":167,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"atisfied.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":168,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Proceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":169,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \nOutput","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":170,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" matches schema","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":171,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\nDone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":172,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \nWait","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":173,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":", let''s","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":174,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" look","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":175,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" at the exact","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":176,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" wording","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":177,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":": \"find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":178,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" both a current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":179,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"-weather function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":180,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and a travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":181,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" time-zone function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":182,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\"\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":183,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"''ll use","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":184,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" that","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":185,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" ph","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":186,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"rasing in","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":187,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the query.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":188,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":189,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"tool_search(query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":190,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"=\"find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":191,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" a current-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":192,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"weather function and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":193,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" a travel time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":194,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"-zone function\")","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":195,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`\nThis","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":196,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" should","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":197,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" work.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":198,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Let''s generate","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":199,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":200,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":201,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \nAll","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":202,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" good","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":203,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \nProceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":204,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"ing. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":205,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"[Self-C","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":206,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"orrection/","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":207,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Verification during thought","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":208,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"]\nThe","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":209,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" prompt says:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":210,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \"Search the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":211,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" client tool catalog","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":212,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for tools that","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":213,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" can satisfy the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":214,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" request. Available","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":215,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" catalog entries:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":216,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" get_weather \u2014","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":217,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Get the current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":218,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" weather for a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":219,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" city; get","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":220,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_exchange_rate \u2014","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":221,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Get the exchange","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":222,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" rate between two","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":223,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" currencies; search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":224,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_hotels \u2014","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":225,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Search for hotels","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":226,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" in a city","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":227,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"; travel \u2014","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":228,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Travel location tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":229,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\"\nSo","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":230,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `get","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":231,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_weather` and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":232,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `travel`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":233,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" are the ones","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":234,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":235,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" will call `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":236,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"tool_search`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":237,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" with a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":238,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":239,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" that matches","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":240,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" these.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":241,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":242,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"query:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":243,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \"current weather","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":244,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and travel time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":245,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" zone tools\"`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":246,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nReady.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":247,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \nOutput matches","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":248,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" expected","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":249,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" format.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":250,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Proceeds.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":251,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \n[Output","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":252,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Generation] ->","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":253,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" calls tool_search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":254,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \n(Self","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":255,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"-Correction","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":256,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"/Refinement","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":257,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" during thought)","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":258,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nThe","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":259,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" prompt says \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":260,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":261,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" both a current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":262,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"-weather function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":263,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and a travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":264,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" time-zone function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":265,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\"\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":266,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"''ll","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":267,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" just put","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":268,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" that exactly","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":269,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" in the query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":270,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":271,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":272,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":": \"current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":273,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" weather and travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":274,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" time zone functions","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":275,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\"`\nDone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":276,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \nProceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":277,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"s. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":278,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"[Output","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":279,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Generation","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":280,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"]\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":281,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"```","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":282,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"json\n{\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":283,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"query\": \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":284,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"current weather and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":285,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" travel time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":286,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" functions\"}\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":287,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"```\nWait","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":288,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":", the tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":289,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" definition","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":290,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" expects","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":291,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `query`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":292,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" as a string","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":293,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":294,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" will generate","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":295,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the tool call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":296,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" now. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":297,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"All steps verified","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":298,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \nProceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":299,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"s. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":300,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"[Final Check","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":301,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"]\n-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":302,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":303,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":304,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"tool_search`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":305,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" exactly once?","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":306,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Yes.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":307,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"- Find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":308,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" both functions","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":309,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"? The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":310,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" query asks","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":311,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for them","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":312,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":313,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Do not call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":314,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" any other tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":315,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"?","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":316,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Yes.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":317,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":318,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Parameters","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":319,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" correct","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":320,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"? Yes","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":321,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\nDone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":322,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \nOutput","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":323,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" matches. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":324,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Proceeds.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":325,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \n[Output","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":326,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Generation] (","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":327,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Internal","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":328,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" thought","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":329,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" process","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":330,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" complete","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":331,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":")\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":332,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"tool_search`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":333,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" with query \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":334,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"current weather and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":335,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" travel time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":336,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" functions","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":337,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\"\nDone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":338,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \n(Self","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":339,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"-Correction","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":340,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"/Note:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":341,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" I should just","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":342,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" follow","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":343,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the instruction","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":344,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" exactly.)\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":345,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Calling","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":346,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tool.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":347,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \n[Done","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":348,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"] \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":349,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`{\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":350,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"query\": \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":351,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"current weather and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":352,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" travel time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":353,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" functions\"}`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":354,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" ->","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":355,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" This","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":356,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" is what","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":357,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" I will output","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":358,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":359,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Wait, the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":360,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" prompt says \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":361,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"First","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":362,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" call tool_search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":363,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" exactly once to","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":364,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" find both a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":365,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" current-weather","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":366,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" function and a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":367,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" travel time-zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":368,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" function.\"\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":369,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"I will just","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":370,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":371,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" it.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":372,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Done. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":373,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Proceeds","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":374,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \n[","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":375,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Output Generation]","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":376,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" ->","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":377,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" calls","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":378,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tool_search.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":379,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":380,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.done + + data: {"content_index":0,"item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":381,"text":"The + user wants me to find two specific tools using `tool_search`:\n1. A current-weather + function\n2. A travel time-zone function\n\nI need to call `tool_search` exactly + once. I should construct a query that captures the description of these capabilities.\nThe + available tools listed in the system prompt are: `get_weather`, `get_exchange_rate`, + `search_hotels`, `travel`.\n`get_weather` seems to match \"current-weather function\".\n`travel` + seems to match \"travel time-zone function\" (or perhaps I should search for + \"time zone\" specifically).\n\nLet''s formulate a query that searches for both + or asks the tool to return results for both types of capabilities. The prompt + says \"Search the client tool catalog for tools that can satisfy the request.\"\n\nQuery + idea: \"tools for current weather and travel time zones\"\n\nLet''s check the + descriptions in the prompt:\n`get_weather` \u2014 Get the current weather for + a city\n`travel` \u2014 Travel location tools.\n\nI will construct a query that + asks for current weather and travel time zone tools.\n\nQuery: \"current weather + and travel time zone tools\"\n\nWait, the prompt says \"find both a current-weather + function and a travel time-zone function.\"\nI will call `tool_search` with + this query.\nI must ensure I only call it once.\n\nParameters:\nquery: \"current + weather and travel time zone tools\"\n\nLet''s do it. \nActually, the description + for `travel` says \"Travel location tools.\" It might not explicitly say time + zone, but I''ll search for it. The user''s instruction is to call `tool_search` + exactly once to find them.\n\nI will proceed. \nCall: `tool_search(query=\"current + weather and travel time zone tools\")`\nCheck constraints: \"First call tool_search + exactly once... Do not call any other tool yet.\" -> Satisfied.\nProceed. \nOutput + matches schema.\nDone. \nWait, let''s look at the exact wording: \"find both + a current-weather function and a travel time-zone function.\"\nI''ll use that + phrasing in the query.\n`tool_search(query=\"find a current-weather function + and a travel time-zone function\")`\nThis should work.\nLet''s generate the + call. \nAll good. \nProceeding. \n[Self-Correction/Verification during thought]\nThe + prompt says: \"Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather \u2014 Get the current weather + for a city; get_exchange_rate \u2014 Get the exchange rate between two currencies; + search_hotels \u2014 Search for hotels in a city; travel \u2014 Travel location + tools.\"\nSo `get_weather` and `travel` are the ones.\nI will call `tool_search` + with a query that matches these.\n`query: \"current weather and travel time + zone tools\"`\nReady. \nOutput matches expected format.\nProceeds. \n[Output + Generation] -> calls tool_search. \n(Self-Correction/Refinement during thought)\nThe + prompt says \"find both a current-weather function and a travel time-zone function.\"\nI''ll + just put that exactly in the query.\n`query: \"current weather and travel time + zone functions\"`\nDone. \nProceeds. \n[Output Generation]\n```json\n{\"query\": + \"current weather and travel time zone functions\"}\n```\nWait, the tool definition + expects `query` as a string.\nI will generate the tool call now. \nAll steps + verified. \nProceeds. \n[Final Check]\n- Call `tool_search` exactly once? Yes.\n- + Find both functions? The query asks for them.\n- Do not call any other tool? + Yes.\n- Parameters correct? Yes.\nDone. \nOutput matches. \nProceeds. \n[Output + Generation] (Internal thought process complete)\n`tool_search` with query \"current + weather and travel time zone functions\"\nDone. \n(Self-Correction/Note: I should + just follow the instruction exactly.)\nCalling tool. \n[Done] \n`{\"query\": + \"current weather and travel time zone functions\"}` -> This is what I will + output. \nWait, the prompt says \"First call tool_search exactly once to find + both a current-weather function and a travel time-zone function.\"\nI will just + call it.\nDone. \nProceeds. \n[Output Generation] -> calls tool_search.\n","type":"response.reasoning_text.done"} + + ' + - 'event: response.reasoning_part.done + + data: {"content_index":0,"item_id":"a7b8cae9f47f4dc2","output_index":0,"part":{"text":"The + user wants me to find two specific tools using `tool_search`:\n1. A current-weather + function\n2. A travel time-zone function\n\nI need to call `tool_search` exactly + once. I should construct a query that captures the description of these capabilities.\nThe + available tools listed in the system prompt are: `get_weather`, `get_exchange_rate`, + `search_hotels`, `travel`.\n`get_weather` seems to match \"current-weather function\".\n`travel` + seems to match \"travel time-zone function\" (or perhaps I should search for + \"time zone\" specifically).\n\nLet''s formulate a query that searches for both + or asks the tool to return results for both types of capabilities. The prompt + says \"Search the client tool catalog for tools that can satisfy the request.\"\n\nQuery + idea: \"tools for current weather and travel time zones\"\n\nLet''s check the + descriptions in the prompt:\n`get_weather` \u2014 Get the current weather for + a city\n`travel` \u2014 Travel location tools.\n\nI will construct a query that + asks for current weather and travel time zone tools.\n\nQuery: \"current weather + and travel time zone tools\"\n\nWait, the prompt says \"find both a current-weather + function and a travel time-zone function.\"\nI will call `tool_search` with + this query.\nI must ensure I only call it once.\n\nParameters:\nquery: \"current + weather and travel time zone tools\"\n\nLet''s do it. \nActually, the description + for `travel` says \"Travel location tools.\" It might not explicitly say time + zone, but I''ll search for it. The user''s instruction is to call `tool_search` + exactly once to find them.\n\nI will proceed. \nCall: `tool_search(query=\"current + weather and travel time zone tools\")`\nCheck constraints: \"First call tool_search + exactly once... Do not call any other tool yet.\" -> Satisfied.\nProceed. \nOutput + matches schema.\nDone. \nWait, let''s look at the exact wording: \"find both + a current-weather function and a travel time-zone function.\"\nI''ll use that + phrasing in the query.\n`tool_search(query=\"find a current-weather function + and a travel time-zone function\")`\nThis should work.\nLet''s generate the + call. \nAll good. \nProceeding. \n[Self-Correction/Verification during thought]\nThe + prompt says: \"Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather \u2014 Get the current weather + for a city; get_exchange_rate \u2014 Get the exchange rate between two currencies; + search_hotels \u2014 Search for hotels in a city; travel \u2014 Travel location + tools.\"\nSo `get_weather` and `travel` are the ones.\nI will call `tool_search` + with a query that matches these.\n`query: \"current weather and travel time + zone tools\"`\nReady. \nOutput matches expected format.\nProceeds. \n[Output + Generation] -> calls tool_search. \n(Self-Correction/Refinement during thought)\nThe + prompt says \"find both a current-weather function and a travel time-zone function.\"\nI''ll + just put that exactly in the query.\n`query: \"current weather and travel time + zone functions\"`\nDone. \nProceeds. \n[Output Generation]\n```json\n{\"query\": + \"current weather and travel time zone functions\"}\n```\nWait, the tool definition + expects `query` as a string.\nI will generate the tool call now. \nAll steps + verified. \nProceeds. \n[Final Check]\n- Call `tool_search` exactly once? Yes.\n- + Find both functions? The query asks for them.\n- Do not call any other tool? + Yes.\n- Parameters correct? Yes.\nDone. \nOutput matches. \nProceeds. \n[Output + Generation] (Internal thought process complete)\n`tool_search` with query \"current + weather and travel time zone functions\"\nDone. \n(Self-Correction/Note: I should + just follow the instruction exactly.)\nCalling tool. \n[Done] \n`{\"query\": + \"current weather and travel time zone functions\"}` -> This is what I will + output. \nWait, the prompt says \"First call tool_search exactly once to find + both a current-weather function and a travel time-zone function.\"\nI will just + call it.\nDone. \nProceeds. \n[Output Generation] -> calls tool_search.\n","type":"reasoning_text"},"sequence_number":382,"type":"response.reasoning_part.done"} + + ' + - 'event: response.output_item.done + + data: {"item":{"content":[{"text":"The user wants me to find two specific tools + using `tool_search`:\n1. A current-weather function\n2. A travel time-zone function\n\nI + need to call `tool_search` exactly once. I should construct a query that captures + the description of these capabilities.\nThe available tools listed in the system + prompt are: `get_weather`, `get_exchange_rate`, `search_hotels`, `travel`.\n`get_weather` + seems to match \"current-weather function\".\n`travel` seems to match \"travel + time-zone function\" (or perhaps I should search for \"time zone\" specifically).\n\nLet''s + formulate a query that searches for both or asks the tool to return results + for both types of capabilities. The prompt says \"Search the client tool catalog + for tools that can satisfy the request.\"\n\nQuery idea: \"tools for current + weather and travel time zones\"\n\nLet''s check the descriptions in the prompt:\n`get_weather` + \u2014 Get the current weather for a city\n`travel` \u2014 Travel location tools.\n\nI + will construct a query that asks for current weather and travel time zone tools.\n\nQuery: + \"current weather and travel time zone tools\"\n\nWait, the prompt says \"find + both a current-weather function and a travel time-zone function.\"\nI will call + `tool_search` with this query.\nI must ensure I only call it once.\n\nParameters:\nquery: + \"current weather and travel time zone tools\"\n\nLet''s do it. \nActually, + the description for `travel` says \"Travel location tools.\" It might not explicitly + say time zone, but I''ll search for it. The user''s instruction is to call `tool_search` + exactly once to find them.\n\nI will proceed. \nCall: `tool_search(query=\"current + weather and travel time zone tools\")`\nCheck constraints: \"First call tool_search + exactly once... Do not call any other tool yet.\" -> Satisfied.\nProceed. \nOutput + matches schema.\nDone. \nWait, let''s look at the exact wording: \"find both + a current-weather function and a travel time-zone function.\"\nI''ll use that + phrasing in the query.\n`tool_search(query=\"find a current-weather function + and a travel time-zone function\")`\nThis should work.\nLet''s generate the + call. \nAll good. \nProceeding. \n[Self-Correction/Verification during thought]\nThe + prompt says: \"Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather \u2014 Get the current weather + for a city; get_exchange_rate \u2014 Get the exchange rate between two currencies; + search_hotels \u2014 Search for hotels in a city; travel \u2014 Travel location + tools.\"\nSo `get_weather` and `travel` are the ones.\nI will call `tool_search` + with a query that matches these.\n`query: \"current weather and travel time + zone tools\"`\nReady. \nOutput matches expected format.\nProceeds. \n[Output + Generation] -> calls tool_search. \n(Self-Correction/Refinement during thought)\nThe + prompt says \"find both a current-weather function and a travel time-zone function.\"\nI''ll + just put that exactly in the query.\n`query: \"current weather and travel time + zone functions\"`\nDone. \nProceeds. \n[Output Generation]\n```json\n{\"query\": + \"current weather and travel time zone functions\"}\n```\nWait, the tool definition + expects `query` as a string.\nI will generate the tool call now. \nAll steps + verified. \nProceeds. \n[Final Check]\n- Call `tool_search` exactly once? Yes.\n- + Find both functions? The query asks for them.\n- Do not call any other tool? + Yes.\n- Parameters correct? Yes.\nDone. \nOutput matches. \nProceeds. \n[Output + Generation] (Internal thought process complete)\n`tool_search` with query \"current + weather and travel time zone functions\"\nDone. \n(Self-Correction/Note: I should + just follow the instruction exactly.)\nCalling tool. \n[Done] \n`{\"query\": + \"current weather and travel time zone functions\"}` -> This is what I will + output. \nWait, the prompt says \"First call tool_search exactly once to find + both a current-weather function and a travel time-zone function.\"\nI will just + call it.\nDone. \nProceeds. \n[Output Generation] -> calls tool_search.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a7b8cae9f47f4dc2","status":"completed","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":383,"type":"response.output_item.done"} + + ' + - 'event: response.output_item.added + + data: {"item":{"arguments":{},"call_id":"call_bca8ad3ee683ecb1","execution":"client","id":"tsc_cdf3e176e80c30ba","status":"in_progress","type":"tool_search_call"},"output_index":1,"sequence_number":384,"type":"response.output_item.added"} + + ' + - 'event: response.output_item.done + + data: {"item":{"arguments":{"query":"current weather and travel time zone functions"},"call_id":"call_bca8ad3ee683ecb1","execution":"client","id":"tsc_cdf3e176e80c30ba","status":"completed","type":"tool_search_call"},"output_index":1,"sequence_number":385,"type":"response.output_item.done"} + + ' + - 'event: response.completed + + data: {"response":{"conversation_id":null,"created_at":1787715707,"error":null,"id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to find two specific tools using `tool_search`:\n1. A current-weather + function\n2. A travel time-zone function\n\nI need to call `tool_search` exactly + once. I should construct a query that captures the description of these capabilities.\nThe + available tools listed in the system prompt are: `get_weather`, `get_exchange_rate`, + `search_hotels`, `travel`.\n`get_weather` seems to match \"current-weather function\".\n`travel` + seems to match \"travel time-zone function\" (or perhaps I should search for + \"time zone\" specifically).\n\nLet''s formulate a query that searches for both + or asks the tool to return results for both types of capabilities. The prompt + says \"Search the client tool catalog for tools that can satisfy the request.\"\n\nQuery + idea: \"tools for current weather and travel time zones\"\n\nLet''s check the + descriptions in the prompt:\n`get_weather` \u2014 Get the current weather for + a city\n`travel` \u2014 Travel location tools.\n\nI will construct a query that + asks for current weather and travel time zone tools.\n\nQuery: \"current weather + and travel time zone tools\"\n\nWait, the prompt says \"find both a current-weather + function and a travel time-zone function.\"\nI will call `tool_search` with + this query.\nI must ensure I only call it once.\n\nParameters:\nquery: \"current + weather and travel time zone tools\"\n\nLet''s do it. \nActually, the description + for `travel` says \"Travel location tools.\" It might not explicitly say time + zone, but I''ll search for it. The user''s instruction is to call `tool_search` + exactly once to find them.\n\nI will proceed. \nCall: `tool_search(query=\"current + weather and travel time zone tools\")`\nCheck constraints: \"First call tool_search + exactly once... Do not call any other tool yet.\" -> Satisfied.\nProceed. \nOutput + matches schema.\nDone. \nWait, let''s look at the exact wording: \"find both + a current-weather function and a travel time-zone function.\"\nI''ll use that + phrasing in the query.\n`tool_search(query=\"find a current-weather function + and a travel time-zone function\")`\nThis should work.\nLet''s generate the + call. \nAll good. \nProceeding. \n[Self-Correction/Verification during thought]\nThe + prompt says: \"Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather \u2014 Get the current weather + for a city; get_exchange_rate \u2014 Get the exchange rate between two currencies; + search_hotels \u2014 Search for hotels in a city; travel \u2014 Travel location + tools.\"\nSo `get_weather` and `travel` are the ones.\nI will call `tool_search` + with a query that matches these.\n`query: \"current weather and travel time + zone tools\"`\nReady. \nOutput matches expected format.\nProceeds. \n[Output + Generation] -> calls tool_search. \n(Self-Correction/Refinement during thought)\nThe + prompt says \"find both a current-weather function and a travel time-zone function.\"\nI''ll + just put that exactly in the query.\n`query: \"current weather and travel time + zone functions\"`\nDone. \nProceeds. \n[Output Generation]\n```json\n{\"query\": + \"current weather and travel time zone functions\"}\n```\nWait, the tool definition + expects `query` as a string.\nI will generate the tool call now. \nAll steps + verified. \nProceeds. \n[Final Check]\n- Call `tool_search` exactly once? Yes.\n- + Find both functions? The query asks for them.\n- Do not call any other tool? + Yes.\n- Parameters correct? Yes.\nDone. \nOutput matches. \nProceeds. \n[Output + Generation] (Internal thought process complete)\n`tool_search` with query \"current + weather and travel time zone functions\"\nDone. \n(Self-Correction/Note: I should + just follow the instruction exactly.)\nCalling tool. \n[Done] \n`{\"query\": + \"current weather and travel time zone functions\"}` -> This is what I will + output. \nWait, the prompt says \"First call tool_search exactly once to find + both a current-weather function and a travel time-zone function.\"\nI will just + call it.\nDone. \nProceeds. \n[Output Generation] -> calls tool_search.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a7b8cae9f47f4dc2","status":null,"summary":[],"type":"reasoning"},{"arguments":{"query":"current + weather and travel time zone functions"},"call_id":"call_bca8ad3ee683ecb1","execution":"client","id":"tsc_cdf3e176e80c30ba","status":"completed","type":"tool_search_call"}],"previous_response_id":null,"status":"completed","tool_choice":"required","tools":[{"description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"additionalProperties":false,"properties":{"query":{"description":"A + concise description of the needed capabilities.","type":"string"}},"required":["query"],"type":"object"},"type":"tool_search"},{"defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","parameters":{"additionalProperties":false,"properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","parameters":{"additionalProperties":false,"properties":{"destination":{"type":"string"},"origin":{"type":"string"}},"required":["origin","destination"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":394,"input_tokens_details":{"cached_tokens":0},"output_tokens":963,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1357}},"sequence_number":386,"type":"response.completed"} + + ' + - 'data: [DONE] + + ' + status_code: 101 + websocket: + - '{"response":{"background":false,"created_at":1787715702,"frequency_penalty":0.0,"id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"required","tools":[{"description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"additionalProperties":false,"properties":{"query":{"description":"A + concise description of the needed capabilities.","type":"string"}},"required":["query"],"type":"object"},"type":"tool_search"},{"defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","parameters":{"additionalProperties":false,"properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","parameters":{"additionalProperties":false,"properties":{"destination":{"type":"string"},"origin":{"type":"string"}},"required":["origin","destination"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"}' + - '{"response":{"background":false,"created_at":1787715702,"frequency_penalty":0.0,"id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":null,"prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"required","tools":[{"description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"additionalProperties":false,"properties":{"query":{"description":"A + concise description of the needed capabilities.","type":"string"}},"required":["query"],"type":"object"},"type":"tool_search"},{"defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","parameters":{"additionalProperties":false,"properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","parameters":{"additionalProperties":false,"properties":{"destination":{"type":"string"},"origin":{"type":"string"}},"required":["origin","destination"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"}' + - '{"item":{"content":null,"encrypted_content":null,"id":"a7b8cae9f47f4dc2","status":"in_progress","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"}' + - '{"content_index":0,"item_id":"a7b8cae9f47f4dc2","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"}' + - '{"content_index":0,"delta":"The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" user wants me","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" to find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" two","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" specific tools using","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"tool_search`:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\n1.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" A current-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"weather function\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"2. A","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" travel time-zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" function\n\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" need to call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `tool_search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"` exactly once","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" I","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" should construct","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" a query that","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" captures","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" description","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" of these","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" capabilities.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" available tools listed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" in the system","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" prompt are:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"get_weather`,","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `get_exchange","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_rate`, `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"search_hotels","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`, `travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`.\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"get_weather`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" seems to match","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"current-weather","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" function\".\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`travel`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" seems to match","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \"travel time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"-zone function\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" (or perhaps","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" I","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" should","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" search for \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\" specifically","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":").\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Let''s formulate","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" query that searches","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for both or","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" asks","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":63,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" to return","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":64,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" results","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":65,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for both types","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":66,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" of capabilities.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":67,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":68,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" prompt says \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":69,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":70,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the client tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":71,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" catalog for tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":72,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" that can satisfy","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":73,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the request.\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":74,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":75,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":76,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" idea","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":77,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":": \"tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":78,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for current weather","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":79,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and travel time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":80,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" zones\"\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":81,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Let''s check","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":82,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the descriptions","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":83,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" in","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":84,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the prompt:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":85,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\n`get","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":86,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_weather` —","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":87,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Get the current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":88,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" weather for a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":89,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" city\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":90,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"travel` —","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":91,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Travel location tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":92,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":93,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" will construct","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":94,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":95,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":96,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" that","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":97,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" asks","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":98,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":99,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" weather and travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":100,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":101,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tools.\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":102,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":103,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":": \"current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":104,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" weather and travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":105,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" time zone tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":106,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\"\n\nWait","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":107,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":", the prompt","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":108,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" says \"find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":109,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" both a current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":110,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"-weather function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":111,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and a travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":112,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" time-zone function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":113,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\"\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":114,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" will call `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":115,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"tool_search`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":116,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" with this","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":117,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" query.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":118,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"I","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":119,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" must","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":120,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" ensure I only","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":121,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" call it once","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":122,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":123,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Parameters","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":124,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":":\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":125,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":126,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":": \"current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":127,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" weather and travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":128,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" time zone tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":129,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\"\n\nLet","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":130,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"''s do","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":131,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" it. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":132,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Actually","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":133,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":", the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":134,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" description","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":135,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for `travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":136,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"` says","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":137,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \"Travel location","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":138,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tools.\" It","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":139,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" might not","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":140,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" explicitly say","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":141,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" time zone,","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":142,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" but I","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":143,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"''ll search for","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":144,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" it. The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":145,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" user''s","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":146,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" instruction is to","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":147,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":148,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `tool_search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":149,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"` exactly once","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":150,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" to find them","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":151,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":152,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" will proceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":153,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \nCall","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":154,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":": `tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":155,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_search(query=\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":156,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"current weather and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":157,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" travel time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":158,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tools\")`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":159,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nCheck","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":160,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" constraints: \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":161,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"First call tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":162,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_search exactly once","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":163,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"... Do not","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":164,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" call any other","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":165,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tool yet.\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":166,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" -> S","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":167,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"atisfied.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":168,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Proceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":169,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \nOutput","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":170,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" matches schema","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":171,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\nDone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":172,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \nWait","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":173,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":", let''s","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":174,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" look","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":175,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" at the exact","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":176,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" wording","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":177,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":": \"find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":178,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" both a current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":179,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"-weather function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":180,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and a travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":181,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" time-zone function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":182,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\"\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":183,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"''ll use","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":184,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" that","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":185,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" ph","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":186,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"rasing in","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":187,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the query.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":188,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":189,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"tool_search(query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":190,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"=\"find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":191,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" a current-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":192,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"weather function and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":193,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" a travel time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":194,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"-zone function\")","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":195,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`\nThis","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":196,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" should","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":197,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" work.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":198,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Let''s generate","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":199,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":200,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":201,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \nAll","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":202,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" good","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":203,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \nProceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":204,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"ing. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":205,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"[Self-C","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":206,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"orrection/","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":207,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Verification during thought","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":208,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"]\nThe","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":209,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" prompt says:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":210,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \"Search the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":211,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" client tool catalog","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":212,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for tools that","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":213,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" can satisfy the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":214,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" request. Available","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":215,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" catalog entries:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":216,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" get_weather —","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":217,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Get the current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":218,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" weather for a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":219,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" city; get","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":220,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_exchange_rate —","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":221,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Get the exchange","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":222,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" rate between two","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":223,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" currencies; search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":224,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_hotels —","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":225,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Search for hotels","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":226,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" in a city","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":227,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"; travel —","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":228,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Travel location tools","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":229,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\"\nSo","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":230,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `get","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":231,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_weather` and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":232,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `travel`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":233,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" are the ones","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":234,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":235,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" will call `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":236,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"tool_search`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":237,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" with a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":238,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":239,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" that matches","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":240,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" these.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":241,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":242,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"query:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":243,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \"current weather","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":244,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and travel time","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":245,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" zone tools\"`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":246,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nReady.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":247,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \nOutput matches","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":248,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" expected","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":249,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" format.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":250,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Proceeds.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":251,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \n[Output","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":252,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Generation] ->","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":253,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" calls tool_search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":254,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \n(Self","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":255,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"-Correction","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":256,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"/Refinement","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":257,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" during thought)","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":258,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nThe","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":259,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" prompt says \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":260,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":261,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" both a current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":262,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"-weather function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":263,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and a travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":264,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" time-zone function","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":265,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\"\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":266,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"''ll","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":267,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" just put","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":268,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" that exactly","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":269,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" in the query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":270,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":271,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"query","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":272,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":": \"current","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":273,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" weather and travel","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":274,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" time zone functions","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":275,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\"`\nDone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":276,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \nProceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":277,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"s. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":278,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"[Output","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":279,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Generation","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":280,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"]\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":281,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"```","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":282,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"json\n{\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":283,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"query\": \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":284,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"current weather and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":285,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" travel time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":286,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" functions\"}\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":287,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"```\nWait","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":288,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":", the tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":289,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" definition","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":290,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" expects","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":291,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `query`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":292,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" as a string","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":293,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\nI","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":294,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" will generate","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":295,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the tool call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":296,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" now. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":297,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"All steps verified","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":298,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \nProceed","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":299,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"s. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":300,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"[Final Check","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":301,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"]\n-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":302,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":303,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":304,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"tool_search`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":305,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" exactly once?","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":306,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Yes.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":307,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"- Find","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":308,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" both functions","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":309,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"? The","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":310,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" query asks","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":311,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for them","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":312,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":313,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Do not call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":314,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" any other tool","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":315,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"?","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":316,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Yes.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":317,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"-","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":318,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Parameters","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":319,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" correct","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":320,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"? Yes","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":321,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\nDone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":322,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \nOutput","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":323,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" matches. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":324,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Proceeds.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":325,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \n[Output","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":326,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Generation] (","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":327,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Internal","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":328,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" thought","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":329,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" process","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":330,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" complete","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":331,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":")\n`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":332,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"tool_search`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":333,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" with query \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":334,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"current weather and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":335,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" travel time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":336,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" functions","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":337,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\"\nDone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":338,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \n(Self","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":339,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"-Correction","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":340,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"/Note:","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":341,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" I should just","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":342,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" follow","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":343,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the instruction","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":344,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" exactly.)\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":345,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Calling","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":346,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tool.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":347,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \n[Done","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":348,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"] \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":349,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`{\"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":350,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"query\": \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":351,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"current weather and","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":352,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" travel time zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":353,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" functions\"}`","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":354,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" ->","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":355,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" This","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":356,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" is what","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":357,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" I will output","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":358,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":359,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Wait, the","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":360,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" prompt says \"","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":361,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"First","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":362,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" call tool_search","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":363,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" exactly once to","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":364,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" find both a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":365,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" current-weather","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":366,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" function and a","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":367,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" travel time-zone","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":368,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" function.\"\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":369,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"I will just","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":370,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" call","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":371,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" it.\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":372,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Done. \n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":373,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Proceeds","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":374,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \n[","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":375,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Output Generation]","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":376,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" ->","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":377,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" calls","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":378,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tool_search.","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":379,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\n","item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":380,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"item_id":"a7b8cae9f47f4dc2","output_index":0,"sequence_number":381,"text":"The + user wants me to find two specific tools using `tool_search`:\n1. A current-weather + function\n2. A travel time-zone function\n\nI need to call `tool_search` exactly + once. I should construct a query that captures the description of these capabilities.\nThe + available tools listed in the system prompt are: `get_weather`, `get_exchange_rate`, + `search_hotels`, `travel`.\n`get_weather` seems to match \"current-weather function\".\n`travel` + seems to match \"travel time-zone function\" (or perhaps I should search for + \"time zone\" specifically).\n\nLet''s formulate a query that searches for both + or asks the tool to return results for both types of capabilities. The prompt + says \"Search the client tool catalog for tools that can satisfy the request.\"\n\nQuery + idea: \"tools for current weather and travel time zones\"\n\nLet''s check the + descriptions in the prompt:\n`get_weather` — Get the current weather for a city\n`travel` + — Travel location tools.\n\nI will construct a query that asks for current weather + and travel time zone tools.\n\nQuery: \"current weather and travel time zone + tools\"\n\nWait, the prompt says \"find both a current-weather function and + a travel time-zone function.\"\nI will call `tool_search` with this query.\nI + must ensure I only call it once.\n\nParameters:\nquery: \"current weather and + travel time zone tools\"\n\nLet''s do it. \nActually, the description for `travel` + says \"Travel location tools.\" It might not explicitly say time zone, but I''ll + search for it. The user''s instruction is to call `tool_search` exactly once + to find them.\n\nI will proceed. \nCall: `tool_search(query=\"current weather + and travel time zone tools\")`\nCheck constraints: \"First call tool_search + exactly once... Do not call any other tool yet.\" -> Satisfied.\nProceed. \nOutput + matches schema.\nDone. \nWait, let''s look at the exact wording: \"find both + a current-weather function and a travel time-zone function.\"\nI''ll use that + phrasing in the query.\n`tool_search(query=\"find a current-weather function + and a travel time-zone function\")`\nThis should work.\nLet''s generate the + call. \nAll good. \nProceeding. \n[Self-Correction/Verification during thought]\nThe + prompt says: \"Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather — Get the current weather for + a city; get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.\"\nSo `get_weather` + and `travel` are the ones.\nI will call `tool_search` with a query that matches + these.\n`query: \"current weather and travel time zone tools\"`\nReady. \nOutput + matches expected format.\nProceeds. \n[Output Generation] -> calls tool_search. + \n(Self-Correction/Refinement during thought)\nThe prompt says \"find both a + current-weather function and a travel time-zone function.\"\nI''ll just put + that exactly in the query.\n`query: \"current weather and travel time zone functions\"`\nDone. + \nProceeds. \n[Output Generation]\n```json\n{\"query\": \"current weather and + travel time zone functions\"}\n```\nWait, the tool definition expects `query` + as a string.\nI will generate the tool call now. \nAll steps verified. \nProceeds. + \n[Final Check]\n- Call `tool_search` exactly once? Yes.\n- Find both functions? + The query asks for them.\n- Do not call any other tool? Yes.\n- Parameters correct? + Yes.\nDone. \nOutput matches. \nProceeds. \n[Output Generation] (Internal thought + process complete)\n`tool_search` with query \"current weather and travel time + zone functions\"\nDone. \n(Self-Correction/Note: I should just follow the instruction + exactly.)\nCalling tool. \n[Done] \n`{\"query\": \"current weather and travel + time zone functions\"}` -> This is what I will output. \nWait, the prompt says + \"First call tool_search exactly once to find both a current-weather function + and a travel time-zone function.\"\nI will just call it.\nDone. \nProceeds. + \n[Output Generation] -> calls tool_search.\n","type":"response.reasoning_text.done"}' + - '{"content_index":0,"item_id":"a7b8cae9f47f4dc2","output_index":0,"part":{"text":"The + user wants me to find two specific tools using `tool_search`:\n1. A current-weather + function\n2. A travel time-zone function\n\nI need to call `tool_search` exactly + once. I should construct a query that captures the description of these capabilities.\nThe + available tools listed in the system prompt are: `get_weather`, `get_exchange_rate`, + `search_hotels`, `travel`.\n`get_weather` seems to match \"current-weather function\".\n`travel` + seems to match \"travel time-zone function\" (or perhaps I should search for + \"time zone\" specifically).\n\nLet''s formulate a query that searches for both + or asks the tool to return results for both types of capabilities. The prompt + says \"Search the client tool catalog for tools that can satisfy the request.\"\n\nQuery + idea: \"tools for current weather and travel time zones\"\n\nLet''s check the + descriptions in the prompt:\n`get_weather` — Get the current weather for a city\n`travel` + — Travel location tools.\n\nI will construct a query that asks for current weather + and travel time zone tools.\n\nQuery: \"current weather and travel time zone + tools\"\n\nWait, the prompt says \"find both a current-weather function and + a travel time-zone function.\"\nI will call `tool_search` with this query.\nI + must ensure I only call it once.\n\nParameters:\nquery: \"current weather and + travel time zone tools\"\n\nLet''s do it. \nActually, the description for `travel` + says \"Travel location tools.\" It might not explicitly say time zone, but I''ll + search for it. The user''s instruction is to call `tool_search` exactly once + to find them.\n\nI will proceed. \nCall: `tool_search(query=\"current weather + and travel time zone tools\")`\nCheck constraints: \"First call tool_search + exactly once... Do not call any other tool yet.\" -> Satisfied.\nProceed. \nOutput + matches schema.\nDone. \nWait, let''s look at the exact wording: \"find both + a current-weather function and a travel time-zone function.\"\nI''ll use that + phrasing in the query.\n`tool_search(query=\"find a current-weather function + and a travel time-zone function\")`\nThis should work.\nLet''s generate the + call. \nAll good. \nProceeding. \n[Self-Correction/Verification during thought]\nThe + prompt says: \"Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather — Get the current weather for + a city; get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.\"\nSo `get_weather` + and `travel` are the ones.\nI will call `tool_search` with a query that matches + these.\n`query: \"current weather and travel time zone tools\"`\nReady. \nOutput + matches expected format.\nProceeds. \n[Output Generation] -> calls tool_search. + \n(Self-Correction/Refinement during thought)\nThe prompt says \"find both a + current-weather function and a travel time-zone function.\"\nI''ll just put + that exactly in the query.\n`query: \"current weather and travel time zone functions\"`\nDone. + \nProceeds. \n[Output Generation]\n```json\n{\"query\": \"current weather and + travel time zone functions\"}\n```\nWait, the tool definition expects `query` + as a string.\nI will generate the tool call now. \nAll steps verified. \nProceeds. + \n[Final Check]\n- Call `tool_search` exactly once? Yes.\n- Find both functions? + The query asks for them.\n- Do not call any other tool? Yes.\n- Parameters correct? + Yes.\nDone. \nOutput matches. \nProceeds. \n[Output Generation] (Internal thought + process complete)\n`tool_search` with query \"current weather and travel time + zone functions\"\nDone. \n(Self-Correction/Note: I should just follow the instruction + exactly.)\nCalling tool. \n[Done] \n`{\"query\": \"current weather and travel + time zone functions\"}` -> This is what I will output. \nWait, the prompt says + \"First call tool_search exactly once to find both a current-weather function + and a travel time-zone function.\"\nI will just call it.\nDone. \nProceeds. + \n[Output Generation] -> calls tool_search.\n","type":"reasoning_text"},"sequence_number":382,"type":"response.reasoning_part.done"}' + - '{"item":{"content":[{"text":"The user wants me to find two specific tools using + `tool_search`:\n1. A current-weather function\n2. A travel time-zone function\n\nI + need to call `tool_search` exactly once. I should construct a query that captures + the description of these capabilities.\nThe available tools listed in the system + prompt are: `get_weather`, `get_exchange_rate`, `search_hotels`, `travel`.\n`get_weather` + seems to match \"current-weather function\".\n`travel` seems to match \"travel + time-zone function\" (or perhaps I should search for \"time zone\" specifically).\n\nLet''s + formulate a query that searches for both or asks the tool to return results + for both types of capabilities. The prompt says \"Search the client tool catalog + for tools that can satisfy the request.\"\n\nQuery idea: \"tools for current + weather and travel time zones\"\n\nLet''s check the descriptions in the prompt:\n`get_weather` + — Get the current weather for a city\n`travel` — Travel location tools.\n\nI + will construct a query that asks for current weather and travel time zone tools.\n\nQuery: + \"current weather and travel time zone tools\"\n\nWait, the prompt says \"find + both a current-weather function and a travel time-zone function.\"\nI will call + `tool_search` with this query.\nI must ensure I only call it once.\n\nParameters:\nquery: + \"current weather and travel time zone tools\"\n\nLet''s do it. \nActually, + the description for `travel` says \"Travel location tools.\" It might not explicitly + say time zone, but I''ll search for it. The user''s instruction is to call `tool_search` + exactly once to find them.\n\nI will proceed. \nCall: `tool_search(query=\"current + weather and travel time zone tools\")`\nCheck constraints: \"First call tool_search + exactly once... Do not call any other tool yet.\" -> Satisfied.\nProceed. \nOutput + matches schema.\nDone. \nWait, let''s look at the exact wording: \"find both + a current-weather function and a travel time-zone function.\"\nI''ll use that + phrasing in the query.\n`tool_search(query=\"find a current-weather function + and a travel time-zone function\")`\nThis should work.\nLet''s generate the + call. \nAll good. \nProceeding. \n[Self-Correction/Verification during thought]\nThe + prompt says: \"Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather — Get the current weather for + a city; get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.\"\nSo `get_weather` + and `travel` are the ones.\nI will call `tool_search` with a query that matches + these.\n`query: \"current weather and travel time zone tools\"`\nReady. \nOutput + matches expected format.\nProceeds. \n[Output Generation] -> calls tool_search. + \n(Self-Correction/Refinement during thought)\nThe prompt says \"find both a + current-weather function and a travel time-zone function.\"\nI''ll just put + that exactly in the query.\n`query: \"current weather and travel time zone functions\"`\nDone. + \nProceeds. \n[Output Generation]\n```json\n{\"query\": \"current weather and + travel time zone functions\"}\n```\nWait, the tool definition expects `query` + as a string.\nI will generate the tool call now. \nAll steps verified. \nProceeds. + \n[Final Check]\n- Call `tool_search` exactly once? Yes.\n- Find both functions? + The query asks for them.\n- Do not call any other tool? Yes.\n- Parameters correct? + Yes.\nDone. \nOutput matches. \nProceeds. \n[Output Generation] (Internal thought + process complete)\n`tool_search` with query \"current weather and travel time + zone functions\"\nDone. \n(Self-Correction/Note: I should just follow the instruction + exactly.)\nCalling tool. \n[Done] \n`{\"query\": \"current weather and travel + time zone functions\"}` -> This is what I will output. \nWait, the prompt says + \"First call tool_search exactly once to find both a current-weather function + and a travel time-zone function.\"\nI will just call it.\nDone. \nProceeds. + \n[Output Generation] -> calls tool_search.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a7b8cae9f47f4dc2","status":"completed","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":383,"type":"response.output_item.done"}' + - '{"item":{"arguments":{},"call_id":"call_bca8ad3ee683ecb1","execution":"client","id":"tsc_cdf3e176e80c30ba","status":"in_progress","type":"tool_search_call"},"output_index":1,"sequence_number":384,"type":"response.output_item.added"}' + - '{"item":{"arguments":{"query":"current weather and travel time zone functions"},"call_id":"call_bca8ad3ee683ecb1","execution":"client","id":"tsc_cdf3e176e80c30ba","status":"completed","type":"tool_search_call"},"output_index":1,"sequence_number":385,"type":"response.output_item.done"}' + - '{"response":{"conversation_id":null,"created_at":1787715707,"error":null,"id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to find two specific tools using `tool_search`:\n1. A current-weather + function\n2. A travel time-zone function\n\nI need to call `tool_search` exactly + once. I should construct a query that captures the description of these capabilities.\nThe + available tools listed in the system prompt are: `get_weather`, `get_exchange_rate`, + `search_hotels`, `travel`.\n`get_weather` seems to match \"current-weather function\".\n`travel` + seems to match \"travel time-zone function\" (or perhaps I should search for + \"time zone\" specifically).\n\nLet''s formulate a query that searches for both + or asks the tool to return results for both types of capabilities. The prompt + says \"Search the client tool catalog for tools that can satisfy the request.\"\n\nQuery + idea: \"tools for current weather and travel time zones\"\n\nLet''s check the + descriptions in the prompt:\n`get_weather` — Get the current weather for a city\n`travel` + — Travel location tools.\n\nI will construct a query that asks for current weather + and travel time zone tools.\n\nQuery: \"current weather and travel time zone + tools\"\n\nWait, the prompt says \"find both a current-weather function and + a travel time-zone function.\"\nI will call `tool_search` with this query.\nI + must ensure I only call it once.\n\nParameters:\nquery: \"current weather and + travel time zone tools\"\n\nLet''s do it. \nActually, the description for `travel` + says \"Travel location tools.\" It might not explicitly say time zone, but I''ll + search for it. The user''s instruction is to call `tool_search` exactly once + to find them.\n\nI will proceed. \nCall: `tool_search(query=\"current weather + and travel time zone tools\")`\nCheck constraints: \"First call tool_search + exactly once... Do not call any other tool yet.\" -> Satisfied.\nProceed. \nOutput + matches schema.\nDone. \nWait, let''s look at the exact wording: \"find both + a current-weather function and a travel time-zone function.\"\nI''ll use that + phrasing in the query.\n`tool_search(query=\"find a current-weather function + and a travel time-zone function\")`\nThis should work.\nLet''s generate the + call. \nAll good. \nProceeding. \n[Self-Correction/Verification during thought]\nThe + prompt says: \"Search the client tool catalog for tools that can satisfy the + request. Available catalog entries: get_weather — Get the current weather for + a city; get_exchange_rate — Get the exchange rate between two currencies; search_hotels + — Search for hotels in a city; travel — Travel location tools.\"\nSo `get_weather` + and `travel` are the ones.\nI will call `tool_search` with a query that matches + these.\n`query: \"current weather and travel time zone tools\"`\nReady. \nOutput + matches expected format.\nProceeds. \n[Output Generation] -> calls tool_search. + \n(Self-Correction/Refinement during thought)\nThe prompt says \"find both a + current-weather function and a travel time-zone function.\"\nI''ll just put + that exactly in the query.\n`query: \"current weather and travel time zone functions\"`\nDone. + \nProceeds. \n[Output Generation]\n```json\n{\"query\": \"current weather and + travel time zone functions\"}\n```\nWait, the tool definition expects `query` + as a string.\nI will generate the tool call now. \nAll steps verified. \nProceeds. + \n[Final Check]\n- Call `tool_search` exactly once? Yes.\n- Find both functions? + The query asks for them.\n- Do not call any other tool? Yes.\n- Parameters correct? + Yes.\nDone. \nOutput matches. \nProceeds. \n[Output Generation] (Internal thought + process complete)\n`tool_search` with query \"current weather and travel time + zone functions\"\nDone. \n(Self-Correction/Note: I should just follow the instruction + exactly.)\nCalling tool. \n[Done] \n`{\"query\": \"current weather and travel + time zone functions\"}` -> This is what I will output. \nWait, the prompt says + \"First call tool_search exactly once to find both a current-weather function + and a travel time-zone function.\"\nI will just call it.\nDone. \nProceeds. + \n[Output Generation] -> calls tool_search.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a7b8cae9f47f4dc2","status":null,"summary":[],"type":"reasoning"},{"arguments":{"query":"current + weather and travel time zone functions"},"call_id":"call_bca8ad3ee683ecb1","execution":"client","id":"tsc_cdf3e176e80c30ba","status":"completed","type":"tool_search_call"}],"previous_response_id":null,"status":"completed","tool_choice":"required","tools":[{"description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"additionalProperties":false,"properties":{"query":{"description":"A + concise description of the needed capabilities.","type":"string"}},"required":["query"],"type":"object"},"type":"tool_search"},{"defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","parameters":{"additionalProperties":false,"properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","parameters":{"additionalProperties":false,"properties":{"destination":{"type":"string"},"origin":{"type":"string"}},"required":["origin","destination"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":394,"input_tokens_details":{"cached_tokens":0},"output_tokens":963,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1357}},"sequence_number":386,"type":"response.completed"}' +- filename: t2 + request: + body: + input: + - call_id: call_bca8ad3ee683ecb1 + execution: client + status: completed + tools: + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + type: tool_search_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + previous_response_id: resp_01a03c28-cebc-7f43-b83c-186f9eb3b806 + store: true + tool_choice: + name: get_weather + type: function + type: response.create + headers: {} + method: WEBSOCKET + path: /v1/responses + query_params: {} + transport: websocket + response: + headers: + transport: websocket + sse: + - 'event: response.created + + data: {"response":{"background":false,"created_at":1787715709,"frequency_penalty":0.0,"id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_weather","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"} + + ' + - 'event: response.in_progress + + data: {"response":{"background":false,"created_at":1787715709,"frequency_penalty":0.0,"id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_weather","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"} + + ' + - 'event: response.output_item.added + + data: {"item":{"content":null,"encrypted_content":null,"id":"a3098d30d6b5f4b1","status":"in_progress","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} + + ' + - 'event: response.reasoning_part.added + + data: {"content_index":0,"item_id":"a3098d30d6b5f4b1","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"The","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" user wants me","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" to call the","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `get_weather","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"` function with","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the parameter","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `{\"city","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\": \"Paris","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\"}`.","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nI found","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"get_weather`","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `travel`","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" in the previous","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" step.\n","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"The `","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"travel` tool","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" is a","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" namespace, but","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the user specifically","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" asked for `","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"get_weather`","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" right","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" now.\n","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"I will execute","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the tool call","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.done + + data: {"content_index":0,"item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":32,"text":"The + user wants me to call the `get_weather` function with the parameter `{\"city\": + \"Paris\"}`.\nI found `get_weather` and `travel` in the previous step.\nThe + `travel` tool is a namespace, but the user specifically asked for `get_weather` + right now.\nI will execute the tool call.\n","type":"response.reasoning_text.done"} + + ' + - 'event: response.reasoning_part.done + + data: {"content_index":0,"item_id":"a3098d30d6b5f4b1","output_index":0,"part":{"text":"The + user wants me to call the `get_weather` function with the parameter `{\"city\": + \"Paris\"}`.\nI found `get_weather` and `travel` in the previous step.\nThe + `travel` tool is a namespace, but the user specifically asked for `get_weather` + right now.\nI will execute the tool call.\n","type":"reasoning_text"},"sequence_number":33,"type":"response.reasoning_part.done"} + + ' + - 'event: response.output_item.done + + data: {"item":{"content":[{"text":"The user wants me to call the `get_weather` + function with the parameter `{\"city\": \"Paris\"}`.\nI found `get_weather` + and `travel` in the previous step.\nThe `travel` tool is a namespace, but the + user specifically asked for `get_weather` right now.\nI will execute the tool + call.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a3098d30d6b5f4b1","status":"completed","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":34,"type":"response.output_item.done"} + + ' + - 'event: response.output_item.added + + data: {"item":{"arguments":"","call_id":"call_9a756a715a7038af","caller":null,"id":"bceec48cd76816ce","name":"get_weather","namespace":null,"status":"in_progress","type":"function_call"},"output_index":1,"sequence_number":35,"type":"response.output_item.added"} + + ' + - 'event: response.function_call_arguments.delta + + data: {"delta":"{\"city\": \"","item_id":"bceec48cd76816ce","output_index":1,"sequence_number":36,"type":"response.function_call_arguments.delta"} + + ' + - 'event: response.function_call_arguments.delta + + data: {"delta":"Paris","item_id":"bceec48cd76816ce","output_index":1,"sequence_number":37,"type":"response.function_call_arguments.delta"} + + ' + - 'event: response.function_call_arguments.delta + + data: {"delta":"\"}","item_id":"bceec48cd76816ce","output_index":1,"sequence_number":38,"type":"response.function_call_arguments.delta"} + + ' + - 'event: response.function_call_arguments.done + + data: {"arguments":"{\"city\": \"Paris\"}","item_id":"bceec48cd76816ce","name":"get_weather","output_index":1,"sequence_number":39,"type":"response.function_call_arguments.done"} + + ' + - 'event: response.output_item.done + + data: {"item":{"arguments":"{\"city\": \"Paris\"}","call_id":"call_9a756a715a7038af","caller":null,"id":"bceec48cd76816ce","name":"get_weather","namespace":null,"status":"completed","type":"function_call"},"output_index":1,"sequence_number":40,"type":"response.output_item.done"} + + ' + - 'event: response.completed + + data: {"response":{"conversation_id":null,"created_at":1787715710,"error":null,"id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to call the `get_weather` function with the parameter `{\"city\": + \"Paris\"}`.\nI found `get_weather` and `travel` in the previous step.\nThe + `travel` tool is a namespace, but the user specifically asked for `get_weather` + right now.\nI will execute the tool call.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a3098d30d6b5f4b1","status":null,"summary":[],"type":"reasoning"},{"arguments":"{\"city\": + \"Paris\"}","call_id":"call_9a756a715a7038af","id":"bceec48cd76816ce","name":"get_weather","status":"completed","type":"function_call"}],"previous_response_id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","status":"completed","tool_choice":{"name":"get_weather","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":717,"input_tokens_details":{"cached_tokens":0},"output_tokens":99,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":816}},"sequence_number":41,"type":"response.completed"} + + ' + - 'data: [DONE] + + ' + status_code: 101 + websocket: + - '{"response":{"background":false,"created_at":1787715709,"frequency_penalty":0.0,"id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_weather","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"}' + - '{"response":{"background":false,"created_at":1787715709,"frequency_penalty":0.0,"id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_weather","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"}' + - '{"item":{"content":null,"encrypted_content":null,"id":"a3098d30d6b5f4b1","status":"in_progress","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"}' + - '{"content_index":0,"item_id":"a3098d30d6b5f4b1","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"}' + - '{"content_index":0,"delta":"The","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" user wants me","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" to call the","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `get_weather","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"` function with","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the parameter","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `{\"city","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\": \"Paris","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\"}`.","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nI found","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"get_weather`","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `travel`","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" in the previous","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" step.\n","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"The `","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"travel` tool","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" is a","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" namespace, but","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the user specifically","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" asked for `","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"get_weather`","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" right","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" now.\n","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"I will execute","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the tool call","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n","item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"item_id":"a3098d30d6b5f4b1","output_index":0,"sequence_number":32,"text":"The + user wants me to call the `get_weather` function with the parameter `{\"city\": + \"Paris\"}`.\nI found `get_weather` and `travel` in the previous step.\nThe + `travel` tool is a namespace, but the user specifically asked for `get_weather` + right now.\nI will execute the tool call.\n","type":"response.reasoning_text.done"}' + - '{"content_index":0,"item_id":"a3098d30d6b5f4b1","output_index":0,"part":{"text":"The + user wants me to call the `get_weather` function with the parameter `{\"city\": + \"Paris\"}`.\nI found `get_weather` and `travel` in the previous step.\nThe + `travel` tool is a namespace, but the user specifically asked for `get_weather` + right now.\nI will execute the tool call.\n","type":"reasoning_text"},"sequence_number":33,"type":"response.reasoning_part.done"}' + - '{"item":{"content":[{"text":"The user wants me to call the `get_weather` function + with the parameter `{\"city\": \"Paris\"}`.\nI found `get_weather` and `travel` + in the previous step.\nThe `travel` tool is a namespace, but the user specifically + asked for `get_weather` right now.\nI will execute the tool call.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a3098d30d6b5f4b1","status":"completed","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":34,"type":"response.output_item.done"}' + - '{"item":{"arguments":"","call_id":"call_9a756a715a7038af","caller":null,"id":"bceec48cd76816ce","name":"get_weather","namespace":null,"status":"in_progress","type":"function_call"},"output_index":1,"sequence_number":35,"type":"response.output_item.added"}' + - '{"delta":"{\"city\": \"","item_id":"bceec48cd76816ce","output_index":1,"sequence_number":36,"type":"response.function_call_arguments.delta"}' + - '{"delta":"Paris","item_id":"bceec48cd76816ce","output_index":1,"sequence_number":37,"type":"response.function_call_arguments.delta"}' + - '{"delta":"\"}","item_id":"bceec48cd76816ce","output_index":1,"sequence_number":38,"type":"response.function_call_arguments.delta"}' + - '{"arguments":"{\"city\": \"Paris\"}","item_id":"bceec48cd76816ce","name":"get_weather","output_index":1,"sequence_number":39,"type":"response.function_call_arguments.done"}' + - '{"item":{"arguments":"{\"city\": \"Paris\"}","call_id":"call_9a756a715a7038af","caller":null,"id":"bceec48cd76816ce","name":"get_weather","namespace":null,"status":"completed","type":"function_call"},"output_index":1,"sequence_number":40,"type":"response.output_item.done"}' + - '{"response":{"conversation_id":null,"created_at":1787715710,"error":null,"id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to call the `get_weather` function with the parameter `{\"city\": + \"Paris\"}`.\nI found `get_weather` and `travel` in the previous step.\nThe + `travel` tool is a namespace, but the user specifically asked for `get_weather` + right now.\nI will execute the tool call.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"a3098d30d6b5f4b1","status":null,"summary":[],"type":"reasoning"},{"arguments":"{\"city\": + \"Paris\"}","call_id":"call_9a756a715a7038af","id":"bceec48cd76816ce","name":"get_weather","status":"completed","type":"function_call"}],"previous_response_id":"resp_01a03c28-cebc-7f43-b83c-186f9eb3b806","status":"completed","tool_choice":{"name":"get_weather","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":717,"input_tokens_details":{"cached_tokens":0},"output_tokens":99,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":816}},"sequence_number":41,"type":"response.completed"}' +- filename: t3 + request: + body: + input: + - call_id: call_9a756a715a7038af + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + previous_response_id: resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a + store: true + tool_choice: + name: get_timezone + namespace: travel + type: function + type: response.create + headers: {} + method: WEBSOCKET + path: /v1/responses + query_params: {} + transport: websocket + response: + headers: + transport: websocket + sse: + - 'event: response.created + + data: {"response":{"background":false,"created_at":1787715710,"frequency_penalty":0.0,"id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_timezone","namespace":"travel","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"} + + ' + - 'event: response.in_progress + + data: {"response":{"background":false,"created_at":1787715710,"frequency_penalty":0.0,"id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_timezone","namespace":"travel","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"} + + ' + - 'event: response.output_item.added + + data: {"item":{"content":null,"encrypted_content":null,"id":"9cf2828127054765","status":"in_progress","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} + + ' + - 'event: response.reasoning_part.added + + data: {"content_index":0,"item_id":"9cf2828127054765","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"The","item_id":"9cf2828127054765","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" user wants me","item_id":"9cf2828127054765","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" to call the","item_id":"9cf2828127054765","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `get_timezone","item_id":"9cf2828127054765","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"` function from","item_id":"9cf2828127054765","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the `travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"` namespace with","item_id":"9cf2828127054765","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `city","item_id":"9cf2828127054765","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"=\"Paris\"","item_id":"9cf2828127054765","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`.\nThe","item_id":"9cf2828127054765","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" previous","item_id":"9cf2828127054765","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tool search","item_id":"9cf2828127054765","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" result showed `","item_id":"9cf2828127054765","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"travel` is","item_id":"9cf2828127054765","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" a namespace with","item_id":"9cf2828127054765","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" description","item_id":"9cf2828127054765","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \"Travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" location tools\".","item_id":"9cf2828127054765","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nThe available","item_id":"9cf2828127054765","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tools include","item_id":"9cf2828127054765","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `agentic","item_id":"9cf2828127054765","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_ns__travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"__get_timezone","item_id":"9cf2828127054765","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`","item_id":"9cf2828127054765","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" which matches","item_id":"9cf2828127054765","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the description \"","item_id":"9cf2828127054765","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Get the I","item_id":"9cf2828127054765","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"ANA time zone","item_id":"9cf2828127054765","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for a city","item_id":"9cf2828127054765","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\".\nSo","item_id":"9cf2828127054765","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" I will call","item_id":"9cf2828127054765","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `agentic","item_id":"9cf2828127054765","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_ns__travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"__get_timezone","item_id":"9cf2828127054765","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"` with `","item_id":"9cf2828127054765","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"city:","item_id":"9cf2828127054765","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \"Paris\"","item_id":"9cf2828127054765","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`.\nI","item_id":"9cf2828127054765","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" must","item_id":"9cf2828127054765","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" only","item_id":"9cf2828127054765","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" call this tool","item_id":"9cf2828127054765","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and","item_id":"9cf2828127054765","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" no","item_id":"9cf2828127054765","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" others.\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Parameters: `","item_id":"9cf2828127054765","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"city","item_id":"9cf2828127054765","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":": \"Paris","item_id":"9cf2828127054765","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\"","item_id":"9cf2828127054765","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`.\nLet","item_id":"9cf2828127054765","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"''s make","item_id":"9cf2828127054765","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the call.","item_id":"9cf2828127054765","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \n","item_id":"9cf2828127054765","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Wait, the","item_id":"9cf2828127054765","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" prompt","item_id":"9cf2828127054765","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" says \"call","item_id":"9cf2828127054765","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the loaded travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" namespace member get","item_id":"9cf2828127054765","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_timezone exactly once","item_id":"9cf2828127054765","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\". The tool","item_id":"9cf2828127054765","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" name in the","item_id":"9cf2828127054765","output_index":0,"sequence_number":63,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" schema is `","item_id":"9cf2828127054765","output_index":0,"sequence_number":64,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"agentic_ns","item_id":"9cf2828127054765","output_index":0,"sequence_number":65,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"__travel__","item_id":"9cf2828127054765","output_index":0,"sequence_number":66,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"get_timezone`.","item_id":"9cf2828127054765","output_index":0,"sequence_number":67,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" I will use","item_id":"9cf2828127054765","output_index":0,"sequence_number":68,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" that.\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":69,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Checking","item_id":"9cf2828127054765","output_index":0,"sequence_number":70,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the","item_id":"9cf2828127054765","output_index":0,"sequence_number":71,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" schema: `","item_id":"9cf2828127054765","output_index":0,"sequence_number":72,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"agentic_ns","item_id":"9cf2828127054765","output_index":0,"sequence_number":73,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"__travel__","item_id":"9cf2828127054765","output_index":0,"sequence_number":74,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"get_timezone`,","item_id":"9cf2828127054765","output_index":0,"sequence_number":75,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" parameters: `","item_id":"9cf2828127054765","output_index":0,"sequence_number":76,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"{\"city\":","item_id":"9cf2828127054765","output_index":0,"sequence_number":77,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" {\"type\":","item_id":"9cf2828127054765","output_index":0,"sequence_number":78,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \"string\"}}","item_id":"9cf2828127054765","output_index":0,"sequence_number":79,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`.\nAll","item_id":"9cf2828127054765","output_index":0,"sequence_number":80,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" good.\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":81,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Proceeding.","item_id":"9cf2828127054765","output_index":0,"sequence_number":82,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \nNote","item_id":"9cf2828127054765","output_index":0,"sequence_number":83,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":": I","item_id":"9cf2828127054765","output_index":0,"sequence_number":84,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" need","item_id":"9cf2828127054765","output_index":0,"sequence_number":85,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" to format","item_id":"9cf2828127054765","output_index":0,"sequence_number":86,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" it","item_id":"9cf2828127054765","output_index":0,"sequence_number":87,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" correctly","item_id":"9cf2828127054765","output_index":0,"sequence_number":88,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n`","item_id":"9cf2828127054765","output_index":0,"sequence_number":89,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"agentic_ns","item_id":"9cf2828127054765","output_index":0,"sequence_number":90,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"__travel__","item_id":"9cf2828127054765","output_index":0,"sequence_number":91,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"get_timezone(city","item_id":"9cf2828127054765","output_index":0,"sequence_number":92,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"=\"Paris\")","item_id":"9cf2828127054765","output_index":0,"sequence_number":93,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"` or","item_id":"9cf2828127054765","output_index":0,"sequence_number":94,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" in","item_id":"9cf2828127054765","output_index":0,"sequence_number":95,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" JSON format","item_id":"9cf2828127054765","output_index":0,"sequence_number":96,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\nI","item_id":"9cf2828127054765","output_index":0,"sequence_number":97,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" will generate","item_id":"9cf2828127054765","output_index":0,"sequence_number":98,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the tool call","item_id":"9cf2828127054765","output_index":0,"sequence_number":99,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \n","item_id":"9cf2828127054765","output_index":0,"sequence_number":100,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Done. \n","item_id":"9cf2828127054765","output_index":0,"sequence_number":101,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Output","item_id":"9cf2828127054765","output_index":0,"sequence_number":102,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" matches expectation","item_id":"9cf2828127054765","output_index":0,"sequence_number":103,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \nProceed","item_id":"9cf2828127054765","output_index":0,"sequence_number":104,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \n[","item_id":"9cf2828127054765","output_index":0,"sequence_number":105,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Self","item_id":"9cf2828127054765","output_index":0,"sequence_number":106,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"-Correction","item_id":"9cf2828127054765","output_index":0,"sequence_number":107,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"/Verification]","item_id":"9cf2828127054765","output_index":0,"sequence_number":108,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nWait,","item_id":"9cf2828127054765","output_index":0,"sequence_number":109,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the prompt says","item_id":"9cf2828127054765","output_index":0,"sequence_number":110,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \"call","item_id":"9cf2828127054765","output_index":0,"sequence_number":111,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the loaded travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":112,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" namespace member get","item_id":"9cf2828127054765","output_index":0,"sequence_number":113,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_timezone exactly once","item_id":"9cf2828127054765","output_index":0,"sequence_number":114,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\".","item_id":"9cf2828127054765","output_index":0,"sequence_number":115,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" The tool name","item_id":"9cf2828127054765","output_index":0,"sequence_number":116,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" in the catalog","item_id":"9cf2828127054765","output_index":0,"sequence_number":117,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" is `ag","item_id":"9cf2828127054765","output_index":0,"sequence_number":118,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"entic_ns__","item_id":"9cf2828127054765","output_index":0,"sequence_number":119,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"travel__get","item_id":"9cf2828127054765","output_index":0,"sequence_number":120,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_timezone`. I","item_id":"9cf2828127054765","output_index":0,"sequence_number":121,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" will use exactly","item_id":"9cf2828127054765","output_index":0,"sequence_number":122,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" that.\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":123,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Parameters: `","item_id":"9cf2828127054765","output_index":0,"sequence_number":124,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"city","item_id":"9cf2828127054765","output_index":0,"sequence_number":125,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`:","item_id":"9cf2828127054765","output_index":0,"sequence_number":126,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `\"","item_id":"9cf2828127054765","output_index":0,"sequence_number":127,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Paris\"`","item_id":"9cf2828127054765","output_index":0,"sequence_number":128,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nReady.","item_id":"9cf2828127054765","output_index":0,"sequence_number":129,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" \nOutput generation","item_id":"9cf2828127054765","output_index":0,"sequence_number":130,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". \n[","item_id":"9cf2828127054765","output_index":0,"sequence_number":131,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Done","item_id":"9cf2828127054765","output_index":0,"sequence_number":132,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"]\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":133,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.done + + data: {"content_index":0,"item_id":"9cf2828127054765","output_index":0,"sequence_number":134,"text":"The + user wants me to call the `get_timezone` function from the `travel` namespace + with `city=\"Paris\"`.\nThe previous tool search result showed `travel` is a + namespace with description \"Travel location tools\".\nThe available tools include + `agentic_ns__travel__get_timezone` which matches the description \"Get the IANA + time zone for a city\".\nSo I will call `agentic_ns__travel__get_timezone` with + `city: \"Paris\"`.\nI must only call this tool and no others.\nParameters: `city: + \"Paris\"`.\nLet''s make the call. \nWait, the prompt says \"call the loaded + travel namespace member get_timezone exactly once\". The tool name in the schema + is `agentic_ns__travel__get_timezone`. I will use that.\nChecking the schema: + `agentic_ns__travel__get_timezone`, parameters: `{\"city\": {\"type\": \"string\"}}`.\nAll + good.\nProceeding. \nNote: I need to format it correctly.\n`agentic_ns__travel__get_timezone(city=\"Paris\")` + or in JSON format.\nI will generate the tool call. \nDone. \nOutput matches + expectation. \nProceed. \n[Self-Correction/Verification]\nWait, the prompt says + \"call the loaded travel namespace member get_timezone exactly once\". The tool + name in the catalog is `agentic_ns__travel__get_timezone`. I will use exactly + that.\nParameters: `city`: `\"Paris\"`\nReady. \nOutput generation. \n[Done]\n","type":"response.reasoning_text.done"} + + ' + - 'event: response.reasoning_part.done + + data: {"content_index":0,"item_id":"9cf2828127054765","output_index":0,"part":{"text":"The + user wants me to call the `get_timezone` function from the `travel` namespace + with `city=\"Paris\"`.\nThe previous tool search result showed `travel` is a + namespace with description \"Travel location tools\".\nThe available tools include + `agentic_ns__travel__get_timezone` which matches the description \"Get the IANA + time zone for a city\".\nSo I will call `agentic_ns__travel__get_timezone` with + `city: \"Paris\"`.\nI must only call this tool and no others.\nParameters: `city: + \"Paris\"`.\nLet''s make the call. \nWait, the prompt says \"call the loaded + travel namespace member get_timezone exactly once\". The tool name in the schema + is `agentic_ns__travel__get_timezone`. I will use that.\nChecking the schema: + `agentic_ns__travel__get_timezone`, parameters: `{\"city\": {\"type\": \"string\"}}`.\nAll + good.\nProceeding. \nNote: I need to format it correctly.\n`agentic_ns__travel__get_timezone(city=\"Paris\")` + or in JSON format.\nI will generate the tool call. \nDone. \nOutput matches + expectation. \nProceed. \n[Self-Correction/Verification]\nWait, the prompt says + \"call the loaded travel namespace member get_timezone exactly once\". The tool + name in the catalog is `agentic_ns__travel__get_timezone`. I will use exactly + that.\nParameters: `city`: `\"Paris\"`\nReady. \nOutput generation. \n[Done]\n","type":"reasoning_text"},"sequence_number":135,"type":"response.reasoning_part.done"} + + ' + - 'event: response.output_item.done + + data: {"item":{"content":[{"text":"The user wants me to call the `get_timezone` + function from the `travel` namespace with `city=\"Paris\"`.\nThe previous tool + search result showed `travel` is a namespace with description \"Travel location + tools\".\nThe available tools include `agentic_ns__travel__get_timezone` which + matches the description \"Get the IANA time zone for a city\".\nSo I will call + `agentic_ns__travel__get_timezone` with `city: \"Paris\"`.\nI must only call + this tool and no others.\nParameters: `city: \"Paris\"`.\nLet''s make the call. + \nWait, the prompt says \"call the loaded travel namespace member get_timezone + exactly once\". The tool name in the schema is `agentic_ns__travel__get_timezone`. + I will use that.\nChecking the schema: `agentic_ns__travel__get_timezone`, parameters: + `{\"city\": {\"type\": \"string\"}}`.\nAll good.\nProceeding. \nNote: I need + to format it correctly.\n`agentic_ns__travel__get_timezone(city=\"Paris\")` + or in JSON format.\nI will generate the tool call. \nDone. \nOutput matches + expectation. \nProceed. \n[Self-Correction/Verification]\nWait, the prompt says + \"call the loaded travel namespace member get_timezone exactly once\". The tool + name in the catalog is `agentic_ns__travel__get_timezone`. I will use exactly + that.\nParameters: `city`: `\"Paris\"`\nReady. \nOutput generation. \n[Done]\n","type":"reasoning_text"}],"encrypted_content":null,"id":"9cf2828127054765","status":"completed","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":136,"type":"response.output_item.done"} + + ' + - 'event: response.output_item.added + + data: {"item":{"arguments":"","call_id":"call_aad7ec9324a00032","caller":null,"id":"bf24556c9a9d47bb","name":"get_timezone","namespace":"travel","status":"in_progress","type":"function_call"},"output_index":1,"sequence_number":137,"type":"response.output_item.added"} + + ' + - 'event: response.function_call_arguments.delta + + data: {"delta":"{\"city\": \"Paris","item_id":"bf24556c9a9d47bb","output_index":1,"sequence_number":138,"type":"response.function_call_arguments.delta"} + + ' + - 'event: response.function_call_arguments.delta + + data: {"delta":"\"}","item_id":"bf24556c9a9d47bb","output_index":1,"sequence_number":139,"type":"response.function_call_arguments.delta"} + + ' + - 'event: response.function_call_arguments.done + + data: {"arguments":"{\"city\": \"Paris\"}","item_id":"bf24556c9a9d47bb","name":"agentic_ns__travel__get_timezone","output_index":1,"sequence_number":140,"type":"response.function_call_arguments.done"} + + ' + - 'event: response.output_item.done + + data: {"item":{"arguments":"{\"city\": \"Paris\"}","call_id":"call_aad7ec9324a00032","caller":null,"id":"bf24556c9a9d47bb","name":"get_timezone","namespace":"travel","status":"completed","type":"function_call"},"output_index":1,"sequence_number":141,"type":"response.output_item.done"} + + ' + - 'event: response.completed + + data: {"response":{"conversation_id":null,"created_at":1787715712,"error":null,"id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to call the `get_timezone` function from the `travel` namespace + with `city=\"Paris\"`.\nThe previous tool search result showed `travel` is a + namespace with description \"Travel location tools\".\nThe available tools include + `agentic_ns__travel__get_timezone` which matches the description \"Get the IANA + time zone for a city\".\nSo I will call `agentic_ns__travel__get_timezone` with + `city: \"Paris\"`.\nI must only call this tool and no others.\nParameters: `city: + \"Paris\"`.\nLet''s make the call. \nWait, the prompt says \"call the loaded + travel namespace member get_timezone exactly once\". The tool name in the schema + is `agentic_ns__travel__get_timezone`. I will use that.\nChecking the schema: + `agentic_ns__travel__get_timezone`, parameters: `{\"city\": {\"type\": \"string\"}}`.\nAll + good.\nProceeding. \nNote: I need to format it correctly.\n`agentic_ns__travel__get_timezone(city=\"Paris\")` + or in JSON format.\nI will generate the tool call. \nDone. \nOutput matches + expectation. \nProceed. \n[Self-Correction/Verification]\nWait, the prompt says + \"call the loaded travel namespace member get_timezone exactly once\". The tool + name in the catalog is `agentic_ns__travel__get_timezone`. I will use exactly + that.\nParameters: `city`: `\"Paris\"`\nReady. \nOutput generation. \n[Done]\n","type":"reasoning_text"}],"encrypted_content":null,"id":"9cf2828127054765","status":null,"summary":[],"type":"reasoning"},{"arguments":"{\"city\": + \"Paris\"}","call_id":"call_aad7ec9324a00032","id":"bf24556c9a9d47bb","name":"get_timezone","namespace":"travel","status":"completed","type":"function_call"}],"previous_response_id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","status":"completed","tool_choice":{"name":"get_timezone","namespace":"travel","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":801,"input_tokens_details":{"cached_tokens":0},"output_tokens":356,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1157}},"sequence_number":142,"type":"response.completed"} + + ' + - 'data: [DONE] + + ' + status_code: 101 + websocket: + - '{"response":{"background":false,"created_at":1787715710,"frequency_penalty":0.0,"id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_timezone","namespace":"travel","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"}' + - '{"response":{"background":false,"created_at":1787715710,"frequency_penalty":0.0,"id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":{"name":"get_timezone","namespace":"travel","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"}' + - '{"item":{"content":null,"encrypted_content":null,"id":"9cf2828127054765","status":"in_progress","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"}' + - '{"content_index":0,"item_id":"9cf2828127054765","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"}' + - '{"content_index":0,"delta":"The","item_id":"9cf2828127054765","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" user wants me","item_id":"9cf2828127054765","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" to call the","item_id":"9cf2828127054765","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `get_timezone","item_id":"9cf2828127054765","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"` function from","item_id":"9cf2828127054765","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the `travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"` namespace with","item_id":"9cf2828127054765","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `city","item_id":"9cf2828127054765","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"=\"Paris\"","item_id":"9cf2828127054765","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`.\nThe","item_id":"9cf2828127054765","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" previous","item_id":"9cf2828127054765","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tool search","item_id":"9cf2828127054765","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" result showed `","item_id":"9cf2828127054765","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"travel` is","item_id":"9cf2828127054765","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" a namespace with","item_id":"9cf2828127054765","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" description","item_id":"9cf2828127054765","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \"Travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" location tools\".","item_id":"9cf2828127054765","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nThe available","item_id":"9cf2828127054765","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tools include","item_id":"9cf2828127054765","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `agentic","item_id":"9cf2828127054765","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_ns__travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"__get_timezone","item_id":"9cf2828127054765","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`","item_id":"9cf2828127054765","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" which matches","item_id":"9cf2828127054765","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the description \"","item_id":"9cf2828127054765","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Get the I","item_id":"9cf2828127054765","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"ANA time zone","item_id":"9cf2828127054765","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for a city","item_id":"9cf2828127054765","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\".\nSo","item_id":"9cf2828127054765","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" I will call","item_id":"9cf2828127054765","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `agentic","item_id":"9cf2828127054765","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_ns__travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"__get_timezone","item_id":"9cf2828127054765","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"` with `","item_id":"9cf2828127054765","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"city:","item_id":"9cf2828127054765","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \"Paris\"","item_id":"9cf2828127054765","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`.\nI","item_id":"9cf2828127054765","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" must","item_id":"9cf2828127054765","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" only","item_id":"9cf2828127054765","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" call this tool","item_id":"9cf2828127054765","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and","item_id":"9cf2828127054765","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" no","item_id":"9cf2828127054765","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" others.\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Parameters: `","item_id":"9cf2828127054765","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"city","item_id":"9cf2828127054765","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":": \"Paris","item_id":"9cf2828127054765","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\"","item_id":"9cf2828127054765","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`.\nLet","item_id":"9cf2828127054765","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"''s make","item_id":"9cf2828127054765","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the call.","item_id":"9cf2828127054765","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \n","item_id":"9cf2828127054765","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Wait, the","item_id":"9cf2828127054765","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" prompt","item_id":"9cf2828127054765","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" says \"call","item_id":"9cf2828127054765","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the loaded travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" namespace member get","item_id":"9cf2828127054765","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_timezone exactly once","item_id":"9cf2828127054765","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\". The tool","item_id":"9cf2828127054765","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" name in the","item_id":"9cf2828127054765","output_index":0,"sequence_number":63,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" schema is `","item_id":"9cf2828127054765","output_index":0,"sequence_number":64,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"agentic_ns","item_id":"9cf2828127054765","output_index":0,"sequence_number":65,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"__travel__","item_id":"9cf2828127054765","output_index":0,"sequence_number":66,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"get_timezone`.","item_id":"9cf2828127054765","output_index":0,"sequence_number":67,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" I will use","item_id":"9cf2828127054765","output_index":0,"sequence_number":68,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" that.\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":69,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Checking","item_id":"9cf2828127054765","output_index":0,"sequence_number":70,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the","item_id":"9cf2828127054765","output_index":0,"sequence_number":71,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" schema: `","item_id":"9cf2828127054765","output_index":0,"sequence_number":72,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"agentic_ns","item_id":"9cf2828127054765","output_index":0,"sequence_number":73,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"__travel__","item_id":"9cf2828127054765","output_index":0,"sequence_number":74,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"get_timezone`,","item_id":"9cf2828127054765","output_index":0,"sequence_number":75,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" parameters: `","item_id":"9cf2828127054765","output_index":0,"sequence_number":76,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"{\"city\":","item_id":"9cf2828127054765","output_index":0,"sequence_number":77,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" {\"type\":","item_id":"9cf2828127054765","output_index":0,"sequence_number":78,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \"string\"}}","item_id":"9cf2828127054765","output_index":0,"sequence_number":79,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`.\nAll","item_id":"9cf2828127054765","output_index":0,"sequence_number":80,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" good.\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":81,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Proceeding.","item_id":"9cf2828127054765","output_index":0,"sequence_number":82,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \nNote","item_id":"9cf2828127054765","output_index":0,"sequence_number":83,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":": I","item_id":"9cf2828127054765","output_index":0,"sequence_number":84,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" need","item_id":"9cf2828127054765","output_index":0,"sequence_number":85,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" to format","item_id":"9cf2828127054765","output_index":0,"sequence_number":86,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" it","item_id":"9cf2828127054765","output_index":0,"sequence_number":87,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" correctly","item_id":"9cf2828127054765","output_index":0,"sequence_number":88,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n`","item_id":"9cf2828127054765","output_index":0,"sequence_number":89,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"agentic_ns","item_id":"9cf2828127054765","output_index":0,"sequence_number":90,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"__travel__","item_id":"9cf2828127054765","output_index":0,"sequence_number":91,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"get_timezone(city","item_id":"9cf2828127054765","output_index":0,"sequence_number":92,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"=\"Paris\")","item_id":"9cf2828127054765","output_index":0,"sequence_number":93,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"` or","item_id":"9cf2828127054765","output_index":0,"sequence_number":94,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" in","item_id":"9cf2828127054765","output_index":0,"sequence_number":95,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" JSON format","item_id":"9cf2828127054765","output_index":0,"sequence_number":96,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\nI","item_id":"9cf2828127054765","output_index":0,"sequence_number":97,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" will generate","item_id":"9cf2828127054765","output_index":0,"sequence_number":98,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the tool call","item_id":"9cf2828127054765","output_index":0,"sequence_number":99,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \n","item_id":"9cf2828127054765","output_index":0,"sequence_number":100,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Done. \n","item_id":"9cf2828127054765","output_index":0,"sequence_number":101,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Output","item_id":"9cf2828127054765","output_index":0,"sequence_number":102,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" matches expectation","item_id":"9cf2828127054765","output_index":0,"sequence_number":103,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \nProceed","item_id":"9cf2828127054765","output_index":0,"sequence_number":104,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \n[","item_id":"9cf2828127054765","output_index":0,"sequence_number":105,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Self","item_id":"9cf2828127054765","output_index":0,"sequence_number":106,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"-Correction","item_id":"9cf2828127054765","output_index":0,"sequence_number":107,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"/Verification]","item_id":"9cf2828127054765","output_index":0,"sequence_number":108,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nWait,","item_id":"9cf2828127054765","output_index":0,"sequence_number":109,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the prompt says","item_id":"9cf2828127054765","output_index":0,"sequence_number":110,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \"call","item_id":"9cf2828127054765","output_index":0,"sequence_number":111,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the loaded travel","item_id":"9cf2828127054765","output_index":0,"sequence_number":112,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" namespace member get","item_id":"9cf2828127054765","output_index":0,"sequence_number":113,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_timezone exactly once","item_id":"9cf2828127054765","output_index":0,"sequence_number":114,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\".","item_id":"9cf2828127054765","output_index":0,"sequence_number":115,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" The tool name","item_id":"9cf2828127054765","output_index":0,"sequence_number":116,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" in the catalog","item_id":"9cf2828127054765","output_index":0,"sequence_number":117,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" is `ag","item_id":"9cf2828127054765","output_index":0,"sequence_number":118,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"entic_ns__","item_id":"9cf2828127054765","output_index":0,"sequence_number":119,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"travel__get","item_id":"9cf2828127054765","output_index":0,"sequence_number":120,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_timezone`. I","item_id":"9cf2828127054765","output_index":0,"sequence_number":121,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" will use exactly","item_id":"9cf2828127054765","output_index":0,"sequence_number":122,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" that.\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":123,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Parameters: `","item_id":"9cf2828127054765","output_index":0,"sequence_number":124,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"city","item_id":"9cf2828127054765","output_index":0,"sequence_number":125,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`:","item_id":"9cf2828127054765","output_index":0,"sequence_number":126,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `\"","item_id":"9cf2828127054765","output_index":0,"sequence_number":127,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Paris\"`","item_id":"9cf2828127054765","output_index":0,"sequence_number":128,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nReady.","item_id":"9cf2828127054765","output_index":0,"sequence_number":129,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" \nOutput generation","item_id":"9cf2828127054765","output_index":0,"sequence_number":130,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". \n[","item_id":"9cf2828127054765","output_index":0,"sequence_number":131,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Done","item_id":"9cf2828127054765","output_index":0,"sequence_number":132,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"]\n","item_id":"9cf2828127054765","output_index":0,"sequence_number":133,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"item_id":"9cf2828127054765","output_index":0,"sequence_number":134,"text":"The + user wants me to call the `get_timezone` function from the `travel` namespace + with `city=\"Paris\"`.\nThe previous tool search result showed `travel` is a + namespace with description \"Travel location tools\".\nThe available tools include + `agentic_ns__travel__get_timezone` which matches the description \"Get the IANA + time zone for a city\".\nSo I will call `agentic_ns__travel__get_timezone` with + `city: \"Paris\"`.\nI must only call this tool and no others.\nParameters: `city: + \"Paris\"`.\nLet''s make the call. \nWait, the prompt says \"call the loaded + travel namespace member get_timezone exactly once\". The tool name in the schema + is `agentic_ns__travel__get_timezone`. I will use that.\nChecking the schema: + `agentic_ns__travel__get_timezone`, parameters: `{\"city\": {\"type\": \"string\"}}`.\nAll + good.\nProceeding. \nNote: I need to format it correctly.\n`agentic_ns__travel__get_timezone(city=\"Paris\")` + or in JSON format.\nI will generate the tool call. \nDone. \nOutput matches + expectation. \nProceed. \n[Self-Correction/Verification]\nWait, the prompt says + \"call the loaded travel namespace member get_timezone exactly once\". The tool + name in the catalog is `agentic_ns__travel__get_timezone`. I will use exactly + that.\nParameters: `city`: `\"Paris\"`\nReady. \nOutput generation. \n[Done]\n","type":"response.reasoning_text.done"}' + - '{"content_index":0,"item_id":"9cf2828127054765","output_index":0,"part":{"text":"The + user wants me to call the `get_timezone` function from the `travel` namespace + with `city=\"Paris\"`.\nThe previous tool search result showed `travel` is a + namespace with description \"Travel location tools\".\nThe available tools include + `agentic_ns__travel__get_timezone` which matches the description \"Get the IANA + time zone for a city\".\nSo I will call `agentic_ns__travel__get_timezone` with + `city: \"Paris\"`.\nI must only call this tool and no others.\nParameters: `city: + \"Paris\"`.\nLet''s make the call. \nWait, the prompt says \"call the loaded + travel namespace member get_timezone exactly once\". The tool name in the schema + is `agentic_ns__travel__get_timezone`. I will use that.\nChecking the schema: + `agentic_ns__travel__get_timezone`, parameters: `{\"city\": {\"type\": \"string\"}}`.\nAll + good.\nProceeding. \nNote: I need to format it correctly.\n`agentic_ns__travel__get_timezone(city=\"Paris\")` + or in JSON format.\nI will generate the tool call. \nDone. \nOutput matches + expectation. \nProceed. \n[Self-Correction/Verification]\nWait, the prompt says + \"call the loaded travel namespace member get_timezone exactly once\". The tool + name in the catalog is `agentic_ns__travel__get_timezone`. I will use exactly + that.\nParameters: `city`: `\"Paris\"`\nReady. \nOutput generation. \n[Done]\n","type":"reasoning_text"},"sequence_number":135,"type":"response.reasoning_part.done"}' + - '{"item":{"content":[{"text":"The user wants me to call the `get_timezone` function + from the `travel` namespace with `city=\"Paris\"`.\nThe previous tool search + result showed `travel` is a namespace with description \"Travel location tools\".\nThe + available tools include `agentic_ns__travel__get_timezone` which matches the + description \"Get the IANA time zone for a city\".\nSo I will call `agentic_ns__travel__get_timezone` + with `city: \"Paris\"`.\nI must only call this tool and no others.\nParameters: + `city: \"Paris\"`.\nLet''s make the call. \nWait, the prompt says \"call the + loaded travel namespace member get_timezone exactly once\". The tool name in + the schema is `agentic_ns__travel__get_timezone`. I will use that.\nChecking + the schema: `agentic_ns__travel__get_timezone`, parameters: `{\"city\": {\"type\": + \"string\"}}`.\nAll good.\nProceeding. \nNote: I need to format it correctly.\n`agentic_ns__travel__get_timezone(city=\"Paris\")` + or in JSON format.\nI will generate the tool call. \nDone. \nOutput matches + expectation. \nProceed. \n[Self-Correction/Verification]\nWait, the prompt says + \"call the loaded travel namespace member get_timezone exactly once\". The tool + name in the catalog is `agentic_ns__travel__get_timezone`. I will use exactly + that.\nParameters: `city`: `\"Paris\"`\nReady. \nOutput generation. \n[Done]\n","type":"reasoning_text"}],"encrypted_content":null,"id":"9cf2828127054765","status":"completed","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":136,"type":"response.output_item.done"}' + - '{"item":{"arguments":"","call_id":"call_aad7ec9324a00032","caller":null,"id":"bf24556c9a9d47bb","name":"get_timezone","namespace":"travel","status":"in_progress","type":"function_call"},"output_index":1,"sequence_number":137,"type":"response.output_item.added"}' + - '{"delta":"{\"city\": \"Paris","item_id":"bf24556c9a9d47bb","output_index":1,"sequence_number":138,"type":"response.function_call_arguments.delta"}' + - '{"delta":"\"}","item_id":"bf24556c9a9d47bb","output_index":1,"sequence_number":139,"type":"response.function_call_arguments.delta"}' + - '{"arguments":"{\"city\": \"Paris\"}","item_id":"bf24556c9a9d47bb","name":"agentic_ns__travel__get_timezone","output_index":1,"sequence_number":140,"type":"response.function_call_arguments.done"}' + - '{"item":{"arguments":"{\"city\": \"Paris\"}","call_id":"call_aad7ec9324a00032","caller":null,"id":"bf24556c9a9d47bb","name":"get_timezone","namespace":"travel","status":"completed","type":"function_call"},"output_index":1,"sequence_number":141,"type":"response.output_item.done"}' + - '{"response":{"conversation_id":null,"created_at":1787715712,"error":null,"id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to call the `get_timezone` function from the `travel` namespace + with `city=\"Paris\"`.\nThe previous tool search result showed `travel` is a + namespace with description \"Travel location tools\".\nThe available tools include + `agentic_ns__travel__get_timezone` which matches the description \"Get the IANA + time zone for a city\".\nSo I will call `agentic_ns__travel__get_timezone` with + `city: \"Paris\"`.\nI must only call this tool and no others.\nParameters: `city: + \"Paris\"`.\nLet''s make the call. \nWait, the prompt says \"call the loaded + travel namespace member get_timezone exactly once\". The tool name in the schema + is `agentic_ns__travel__get_timezone`. I will use that.\nChecking the schema: + `agentic_ns__travel__get_timezone`, parameters: `{\"city\": {\"type\": \"string\"}}`.\nAll + good.\nProceeding. \nNote: I need to format it correctly.\n`agentic_ns__travel__get_timezone(city=\"Paris\")` + or in JSON format.\nI will generate the tool call. \nDone. \nOutput matches + expectation. \nProceed. \n[Self-Correction/Verification]\nWait, the prompt says + \"call the loaded travel namespace member get_timezone exactly once\". The tool + name in the catalog is `agentic_ns__travel__get_timezone`. I will use exactly + that.\nParameters: `city`: `\"Paris\"`\nReady. \nOutput generation. \n[Done]\n","type":"reasoning_text"}],"encrypted_content":null,"id":"9cf2828127054765","status":null,"summary":[],"type":"reasoning"},{"arguments":"{\"city\": + \"Paris\"}","call_id":"call_aad7ec9324a00032","id":"bf24556c9a9d47bb","name":"get_timezone","namespace":"travel","status":"completed","type":"function_call"}],"previous_response_id":"resp_01a03c28-eb1e-7f92-b5a0-86fedc28f67a","status":"completed","tool_choice":{"name":"get_timezone","namespace":"travel","type":"function"},"tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":801,"input_tokens_details":{"cached_tokens":0},"output_tokens":356,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1157}},"sequence_number":142,"type":"response.completed"}' +- filename: t4 + request: + body: + input: + - call_id: call_aad7ec9324a00032 + output: '{"city":"Paris","iana_timezone":"Europe/Paris"}' + type: function_call_output + - content: Use both function outputs and call no more tools. Reply with exactly + PARIS_MIXED_TOOLS_OK. + role: user + type: message + max_output_tokens: 4096 + model: Qwen/Qwen3.6-35B-A3B-FP8 + parallel_tool_calls: false + previous_response_id: resp_01a03c28-effe-7a91-b578-e382c29bdd22 + store: true + tool_choice: none + type: response.create + headers: {} + method: WEBSOCKET + path: /v1/responses + query_params: {} + transport: websocket + response: + headers: + transport: websocket + sse: + - 'event: response.created + + data: {"response":{"background":false,"created_at":1787715715,"frequency_penalty":0.0,"id":"resp_01a03c29-014a-79f2-a0dd-3ccd8292e36f","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"none","tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"} + + ' + - 'event: response.in_progress + + data: {"response":{"background":false,"created_at":1787715715,"frequency_penalty":0.0,"id":"resp_01a03c29-014a-79f2-a0dd-3ccd8292e36f","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"none","tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"} + + ' + - 'event: response.output_item.added + + data: {"item":{"content":null,"encrypted_content":null,"id":"8cbfc23565c7cd52","status":"in_progress","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} + + ' + - 'event: response.reasoning_part.added + + data: {"content_index":0,"item_id":"8cbfc23565c7cd52","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"The","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" user wants me","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" to verify","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" if","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" tool","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" provided","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" are sufficient","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" to","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" complete","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" a","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" task and","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" then reply","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" with a specific","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" string \"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"PARIS_MIX","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"ED_TOOLS","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_OK\".\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"The","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" task","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" implied","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" is","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" likely","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" getting","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" weather and timezone","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for Paris","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\nThe","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" are:\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"1. Weather","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Paris: {\"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"city\":\"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Paris\",\"condition","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\":\"clear\",\"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"temperature_c\":","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"21}","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\n2.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Timezone for","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Paris: {\"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"city\":\"Paris","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\",\"iana_timezone","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\":\"Europe/","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Paris\"}\n\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"The user instruction","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" says","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":": \"Use","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" both function outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and call no","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" more tools.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Reply with exactly","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" PARIS_MIXED","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_TOOLS_OK","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\"\n\nI","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" have the","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" weather","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" data","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":63,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the timezone","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":64,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" data for","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":65,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Paris. I","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":66,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" have used","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":67,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" both","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":68,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" function outputs.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":69,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" I am","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":70,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" calling","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":71,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" no more tools","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":72,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". I just","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":73,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" need to output","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":74,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the specific","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":75,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" string requested","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":76,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n\nPlan","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":77,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":":\n1","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":78,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". Confirm","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":79,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" I","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":80,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" have the outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":81,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n2","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":82,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". Confirm","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":83,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" I am","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":84,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" not calling any","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":85,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" more tools.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":86,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\n3.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":87,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Output \"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":88,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"PARIS_MIX","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":89,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"ED_TOOLS","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":90,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_OK\".\n\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":91,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"Let","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":92,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"''s double check","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":93,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" the previous turns","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":94,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" to","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":95,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" ensure no","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":96,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" context","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":97,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" is","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":98,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" missing","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":99,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\nTurn","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":100,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" 1:","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":101,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":102,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"tool_search`","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":103,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" found `","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":104,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"get_weather`","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":105,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and `travel","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":106,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"`.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":107,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nTurn ","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":108,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"2: `","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":109,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"get_weather`","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":110,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" returned","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":111,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" weather","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":112,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for Paris.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":113,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nTurn ","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":114,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"3: `","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":115,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"travel","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":116,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"` namespace","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":117,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" member `get","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":118,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_timezone` (","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":119,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"named","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":120,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" `ag","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":121,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"entic_ns__","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":122,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"travel__get","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":123,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"_timezone`","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":124,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" in the execution","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":125,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":") returned timezone","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":126,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" for Paris.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":127,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":"\nCurrent","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":128,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" Turn: User","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":129,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" asks to use","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":130,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":131,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" and reply with","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":132,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" string","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":133,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n\nThe","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":134,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" request","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":135,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" is straightforward","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":136,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":". I will","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":137,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":" output the string","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":138,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.delta + + data: {"content_index":0,"delta":".\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":139,"type":"response.reasoning_text.delta"} + + ' + - 'event: response.reasoning_text.done + + data: {"content_index":0,"item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":140,"text":"The + user wants me to verify if the tool outputs provided are sufficient to complete + a task and then reply with a specific string \"PARIS_MIXED_TOOLS_OK\".\nThe + task implied is likely getting the weather and timezone for Paris.\nThe outputs + are:\n1. Weather for Paris: {\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}\n2. + Timezone for Paris: {\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}\n\nThe + user instruction says: \"Use both function outputs and call no more tools. Reply + with exactly PARIS_MIXED_TOOLS_OK.\"\n\nI have the weather data and the timezone + data for Paris. I have used both function outputs. I am calling no more tools. + I just need to output the specific string requested.\n\nPlan:\n1. Confirm I + have the outputs.\n2. Confirm I am not calling any more tools.\n3. Output \"PARIS_MIXED_TOOLS_OK\".\n\nLet''s + double check the previous turns to ensure no context is missing.\nTurn 1: `tool_search` + found `get_weather` and `travel`.\nTurn 2: `get_weather` returned weather for + Paris.\nTurn 3: `travel` namespace member `get_timezone` (named `agentic_ns__travel__get_timezone` + in the execution) returned timezone for Paris.\nCurrent Turn: User asks to use + outputs and reply with string.\n\nThe request is straightforward. I will output + the string.\n","type":"response.reasoning_text.done"} + + ' + - 'event: response.reasoning_part.done + + data: {"content_index":0,"item_id":"8cbfc23565c7cd52","output_index":0,"part":{"text":"The + user wants me to verify if the tool outputs provided are sufficient to complete + a task and then reply with a specific string \"PARIS_MIXED_TOOLS_OK\".\nThe + task implied is likely getting the weather and timezone for Paris.\nThe outputs + are:\n1. Weather for Paris: {\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}\n2. + Timezone for Paris: {\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}\n\nThe + user instruction says: \"Use both function outputs and call no more tools. Reply + with exactly PARIS_MIXED_TOOLS_OK.\"\n\nI have the weather data and the timezone + data for Paris. I have used both function outputs. I am calling no more tools. + I just need to output the specific string requested.\n\nPlan:\n1. Confirm I + have the outputs.\n2. Confirm I am not calling any more tools.\n3. Output \"PARIS_MIXED_TOOLS_OK\".\n\nLet''s + double check the previous turns to ensure no context is missing.\nTurn 1: `tool_search` + found `get_weather` and `travel`.\nTurn 2: `get_weather` returned weather for + Paris.\nTurn 3: `travel` namespace member `get_timezone` (named `agentic_ns__travel__get_timezone` + in the execution) returned timezone for Paris.\nCurrent Turn: User asks to use + outputs and reply with string.\n\nThe request is straightforward. I will output + the string.\n","type":"reasoning_text"},"sequence_number":141,"type":"response.reasoning_part.done"} + + ' + - 'event: response.output_item.done + + data: {"item":{"content":[{"text":"The user wants me to verify if the tool outputs + provided are sufficient to complete a task and then reply with a specific string + \"PARIS_MIXED_TOOLS_OK\".\nThe task implied is likely getting the weather and + timezone for Paris.\nThe outputs are:\n1. Weather for Paris: {\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}\n2. + Timezone for Paris: {\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}\n\nThe + user instruction says: \"Use both function outputs and call no more tools. Reply + with exactly PARIS_MIXED_TOOLS_OK.\"\n\nI have the weather data and the timezone + data for Paris. I have used both function outputs. I am calling no more tools. + I just need to output the specific string requested.\n\nPlan:\n1. Confirm I + have the outputs.\n2. Confirm I am not calling any more tools.\n3. Output \"PARIS_MIXED_TOOLS_OK\".\n\nLet''s + double check the previous turns to ensure no context is missing.\nTurn 1: `tool_search` + found `get_weather` and `travel`.\nTurn 2: `get_weather` returned weather for + Paris.\nTurn 3: `travel` namespace member `get_timezone` (named `agentic_ns__travel__get_timezone` + in the execution) returned timezone for Paris.\nCurrent Turn: User asks to use + outputs and reply with string.\n\nThe request is straightforward. I will output + the string.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"8cbfc23565c7cd52","status":"completed","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":142,"type":"response.output_item.done"} + + ' + - 'event: response.output_item.added + + data: {"item":{"content":[],"id":"aba0b5f455081834","phase":null,"role":"assistant","status":"in_progress","type":"message"},"output_index":1,"sequence_number":143,"type":"response.output_item.added"} + + ' + - 'event: response.content_part.added + + data: {"content_index":0,"item_id":"aba0b5f455081834","output_index":1,"part":{"annotations":[],"logprobs":[],"text":"","type":"output_text"},"sequence_number":144,"type":"response.content_part.added"} + + ' + - 'event: response.output_text.delta + + data: {"content_index":0,"delta":"\n\nPARIS","item_id":"aba0b5f455081834","logprobs":[],"output_index":1,"sequence_number":145,"type":"response.output_text.delta"} + + ' + - 'event: response.output_text.delta + + data: {"content_index":0,"delta":"_MIXED_TO","item_id":"aba0b5f455081834","logprobs":[],"output_index":1,"sequence_number":146,"type":"response.output_text.delta"} + + ' + - 'event: response.output_text.delta + + data: {"content_index":0,"delta":"OLS_OK","item_id":"aba0b5f455081834","logprobs":[],"output_index":1,"sequence_number":147,"type":"response.output_text.delta"} + + ' + - 'event: response.output_text.done + + data: {"content_index":0,"item_id":"aba0b5f455081834","logprobs":[],"output_index":1,"sequence_number":148,"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"response.output_text.done"} + + ' + - 'event: response.content_part.done + + data: {"content_index":0,"item_id":"aba0b5f455081834","output_index":1,"part":{"annotations":[],"logprobs":null,"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text"},"sequence_number":149,"type":"response.content_part.done"} + + ' + - 'event: response.output_item.done + + data: {"item":{"content":[{"annotations":[],"logprobs":null,"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text"}],"id":"aba0b5f455081834","phase":null,"role":"assistant","status":"completed","summary":[],"type":"message"},"output_index":1,"sequence_number":150,"type":"response.output_item.done"} + + ' + - 'event: response.completed + + data: {"response":{"conversation_id":null,"created_at":1787715717,"error":null,"id":"resp_01a03c29-014a-79f2-a0dd-3ccd8292e36f","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to verify if the tool outputs provided are sufficient to complete + a task and then reply with a specific string \"PARIS_MIXED_TOOLS_OK\".\nThe + task implied is likely getting the weather and timezone for Paris.\nThe outputs + are:\n1. Weather for Paris: {\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}\n2. + Timezone for Paris: {\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}\n\nThe + user instruction says: \"Use both function outputs and call no more tools. Reply + with exactly PARIS_MIXED_TOOLS_OK.\"\n\nI have the weather data and the timezone + data for Paris. I have used both function outputs. I am calling no more tools. + I just need to output the specific string requested.\n\nPlan:\n1. Confirm I + have the outputs.\n2. Confirm I am not calling any more tools.\n3. Output \"PARIS_MIXED_TOOLS_OK\".\n\nLet''s + double check the previous turns to ensure no context is missing.\nTurn 1: `tool_search` + found `get_weather` and `travel`.\nTurn 2: `get_weather` returned weather for + Paris.\nTurn 3: `travel` namespace member `get_timezone` (named `agentic_ns__travel__get_timezone` + in the execution) returned timezone for Paris.\nCurrent Turn: User asks to use + outputs and reply with string.\n\nThe request is straightforward. I will output + the string.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"8cbfc23565c7cd52","status":null,"summary":[],"type":"reasoning"},{"content":[{"annotations":[],"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text"}],"id":"aba0b5f455081834","role":"assistant","status":"completed","type":"message"}],"previous_response_id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","status":"completed","tool_choice":"none","tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":346,"input_tokens_details":{"cached_tokens":0},"output_tokens":314,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":660}},"sequence_number":151,"type":"response.completed"} + + ' + - 'data: [DONE] + + ' + status_code: 101 + websocket: + - '{"response":{"background":false,"created_at":1787715715,"frequency_penalty":0.0,"id":"resp_01a03c29-014a-79f2-a0dd-3ccd8292e36f","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"none","tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"}' + - '{"response":{"background":false,"created_at":1787715715,"frequency_penalty":0.0,"id":"resp_01a03c29-014a-79f2-a0dd-3ccd8292e36f","incomplete_details":null,"input_messages":null,"instructions":null,"kv_transfer_params":null,"max_output_tokens":4096,"max_tool_calls":null,"metadata":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[],"output_messages":null,"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","prompt":null,"reasoning":null,"service_tier":"auto","status":"in_progress","temperature":1.0,"text":null,"tool_choice":"none","tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"top_logprobs":null,"top_p":0.95,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"}' + - '{"item":{"content":null,"encrypted_content":null,"id":"8cbfc23565c7cd52","status":"in_progress","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"}' + - '{"content_index":0,"item_id":"8cbfc23565c7cd52","output_index":0,"part":{"text":"","type":"reasoning_text"},"sequence_number":3,"type":"response.reasoning_part.added"}' + - '{"content_index":0,"delta":"The","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":4,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" user wants me","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":5,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" to verify","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":6,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" if","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":7,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":8,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" tool","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":9,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":10,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" provided","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":11,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" are sufficient","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":12,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" to","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":13,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" complete","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":14,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" a","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":15,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" task and","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":16,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" then reply","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":17,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" with a specific","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":18,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" string \"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":19,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"PARIS_MIX","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":20,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"ED_TOOLS","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":21,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_OK\".\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":22,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"The","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":23,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" task","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":24,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" implied","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":25,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" is","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":26,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" likely","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":27,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" getting","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":28,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":29,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" weather and timezone","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":30,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for Paris","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":31,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\nThe","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":32,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":33,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" are:\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":34,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"1. Weather","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":35,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":36,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Paris: {\"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":37,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"city\":\"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":38,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Paris\",\"condition","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":39,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\":\"clear\",\"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":40,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"temperature_c\":","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":41,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"21}","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":42,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\n2.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":43,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Timezone for","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":44,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Paris: {\"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":45,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"city\":\"Paris","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":46,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\",\"iana_timezone","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":47,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\":\"Europe/","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":48,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Paris\"}\n\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":49,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"The user instruction","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":50,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" says","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":51,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":": \"Use","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":52,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" both function outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":53,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and call no","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":54,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" more tools.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":55,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Reply with exactly","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":56,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" PARIS_MIXED","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":57,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_TOOLS_OK","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":58,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\"\n\nI","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":59,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" have the","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":60,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" weather","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":61,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" data","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":62,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":63,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the timezone","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":64,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" data for","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":65,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Paris. I","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":66,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" have used","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":67,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" both","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":68,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" function outputs.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":69,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" I am","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":70,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" calling","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":71,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" no more tools","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":72,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". I just","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":73,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" need to output","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":74,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the specific","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":75,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" string requested","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":76,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n\nPlan","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":77,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":":\n1","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":78,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". Confirm","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":79,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" I","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":80,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" have the outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":81,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n2","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":82,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". Confirm","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":83,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" I am","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":84,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" not calling any","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":85,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" more tools.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":86,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\n3.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":87,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Output \"","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":88,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"PARIS_MIX","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":89,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"ED_TOOLS","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":90,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_OK\".\n\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":91,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"Let","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":92,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"''s double check","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":93,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" the previous turns","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":94,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" to","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":95,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" ensure no","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":96,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" context","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":97,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" is","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":98,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" missing","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":99,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\nTurn","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":100,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" 1:","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":101,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":102,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"tool_search`","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":103,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" found `","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":104,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"get_weather`","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":105,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and `travel","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":106,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"`.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":107,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nTurn ","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":108,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"2: `","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":109,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"get_weather`","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":110,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" returned","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":111,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" weather","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":112,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for Paris.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":113,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nTurn ","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":114,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"3: `","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":115,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"travel","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":116,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"` namespace","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":117,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" member `get","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":118,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_timezone` (","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":119,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"named","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":120,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" `ag","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":121,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"entic_ns__","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":122,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"travel__get","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":123,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"_timezone`","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":124,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" in the execution","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":125,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":") returned timezone","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":126,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" for Paris.","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":127,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":"\nCurrent","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":128,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" Turn: User","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":129,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" asks to use","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":130,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" outputs","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":131,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" and reply with","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":132,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" string","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":133,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n\nThe","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":134,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" request","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":135,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" is straightforward","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":136,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":". I will","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":137,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":" output the string","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":138,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"delta":".\n","item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":139,"type":"response.reasoning_text.delta"}' + - '{"content_index":0,"item_id":"8cbfc23565c7cd52","output_index":0,"sequence_number":140,"text":"The + user wants me to verify if the tool outputs provided are sufficient to complete + a task and then reply with a specific string \"PARIS_MIXED_TOOLS_OK\".\nThe + task implied is likely getting the weather and timezone for Paris.\nThe outputs + are:\n1. Weather for Paris: {\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}\n2. + Timezone for Paris: {\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}\n\nThe + user instruction says: \"Use both function outputs and call no more tools. Reply + with exactly PARIS_MIXED_TOOLS_OK.\"\n\nI have the weather data and the timezone + data for Paris. I have used both function outputs. I am calling no more tools. + I just need to output the specific string requested.\n\nPlan:\n1. Confirm I + have the outputs.\n2. Confirm I am not calling any more tools.\n3. Output \"PARIS_MIXED_TOOLS_OK\".\n\nLet''s + double check the previous turns to ensure no context is missing.\nTurn 1: `tool_search` + found `get_weather` and `travel`.\nTurn 2: `get_weather` returned weather for + Paris.\nTurn 3: `travel` namespace member `get_timezone` (named `agentic_ns__travel__get_timezone` + in the execution) returned timezone for Paris.\nCurrent Turn: User asks to use + outputs and reply with string.\n\nThe request is straightforward. I will output + the string.\n","type":"response.reasoning_text.done"}' + - '{"content_index":0,"item_id":"8cbfc23565c7cd52","output_index":0,"part":{"text":"The + user wants me to verify if the tool outputs provided are sufficient to complete + a task and then reply with a specific string \"PARIS_MIXED_TOOLS_OK\".\nThe + task implied is likely getting the weather and timezone for Paris.\nThe outputs + are:\n1. Weather for Paris: {\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}\n2. + Timezone for Paris: {\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}\n\nThe + user instruction says: \"Use both function outputs and call no more tools. Reply + with exactly PARIS_MIXED_TOOLS_OK.\"\n\nI have the weather data and the timezone + data for Paris. I have used both function outputs. I am calling no more tools. + I just need to output the specific string requested.\n\nPlan:\n1. Confirm I + have the outputs.\n2. Confirm I am not calling any more tools.\n3. Output \"PARIS_MIXED_TOOLS_OK\".\n\nLet''s + double check the previous turns to ensure no context is missing.\nTurn 1: `tool_search` + found `get_weather` and `travel`.\nTurn 2: `get_weather` returned weather for + Paris.\nTurn 3: `travel` namespace member `get_timezone` (named `agentic_ns__travel__get_timezone` + in the execution) returned timezone for Paris.\nCurrent Turn: User asks to use + outputs and reply with string.\n\nThe request is straightforward. I will output + the string.\n","type":"reasoning_text"},"sequence_number":141,"type":"response.reasoning_part.done"}' + - '{"item":{"content":[{"text":"The user wants me to verify if the tool outputs + provided are sufficient to complete a task and then reply with a specific string + \"PARIS_MIXED_TOOLS_OK\".\nThe task implied is likely getting the weather and + timezone for Paris.\nThe outputs are:\n1. Weather for Paris: {\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}\n2. + Timezone for Paris: {\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}\n\nThe + user instruction says: \"Use both function outputs and call no more tools. Reply + with exactly PARIS_MIXED_TOOLS_OK.\"\n\nI have the weather data and the timezone + data for Paris. I have used both function outputs. I am calling no more tools. + I just need to output the specific string requested.\n\nPlan:\n1. Confirm I + have the outputs.\n2. Confirm I am not calling any more tools.\n3. Output \"PARIS_MIXED_TOOLS_OK\".\n\nLet''s + double check the previous turns to ensure no context is missing.\nTurn 1: `tool_search` + found `get_weather` and `travel`.\nTurn 2: `get_weather` returned weather for + Paris.\nTurn 3: `travel` namespace member `get_timezone` (named `agentic_ns__travel__get_timezone` + in the execution) returned timezone for Paris.\nCurrent Turn: User asks to use + outputs and reply with string.\n\nThe request is straightforward. I will output + the string.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"8cbfc23565c7cd52","status":"completed","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":142,"type":"response.output_item.done"}' + - '{"item":{"content":[],"id":"aba0b5f455081834","phase":null,"role":"assistant","status":"in_progress","type":"message"},"output_index":1,"sequence_number":143,"type":"response.output_item.added"}' + - '{"content_index":0,"item_id":"aba0b5f455081834","output_index":1,"part":{"annotations":[],"logprobs":[],"text":"","type":"output_text"},"sequence_number":144,"type":"response.content_part.added"}' + - '{"content_index":0,"delta":"\n\nPARIS","item_id":"aba0b5f455081834","logprobs":[],"output_index":1,"sequence_number":145,"type":"response.output_text.delta"}' + - '{"content_index":0,"delta":"_MIXED_TO","item_id":"aba0b5f455081834","logprobs":[],"output_index":1,"sequence_number":146,"type":"response.output_text.delta"}' + - '{"content_index":0,"delta":"OLS_OK","item_id":"aba0b5f455081834","logprobs":[],"output_index":1,"sequence_number":147,"type":"response.output_text.delta"}' + - '{"content_index":0,"item_id":"aba0b5f455081834","logprobs":[],"output_index":1,"sequence_number":148,"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"response.output_text.done"}' + - '{"content_index":0,"item_id":"aba0b5f455081834","output_index":1,"part":{"annotations":[],"logprobs":null,"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text"},"sequence_number":149,"type":"response.content_part.done"}' + - '{"item":{"content":[{"annotations":[],"logprobs":null,"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text"}],"id":"aba0b5f455081834","phase":null,"role":"assistant","status":"completed","summary":[],"type":"message"},"output_index":1,"sequence_number":150,"type":"response.output_item.done"}' + - '{"response":{"conversation_id":null,"created_at":1787715717,"error":null,"id":"resp_01a03c29-014a-79f2-a0dd-3ccd8292e36f","incomplete_details":null,"instructions":null,"model":"Qwen/Qwen3.6-35B-A3B-FP8","object":"response","output":[{"content":[{"text":"The + user wants me to verify if the tool outputs provided are sufficient to complete + a task and then reply with a specific string \"PARIS_MIXED_TOOLS_OK\".\nThe + task implied is likely getting the weather and timezone for Paris.\nThe outputs + are:\n1. Weather for Paris: {\"city\":\"Paris\",\"condition\":\"clear\",\"temperature_c\":21}\n2. + Timezone for Paris: {\"city\":\"Paris\",\"iana_timezone\":\"Europe/Paris\"}\n\nThe + user instruction says: \"Use both function outputs and call no more tools. Reply + with exactly PARIS_MIXED_TOOLS_OK.\"\n\nI have the weather data and the timezone + data for Paris. I have used both function outputs. I am calling no more tools. + I just need to output the specific string requested.\n\nPlan:\n1. Confirm I + have the outputs.\n2. Confirm I am not calling any more tools.\n3. Output \"PARIS_MIXED_TOOLS_OK\".\n\nLet''s + double check the previous turns to ensure no context is missing.\nTurn 1: `tool_search` + found `get_weather` and `travel`.\nTurn 2: `get_weather` returned weather for + Paris.\nTurn 3: `travel` namespace member `get_timezone` (named `agentic_ns__travel__get_timezone` + in the execution) returned timezone for Paris.\nCurrent Turn: User asks to use + outputs and reply with string.\n\nThe request is straightforward. I will output + the string.\n","type":"reasoning_text"}],"encrypted_content":null,"id":"8cbfc23565c7cd52","status":null,"summary":[],"type":"reasoning"},{"content":[{"annotations":[],"text":"\n\nPARIS_MIXED_TOOLS_OK","type":"output_text"}],"id":"aba0b5f455081834","role":"assistant","status":"completed","type":"message"}],"previous_response_id":"resp_01a03c28-effe-7a91-b578-e382c29bdd22","status":"completed","tool_choice":"none","tools":[{"description":"Get + the current weather for a city","name":"get_weather","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"},{"description":"Travel + location tools","name":"travel","tools":[{"description":"Get the IANA time zone + for a city","name":"get_timezone","parameters":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"},"strict":true,"type":"function"}],"type":"namespace"}],"usage":{"input_tokens":346,"input_tokens_details":{"cached_tokens":0},"output_tokens":314,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":660}},"sequence_number":151,"type":"response.completed"}' diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-openai-reference-gpt-5.6-nonstreaming.yaml b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-openai-reference-gpt-5.6-nonstreaming.yaml new file mode 100644 index 00000000..c2ca5676 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-openai-reference-gpt-5.6-nonstreaming.yaml @@ -0,0 +1,741 @@ +turns: +- filename: t1 + request: + body: + input: First call tool_search exactly once to find both a current-weather function + and a travel time-zone function. Do not call any other tool yet. + max_output_tokens: 4096 + model: gpt-5.6 + parallel_tool_calls: false + store: true + stream: false + tool_choice: required + tools: + - description: Search the client tool catalog for tools that can satisfy the + request. + execution: client + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + type: tool_search + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get the exchange rate between two currencies + name: get_exchange_rate + parameters: + additionalProperties: false + properties: + base: + type: string + quote: + type: string + required: + - base + - quote + type: object + strict: true + type: function + - defer_loading: true + description: Search for hotels in a city + name: search_hotels + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get latitude and longitude for a city + name: get_coordinates + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Calculate the distance between two cities + name: calculate_distance + parameters: + additionalProperties: false + properties: + destination: + type: string + origin: + type: string + required: + - origin + - destination + type: object + strict: true + type: function + type: namespace + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + background: false + billing: + payer: developer + completed_at: 1787714186 + created_at: 1787714184 + error: null + frequency_penalty: 0.0 + id: resp_0d6efd6f8bc085ed006a8e5a883b9887d0b3b9ac1317ca2ba3 + incomplete_details: null + instructions: null + max_output_tokens: 4096 + max_tool_calls: null + metadata: {} + model: gpt-5.6-sol + moderation: null + object: response + output: + - arguments: + query: Find both (1) a function for current weather conditions by location + and (2) a travel time-zone function that can determine the destination/local + time zone (or time-zone difference) for travel. + call_id: call_kwlKLORMuHQL8SfNf00ewnkh + execution: client + id: tsc_0d6efd6f8bc085ed006a8e5a895ba487d0b3646b3c7b067034 + status: completed + type: tool_search_call + parallel_tool_calls: false + presence_penalty: 0.0 + previous_response_id: null + prompt_cache_key: null + prompt_cache_retention: 24h + reasoning: + context: all_turns + effort: medium + mode: standard + summary: null + safety_identifier: null + service_tier: default + status: completed + store: true + temperature: 1.0 + text: + format: + type: text + verbosity: medium + tool_choice: required + tool_usage: + image_gen: + input_tokens: 0 + input_tokens_details: + image_tokens: 0 + text_tokens: 0 + output_tokens: 0 + output_tokens_details: + image_tokens: 0 + text_tokens: 0 + total_tokens: 0 + web_search: + num_requests: 0 + tools: + - defer_loading: true + description: Get the current weather for a city + name: get_weather + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get the exchange rate between two currencies + name: get_exchange_rate + output_schema: null + parameters: + additionalProperties: false + properties: + base: + type: string + quote: + type: string + required: + - base + - quote + type: object + strict: true + type: function + - defer_loading: true + description: Search for hotels in a city + name: search_hotels + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Search the client tool catalog for tools that can satisfy the + request. + execution: client + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + type: tool_search + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get latitude and longitude for a city + name: get_coordinates + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Calculate the distance between two cities + name: calculate_distance + output_schema: null + parameters: + additionalProperties: false + properties: + destination: + type: string + origin: + type: string + required: + - origin + - destination + type: object + strict: true + type: function + type: namespace + top_logprobs: 0 + top_p: 0.98 + truncation: disabled + usage: + input_tokens: 166 + input_tokens_details: + cache_write_tokens: 0 + cached_tokens: 0 + output_tokens: 58 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 224 + user: null + headers: + content-type: application/json + status_code: 200 +- filename: t2 + request: + body: + input: + - call_id: call_kwlKLORMuHQL8SfNf00ewnkh + execution: client + status: completed + tools: + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + type: tool_search_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + max_output_tokens: 4096 + model: gpt-5.6 + parallel_tool_calls: false + previous_response_id: resp_0d6efd6f8bc085ed006a8e5a883b9887d0b3b9ac1317ca2ba3 + store: true + stream: false + tool_choice: + name: get_weather + type: function + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + background: false + billing: + payer: developer + completed_at: 1787714188 + created_at: 1787714187 + error: null + frequency_penalty: 0.0 + id: resp_0d6efd6f8bc085ed006a8e5a8b15ec87d0a35c204381e56962 + incomplete_details: null + instructions: null + max_output_tokens: 4096 + max_tool_calls: null + metadata: {} + model: gpt-5.6-sol + moderation: null + object: response + output: + - arguments: '{"city":"Paris"}' + call_id: call_xmP2yczPU7r7TSwBySUnjvDH + id: fc_0d6efd6f8bc085ed006a8e5a8be96c87d0aa5e12cee1328c7c + name: get_weather + status: completed + type: function_call + parallel_tool_calls: false + presence_penalty: 0.0 + previous_response_id: resp_0d6efd6f8bc085ed006a8e5a883b9887d0b3b9ac1317ca2ba3 + prompt_cache_key: null + prompt_cache_retention: 24h + reasoning: + context: all_turns + effort: medium + mode: standard + summary: null + safety_identifier: null + service_tier: default + status: completed + store: true + temperature: 1.0 + text: + format: + type: text + verbosity: medium + tool_choice: + name: get_weather + type: function + tool_usage: + image_gen: + input_tokens: 0 + input_tokens_details: + image_tokens: 0 + text_tokens: 0 + output_tokens: 0 + output_tokens_details: + image_tokens: 0 + text_tokens: 0 + total_tokens: 0 + web_search: + num_requests: 0 + tools: + - description: Get the current weather for a city + name: get_weather + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - description: Get the IANA time zone for a city + name: get_timezone + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + top_logprobs: 0 + top_p: 0.98 + truncation: disabled + usage: + input_tokens: 307 + input_tokens_details: + cache_write_tokens: 0 + cached_tokens: 0 + output_tokens: 18 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 325 + user: null + headers: + content-type: application/json + status_code: 200 +- filename: t3 + request: + body: + input: + - call_id: call_xmP2yczPU7r7TSwBySUnjvDH + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + max_output_tokens: 4096 + model: gpt-5.6 + parallel_tool_calls: false + previous_response_id: resp_0d6efd6f8bc085ed006a8e5a8b15ec87d0a35c204381e56962 + store: true + stream: false + tool_choice: auto + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + background: false + billing: + payer: developer + completed_at: 1787714190 + created_at: 1787714189 + error: null + frequency_penalty: 0.0 + id: resp_0d6efd6f8bc085ed006a8e5a8d5e1487d08cf945156d59472b + incomplete_details: null + instructions: null + max_output_tokens: 4096 + max_tool_calls: null + metadata: {} + model: gpt-5.6-sol + moderation: null + object: response + output: + - arguments: '{"city":"Paris"}' + call_id: call_UCdhYlVJCK8uWaIKXHKcvdJd + id: fc_0d6efd6f8bc085ed006a8e5a8e9a7087d084360ac253a173c8 + name: get_timezone + namespace: travel + status: completed + type: function_call + parallel_tool_calls: false + presence_penalty: 0.0 + previous_response_id: resp_0d6efd6f8bc085ed006a8e5a8b15ec87d0a35c204381e56962 + prompt_cache_key: null + prompt_cache_retention: 24h + reasoning: + context: all_turns + effort: medium + mode: standard + summary: null + safety_identifier: null + service_tier: default + status: completed + store: true + temperature: 1.0 + text: + format: + type: text + verbosity: medium + tool_choice: auto + tool_usage: + image_gen: + input_tokens: 0 + input_tokens_details: + image_tokens: 0 + text_tokens: 0 + output_tokens: 0 + output_tokens_details: + image_tokens: 0 + text_tokens: 0 + total_tokens: 0 + web_search: + num_requests: 0 + tools: + - description: Get the current weather for a city + name: get_weather + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - description: Get the IANA time zone for a city + name: get_timezone + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + top_logprobs: 0 + top_p: 0.98 + truncation: disabled + usage: + input_tokens: 379 + input_tokens_details: + cache_write_tokens: 0 + cached_tokens: 0 + output_tokens: 18 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 397 + user: null + headers: + content-type: application/json + status_code: 200 +- filename: t4 + request: + body: + input: + - call_id: call_UCdhYlVJCK8uWaIKXHKcvdJd + output: '{"city":"Paris","iana_timezone":"Europe/Paris"}' + type: function_call_output + - content: Use both function outputs and call no more tools. Reply with exactly + PARIS_MIXED_TOOLS_OK. + role: user + type: message + max_output_tokens: 4096 + model: gpt-5.6 + parallel_tool_calls: false + previous_response_id: resp_0d6efd6f8bc085ed006a8e5a8d5e1487d08cf945156d59472b + store: true + stream: false + tool_choice: none + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + body: + background: false + billing: + payer: developer + completed_at: 1787714192 + created_at: 1787714191 + error: null + frequency_penalty: 0.0 + id: resp_0d6efd6f8bc085ed006a8e5a8f60e887d0a05c2bb86dfd4998 + incomplete_details: null + instructions: null + max_output_tokens: 4096 + max_tool_calls: null + metadata: {} + model: gpt-5.6-sol + moderation: null + object: response + output: + - content: + - annotations: [] + logprobs: [] + text: PARIS_MIXED_TOOLS_OK + type: output_text + id: msg_0d6efd6f8bc085ed006a8e5a9036f087d0be191c44dbfdddee + phase: final_answer + role: assistant + status: completed + type: message + parallel_tool_calls: false + presence_penalty: 0.0 + previous_response_id: resp_0d6efd6f8bc085ed006a8e5a8d5e1487d08cf945156d59472b + prompt_cache_key: null + prompt_cache_retention: 24h + reasoning: + context: all_turns + effort: medium + mode: standard + summary: null + safety_identifier: null + service_tier: default + status: completed + store: true + temperature: 1.0 + text: + format: + type: text + verbosity: medium + tool_choice: none + tool_usage: + image_gen: + input_tokens: 0 + input_tokens_details: + image_tokens: 0 + text_tokens: 0 + output_tokens: 0 + output_tokens_details: + image_tokens: 0 + text_tokens: 0 + total_tokens: 0 + web_search: + num_requests: 0 + tools: + - description: Get the current weather for a city + name: get_weather + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - description: Get the IANA time zone for a city + name: get_timezone + output_schema: null + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + top_logprobs: 0 + top_p: 0.98 + truncation: disabled + usage: + input_tokens: 446 + input_tokens_details: + cache_write_tokens: 0 + cached_tokens: 0 + output_tokens: 12 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 458 + user: null + headers: + content-type: application/json + status_code: 200 diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-openai-reference-gpt-5.6-streaming.yaml b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-openai-reference-gpt-5.6-streaming.yaml new file mode 100644 index 00000000..24a3df71 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/tool-search-openai-reference-gpt-5.6-streaming.yaml @@ -0,0 +1,701 @@ +turns: +- filename: t1 + request: + body: + input: First call tool_search exactly once to find both a current-weather function + and a travel time-zone function. Do not call any other tool yet. + max_output_tokens: 4096 + model: gpt-5.6 + parallel_tool_calls: false + store: true + stream: true + tool_choice: required + tools: + - description: Search the client tool catalog for tools that can satisfy the + request. + execution: client + parameters: + additionalProperties: false + properties: + query: + description: A concise description of the needed capabilities. + type: string + required: + - query + type: object + type: tool_search + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get the exchange rate between two currencies + name: get_exchange_rate + parameters: + additionalProperties: false + properties: + base: + type: string + quote: + type: string + required: + - base + - quote + type: object + strict: true + type: function + - defer_loading: true + description: Search for hotels in a city + name: search_hotels + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Get latitude and longitude for a city + name: get_coordinates + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - defer_loading: true + description: Calculate the distance between two cities + name: calculate_distance + parameters: + additionalProperties: false + properties: + destination: + type: string + origin: + type: string + required: + - origin + - destination + type: object + strict: true + type: function + type: namespace + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"type":"response.created","response":{"id":"resp_09b6d06342202e77006a8e5a92598887d08852a79bcc7fc981","object":"response","created_at":1787714194,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"required","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","output_schema":null,"parameters":{"type":"object","properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"tool_search","description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false}},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","output_schema":null,"parameters":{"type":"object","properties":{"origin":{"type":"string"},"destination":{"type":"string"}},"required":["origin","destination"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"type":"response.in_progress","response":{"id":"resp_09b6d06342202e77006a8e5a92598887d08852a79bcc7fc981","object":"response","created_at":1787714194,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"required","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","output_schema":null,"parameters":{"type":"object","properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"tool_search","description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false}},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","output_schema":null,"parameters":{"type":"object","properties":{"origin":{"type":"string"},"destination":{"type":"string"}},"required":["origin","destination"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","item":{"id":"tsc_09b6d06342202e77006a8e5a94076087d0b8b974aa62486abc","type":"tool_search_call","status":"in_progress","arguments":{},"call_id":"call_3SyAHfZ8PhLjASVy8iEFa2AQ","execution":"client"},"output_index":0,"sequence_number":2} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","item":{"id":"tsc_09b6d06342202e77006a8e5a94076087d0b8b974aa62486abc","type":"tool_search_call","status":"completed","arguments":{"query":"Find + both (1) a function that returns current weather for a location and (2) a travel + time-zone function that determines a destination''s time zone or local time + for travel planning."},"call_id":"call_3SyAHfZ8PhLjASVy8iEFa2AQ","execution":"client"},"output_index":0,"sequence_number":3} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"type":"response.completed","response":{"id":"resp_09b6d06342202e77006a8e5a92598887d08852a79bcc7fc981","object":"response","created_at":1787714194,"status":"completed","background":false,"completed_at":1787714196,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[{"id":"tsc_09b6d06342202e77006a8e5a94076087d0b8b974aa62486abc","type":"tool_search_call","status":"completed","arguments":{"query":"Find + both (1) a function that returns current weather for a location and (2) a travel + time-zone function that determines a destination''s time zone or local time + for travel planning."},"call_id":"call_3SyAHfZ8PhLjASVy8iEFa2AQ","execution":"client"}],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"required","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","defer_loading":true,"description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Get + the exchange rate between two currencies","name":"get_exchange_rate","output_schema":null,"parameters":{"type":"object","properties":{"base":{"type":"string"},"quote":{"type":"string"}},"required":["base","quote"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Search + for hotels in a city","name":"search_hotels","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"tool_search","description":"Search + the client tool catalog for tools that can satisfy the request.","execution":"client","parameters":{"type":"object","properties":{"query":{"type":"string","description":"A + concise description of the needed capabilities."}},"required":["query"],"additionalProperties":false}},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","defer_loading":true,"description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Get + latitude and longitude for a city","name":"get_coordinates","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"function","defer_loading":true,"description":"Calculate + the distance between two cities","name":"calculate_distance","output_schema":null,"parameters":{"type":"object","properties":{"origin":{"type":"string"},"destination":{"type":"string"}},"required":["origin","destination"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":{"input_tokens":166,"input_tokens_details":{"cache_write_tokens":0,"cached_tokens":0},"output_tokens":56,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":222},"user":null,"metadata":{}},"sequence_number":4} + + ' + - ' + + ' + status_code: 200 +- filename: t2 + request: + body: + input: + - call_id: call_3SyAHfZ8PhLjASVy8iEFa2AQ + execution: client + status: completed + tools: + - defer_loading: true + description: Get the current weather for a city + name: get_weather + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + - description: Travel location tools + name: travel + tools: + - defer_loading: true + description: Get the IANA time zone for a city + name: get_timezone + parameters: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + strict: true + type: function + type: namespace + type: tool_search_output + - content: Now call get_weather exactly once with {"city":"Paris"}. Do not call + any other tool. + role: user + type: message + max_output_tokens: 4096 + model: gpt-5.6 + parallel_tool_calls: false + previous_response_id: resp_09b6d06342202e77006a8e5a92598887d08852a79bcc7fc981 + store: true + stream: true + tool_choice: + name: get_weather + type: function + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"type":"response.created","response":{"id":"resp_09b6d06342202e77006a8e5a95861887d0bd00969c33f529ca","object":"response","created_at":1787714197,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_09b6d06342202e77006a8e5a92598887d08852a79bcc7fc981","prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":{"type":"function","name":"get_weather"},"tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"type":"response.in_progress","response":{"id":"resp_09b6d06342202e77006a8e5a95861887d0bd00969c33f529ca","object":"response","created_at":1787714197,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_09b6d06342202e77006a8e5a92598887d08852a79bcc7fc981","prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":{"type":"function","name":"get_weather"},"tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","item":{"id":"fc_09b6d06342202e77006a8e5a9687c087d08411dc30df8f3051","type":"function_call","status":"in_progress","arguments":"","call_id":"call_cUesiZFNF0gLgnw5j85LzWC0","name":"get_weather"},"output_index":0,"sequence_number":2} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"{\"","item_id":"fc_09b6d06342202e77006a8e5a9687c087d08411dc30df8f3051","obfuscation":"5rVjKITNgMDH4z","output_index":0,"sequence_number":3} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"city","item_id":"fc_09b6d06342202e77006a8e5a9687c087d08411dc30df8f3051","obfuscation":"oJCzUK8Gkyt6","output_index":0,"sequence_number":4} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"\":\"","item_id":"fc_09b6d06342202e77006a8e5a9687c087d08411dc30df8f3051","obfuscation":"U1Zq3q7lNKvLd","output_index":0,"sequence_number":5} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"Paris","item_id":"fc_09b6d06342202e77006a8e5a9687c087d08411dc30df8f3051","obfuscation":"reFrhFQOt1s","output_index":0,"sequence_number":6} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"\"}","item_id":"fc_09b6d06342202e77006a8e5a9687c087d08411dc30df8f3051","obfuscation":"Bdyq0fhBBSjZz4","output_index":0,"sequence_number":7} + + ' + - ' + + ' + - 'event: response.function_call_arguments.done + + ' + - 'data: {"type":"response.function_call_arguments.done","arguments":"{\"city\":\"Paris\"}","item_id":"fc_09b6d06342202e77006a8e5a9687c087d08411dc30df8f3051","output_index":0,"sequence_number":8} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","item":{"id":"fc_09b6d06342202e77006a8e5a9687c087d08411dc30df8f3051","type":"function_call","status":"completed","arguments":"{\"city\":\"Paris\"}","call_id":"call_cUesiZFNF0gLgnw5j85LzWC0","name":"get_weather"},"output_index":0,"sequence_number":9} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"type":"response.completed","response":{"id":"resp_09b6d06342202e77006a8e5a95861887d0bd00969c33f529ca","object":"response","created_at":1787714197,"status":"completed","background":false,"completed_at":1787714198,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[{"id":"fc_09b6d06342202e77006a8e5a9687c087d08411dc30df8f3051","type":"function_call","status":"completed","arguments":"{\"city\":\"Paris\"}","call_id":"call_cUesiZFNF0gLgnw5j85LzWC0","name":"get_weather"}],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_09b6d06342202e77006a8e5a92598887d08852a79bcc7fc981","prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":{"type":"function","name":"get_weather"},"tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":{"input_tokens":305,"input_tokens_details":{"cache_write_tokens":0,"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":323},"user":null,"metadata":{}},"sequence_number":10} + + ' + - ' + + ' + status_code: 200 +- filename: t3 + request: + body: + input: + - call_id: call_cUesiZFNF0gLgnw5j85LzWC0 + output: '{"city":"Paris","condition":"clear","temperature_c":21}' + type: function_call_output + - content: Now call the loaded travel namespace member get_timezone exactly + once with {"city":"Paris"}. Do not call any other tool. + role: user + type: message + max_output_tokens: 4096 + model: gpt-5.6 + parallel_tool_calls: false + previous_response_id: resp_09b6d06342202e77006a8e5a95861887d0bd00969c33f529ca + store: true + stream: true + tool_choice: auto + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"type":"response.created","response":{"id":"resp_09b6d06342202e77006a8e5a97688887d089b8d9eed0314f80","object":"response","created_at":1787714199,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_09b6d06342202e77006a8e5a95861887d0bd00969c33f529ca","prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"type":"response.in_progress","response":{"id":"resp_09b6d06342202e77006a8e5a97688887d089b8d9eed0314f80","object":"response","created_at":1787714199,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_09b6d06342202e77006a8e5a95861887d0bd00969c33f529ca","prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","item":{"id":"fc_09b6d06342202e77006a8e5a98f23087d09eeb5d379d9284cd","type":"function_call","status":"in_progress","arguments":"","call_id":"call_gP1EXR0KtNa0MCfzjh5fCsZQ","name":"get_timezone","namespace":"travel"},"output_index":0,"sequence_number":2} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"{\"","item_id":"fc_09b6d06342202e77006a8e5a98f23087d09eeb5d379d9284cd","obfuscation":"UtCM30Dyqigyxv","output_index":0,"sequence_number":3} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"city","item_id":"fc_09b6d06342202e77006a8e5a98f23087d09eeb5d379d9284cd","obfuscation":"xXypsqWj1DjX","output_index":0,"sequence_number":4} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"\":\"","item_id":"fc_09b6d06342202e77006a8e5a98f23087d09eeb5d379d9284cd","obfuscation":"losPggMyhFmvT","output_index":0,"sequence_number":5} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"Paris","item_id":"fc_09b6d06342202e77006a8e5a98f23087d09eeb5d379d9284cd","obfuscation":"iS39SPucwq2","output_index":0,"sequence_number":6} + + ' + - ' + + ' + - 'event: response.function_call_arguments.delta + + ' + - 'data: {"type":"response.function_call_arguments.delta","delta":"\"}","item_id":"fc_09b6d06342202e77006a8e5a98f23087d09eeb5d379d9284cd","obfuscation":"5pUHuZkUbpKdXV","output_index":0,"sequence_number":7} + + ' + - ' + + ' + - 'event: response.function_call_arguments.done + + ' + - 'data: {"type":"response.function_call_arguments.done","arguments":"{\"city\":\"Paris\"}","item_id":"fc_09b6d06342202e77006a8e5a98f23087d09eeb5d379d9284cd","output_index":0,"sequence_number":8} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","item":{"id":"fc_09b6d06342202e77006a8e5a98f23087d09eeb5d379d9284cd","type":"function_call","status":"completed","arguments":"{\"city\":\"Paris\"}","call_id":"call_gP1EXR0KtNa0MCfzjh5fCsZQ","name":"get_timezone","namespace":"travel"},"output_index":0,"sequence_number":9} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"type":"response.completed","response":{"id":"resp_09b6d06342202e77006a8e5a97688887d089b8d9eed0314f80","object":"response","created_at":1787714199,"status":"completed","background":false,"completed_at":1787714201,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[{"id":"fc_09b6d06342202e77006a8e5a98f23087d09eeb5d379d9284cd","type":"function_call","status":"completed","arguments":"{\"city\":\"Paris\"}","call_id":"call_gP1EXR0KtNa0MCfzjh5fCsZQ","name":"get_timezone","namespace":"travel"}],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_09b6d06342202e77006a8e5a95861887d0bd00969c33f529ca","prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":{"input_tokens":377,"input_tokens_details":{"cache_write_tokens":0,"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":395},"user":null,"metadata":{}},"sequence_number":10} + + ' + - ' + + ' + status_code: 200 +- filename: t4 + request: + body: + input: + - call_id: call_gP1EXR0KtNa0MCfzjh5fCsZQ + output: '{"city":"Paris","iana_timezone":"Europe/Paris"}' + type: function_call_output + - content: Use both function outputs and call no more tools. Reply with exactly + PARIS_MIXED_TOOLS_OK. + role: user + type: message + max_output_tokens: 4096 + model: gpt-5.6 + parallel_tool_calls: false + previous_response_id: resp_09b6d06342202e77006a8e5a97688887d089b8d9eed0314f80 + store: true + stream: true + tool_choice: none + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + user-agent: python-httpx/0.28.1 + method: POST + path: /v1/responses + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: response.created + + ' + - 'data: {"type":"response.created","response":{"id":"resp_09b6d06342202e77006a8e5a99cc8087d0a13c2695d1547e2b","object":"response","created_at":1787714201,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_09b6d06342202e77006a8e5a97688887d089b8d9eed0314f80","prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"none","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + ' + - ' + + ' + - 'event: response.in_progress + + ' + - 'data: {"type":"response.in_progress","response":{"id":"resp_09b6d06342202e77006a8e5a99cc8087d0a13c2695d1547e2b","object":"response","created_at":1787714201,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_09b6d06342202e77006a8e5a97688887d089b8d9eed0314f80","prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"none","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + ' + - ' + + ' + - 'event: response.output_item.added + + ' + - 'data: {"type":"response.output_item.added","item":{"id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","type":"message","status":"in_progress","content":[],"phase":"final_answer","role":"assistant"},"output_index":0,"sequence_number":2} + + ' + - ' + + ' + - 'event: response.content_part.added + + ' + - 'data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","content_index":0,"delta":"PAR","item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","logprobs":[],"obfuscation":"zeoN8KyyCd2VT","output_index":0,"sequence_number":4} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","content_index":0,"delta":"IS","item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","logprobs":[],"obfuscation":"KE1EZAjDnT4TDM","output_index":0,"sequence_number":5} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","content_index":0,"delta":"_M","item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","logprobs":[],"obfuscation":"nWDqsrSg1OcL63","output_index":0,"sequence_number":6} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","content_index":0,"delta":"IX","item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","logprobs":[],"obfuscation":"tAqnIifuPpWOQk","output_index":0,"sequence_number":7} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","content_index":0,"delta":"ED","item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","logprobs":[],"obfuscation":"dyYFy0raO7i52N","output_index":0,"sequence_number":8} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","content_index":0,"delta":"_TO","item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","logprobs":[],"obfuscation":"ROBuypXCR2xEk","output_index":0,"sequence_number":9} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","content_index":0,"delta":"OLS","item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","logprobs":[],"obfuscation":"NR7WSk37DNNsH","output_index":0,"sequence_number":10} + + ' + - ' + + ' + - 'event: response.output_text.delta + + ' + - 'data: {"type":"response.output_text.delta","content_index":0,"delta":"_OK","item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","logprobs":[],"obfuscation":"Yi7XWBauOo8f9","output_index":0,"sequence_number":11} + + ' + - ' + + ' + - 'event: response.output_text.done + + ' + - 'data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","logprobs":[],"output_index":0,"sequence_number":12,"text":"PARIS_MIXED_TOOLS_OK"} + + ' + - ' + + ' + - 'event: response.content_part.done + + ' + - 'data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"PARIS_MIXED_TOOLS_OK"},"sequence_number":13} + + ' + - ' + + ' + - 'event: response.output_item.done + + ' + - 'data: {"type":"response.output_item.done","item":{"id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"PARIS_MIXED_TOOLS_OK"}],"phase":"final_answer","role":"assistant"},"output_index":0,"sequence_number":14} + + ' + - ' + + ' + - 'event: response.completed + + ' + - 'data: {"type":"response.completed","response":{"id":"resp_09b6d06342202e77006a8e5a99cc8087d0a13c2695d1547e2b","object":"response","created_at":1787714201,"status":"completed","background":false,"completed_at":1787714202,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":4096,"max_tool_calls":null,"model":"gpt-5.6-sol","moderation":null,"output":[{"id":"msg_09b6d06342202e77006a8e5a9a6f8087d0836ff8285bd57d7c","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"PARIS_MIXED_TOOLS_OK"}],"phase":"final_answer","role":"assistant"}],"parallel_tool_calls":false,"presence_penalty":0.0,"previous_response_id":"resp_09b6d06342202e77006a8e5a97688887d089b8d9eed0314f80","prompt_cache_key":null,"prompt_cache_retention":"24h","reasoning":{"context":"all_turns","effort":"medium","mode":"standard","summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"none","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[{"type":"function","description":"Get + the current weather for a city","name":"get_weather","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true},{"type":"namespace","description":"Travel + location tools","name":"travel","tools":[{"type":"function","description":"Get + the IANA time zone for a city","name":"get_timezone","output_schema":null,"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],"additionalProperties":false},"strict":true}]}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":{"input_tokens":444,"input_tokens_details":{"cache_write_tokens":0,"cached_tokens":0},"output_tokens":12,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":456},"user":null,"metadata":{}},"sequence_number":15} + + ' + - ' + + ' + status_code: 200 diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/vllm_initial_tools.json b/crates/agentic-server-core/tests/cassettes/tool_search/vllm_initial_tools.json new file mode 100644 index 00000000..c0c97683 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/vllm_initial_tools.json @@ -0,0 +1,19 @@ +[ + { + "type": "function", + "name": "tool_search", + "description": "Search the client tool catalog for tools that can satisfy the request. Available catalog entries: get_weather — Get the current weather for a city; get_exchange_rate — Get the exchange rate between two currencies; search_hotels — Search for hotels in a city; travel — Travel location tools.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "A concise description of the needed capabilities." + } + }, + "required": ["query"], + "additionalProperties": false + }, + "strict": true + } +] diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/vllm_tool_choice_sequence.json b/crates/agentic-server-core/tests/cassettes/tool_search/vllm_tool_choice_sequence.json new file mode 100644 index 00000000..89ba6c8b --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/vllm_tool_choice_sequence.json @@ -0,0 +1,15 @@ +[ + { + "type": "function", + "name": "tool_search" + }, + { + "type": "function", + "name": "get_weather" + }, + { + "type": "function", + "name": "agentic_ns__travel__get_timezone" + }, + "none" +] diff --git a/crates/agentic-server-core/tests/cassettes/tool_search/vllm_tools_after_search.json b/crates/agentic-server-core/tests/cassettes/tool_search/vllm_tools_after_search.json new file mode 100644 index 00000000..fe4a989a --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/tool_search/vllm_tools_after_search.json @@ -0,0 +1,51 @@ +[ + { + "type": "function", + "name": "tool_search", + "description": "Search the client tool catalog for tools that can satisfy the request. Available catalog entries: get_exchange_rate — Get the exchange rate between two currencies; search_hotels — Search for hotels in a city; travel — Travel location tools.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "A concise description of the needed capabilities." + } + }, + "required": ["query"], + "additionalProperties": false + }, + "strict": true + }, + { + "type": "function", + "name": "get_weather", + "description": "Get the current weather for a city", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "additionalProperties": false + }, + "strict": true + }, + { + "type": "function", + "name": "agentic_ns__travel__get_timezone", + "description": "Get the IANA time zone for a city", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "additionalProperties": false + }, + "strict": true + } +] diff --git a/crates/agentic-server-core/tests/compaction_cassette_test.rs b/crates/agentic-server-core/tests/compaction_cassette_test.rs index 774eec75..1642f11c 100644 --- a/crates/agentic-server-core/tests/compaction_cassette_test.rs +++ b/crates/agentic-server-core/tests/compaction_cassette_test.rs @@ -2,10 +2,13 @@ mod support; use std::sync::Arc; -use agentic_core::executor::{compact_response, execute}; +use agentic_core::executor::{compact_response, create_conversation, execute}; use agentic_core::{CompactRequest, InputItem, RequestPayload}; use serde_json::{Value, json}; -use support::{MockResponse, TestFixture, load_cassette, output_text, unwrap_blocking}; +use support::{ + MockResponse, TestFixture, function_call_response, load_cassette, output_text, text_response, tool_search_request, + unwrap_blocking, +}; const DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/cassettes/compaction"); const COMPACTION_PROMPT: &str = "You are performing a CONTEXT CHECKPOINT COMPACTION. Create a concise handoff summary that preserves current progress, decisions, constraints, unresolved work, and critical references for the next model. Return only the summary."; @@ -89,6 +92,58 @@ fn response_request(input: &Value, context_management: Option<&Value>) -> Reques .expect("valid Responses request") } +fn tool_search_declarations() -> Value { + json!([ + { + "type": "tool_search", + "execution": "client", + "description": "Find a tool", + "parameters": {"type": "object"} + }, + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }, + { + "type": "function", + "name": "get_time", + "description": "Get time", + "parameters": {"type": "object"}, + "defer_loading": true + } + ]) +} + +fn completed_tool_search_history() -> Value { + json!([ + { + "type": "message", + "role": "user", + "content": "Find the weather tool" + }, + { + "type": "tool_search_call", + "id": "tsc_search", + "call_id": "call_search", + "arguments": {"query": "weather"} + }, + { + "type": "tool_search_output", + "call_id": "call_search", + "tools": [{ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }] + } + ]) +} + fn cassette(name: &str) -> support::Cassette { load_cassette(&format!("{DIR}/{name}")) } @@ -245,3 +300,157 @@ async fn automatic_compaction_replays_both_rounds_and_accumulates_usage() { .all(|request| request.get("context_management").is_none()) ); } + +#[tokio::test] +async fn tool_search_standalone_compaction_lowers_summary_history_and_preserves_loaded_state() { + let fixture = TestFixture::new_with_responses(vec![ + function_call_response("fc_search", "call_search", "tool_search", r#"{"query":"weather"}"#), + text_response("tool loaded"), + text_response("durable tool summary"), + function_call_response("fc_weather", "call_weather", "get_weather", r#"{"city":"Paris"}"#), + ]) + .await; + + let first = unwrap_blocking( + execute( + tool_search_request("find weather", Some(tool_search_declarations()), true, None, None), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("stored search call"), + ); + let second = unwrap_blocking( + execute( + tool_search_request( + json!([{ + "type": "tool_search_output", + "call_id": "call_search", + "tools": [tool_search_declarations()[1].clone()] + }]), + None, + true, + Some(first.id), + None, + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("persist loaded state"), + ); + let compact_request: CompactRequest = serde_json::from_value(json!({ + "model": "test-model", + "previous_response_id": second.id + })) + .expect("valid stored compaction request"); + let compacted = compact_response(compact_request, &fixture.exec_ctx, None) + .await + .expect("standalone tool-search compaction"); + + assert!( + compacted + .output + .iter() + .all(|item| !matches!(item, InputItem::ToolSearchCall(_) | InputItem::ToolSearchOutput(_))) + ); + let followup = unwrap_blocking( + execute( + tool_search_request("use the loaded weather tool", None, false, Some(compacted.id), None), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("compacted continuation"), + ); + assert_eq!( + serde_json::to_value(&followup.output[0]).unwrap()["name"], + "get_weather" + ); + + let requests = fixture.request_bodies().await; + assert_eq!(requests.len(), 4); + let summary_request = &requests[2]; + assert!( + summary_request["input"] + .as_array() + .unwrap() + .iter() + .any(|item| { item["type"] == "function_call" && item["name"] == "tool_search" }) + ); + assert!(!summary_request.to_string().contains("tool_search_call")); + assert!(!summary_request.to_string().contains("tool_search_output")); + let followup_tools = requests[3]["tools"].as_array().expect("compacted effective tools"); + assert!(followup_tools.iter().any(|tool| tool["name"] == "get_weather")); + assert!(!followup_tools.iter().any(|tool| tool["name"] == "get_time")); +} + +#[tokio::test] +async fn tool_search_automatic_compaction_restores_only_loaded_definition_on_continuation() { + let fixture = TestFixture::new_with_responses(vec![ + text_response("durable automatic tool summary"), + function_call_response("fc_weather", "call_weather", "get_weather", r#"{"city":"Paris"}"#), + text_response("automatic compaction complete"), + ]) + .await; + let conversation_id = create_conversation(&fixture.exec_ctx) + .await + .expect("create compacted conversation") + .conversation_id; + let mut request = tool_search_request( + completed_tool_search_history(), + Some(tool_search_declarations()), + true, + None, + Some(conversation_id.clone()), + ); + request.context_management = serde_json::from_value(json!([{ + "type": "compaction", + "compact_threshold": 1 + }])) + .expect("valid automatic compaction policy"); + + let first = unwrap_blocking( + execute(request, Arc::clone(&fixture.exec_ctx)) + .await + .expect("automatic tool-search compaction"), + ); + assert_eq!(serde_json::to_value(&first.output[0]).unwrap()["name"], "get_weather"); + let final_response = unwrap_blocking( + execute( + tool_search_request( + json!([{ + "type": "function_call_output", + "call_id": "call_weather", + "output": "sunny" + }]), + None, + true, + None, + Some(conversation_id), + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("continue automatically compacted state"), + ); + assert_eq!(output_text(&final_response), "automatic compaction complete"); + + let requests = fixture.request_bodies().await; + assert_eq!(requests.len(), 3); + assert!(!requests[0].to_string().contains("tool_search_call")); + assert!(!requests[0].to_string().contains("tool_search_output")); + assert!( + requests[0]["input"] + .as_array() + .unwrap() + .iter() + .any(|item| { item["type"] == "function_call" && item["name"] == "tool_search" }) + ); + assert!(requests[1]["input"].as_array().unwrap().iter().all(|item| { + !(item["type"] == "function_call" && item["name"] == "tool_search" + || item["type"] == "function_call_output" && item["call_id"] == "call_search") + })); + for request in &requests[1..] { + let tools = request["tools"].as_array().expect("effective private tools"); + assert!(tools.iter().any(|tool| tool["name"] == "get_weather")); + assert!(!tools.iter().any(|tool| tool["name"] == "get_time")); + } +} diff --git a/crates/agentic-server-core/tests/stateful_conversation_integration.rs b/crates/agentic-server-core/tests/stateful_conversation_integration.rs index a6174eb5..4b6c4184 100644 --- a/crates/agentic-server-core/tests/stateful_conversation_integration.rs +++ b/crates/agentic-server-core/tests/stateful_conversation_integration.rs @@ -7,10 +7,12 @@ mod support; use agentic_core::executor::{create_conversation, execute}; +use serde_json::json; use std::sync::Arc; use support::{ - TestFixture, collect_stream, expected_text, load_cassette, make_request, output_text, request_input_texts, - responses_turns, unwrap_blocking, + TestFixture, collect_stream, expected_text, function_call_response, load_cassette, make_request, output_text, + request_input_texts, responses_turns, text_response, tool_search_function_declarations, tool_search_output, + tool_search_request, unwrap_blocking, }; const DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/cassettes/text_only/conversation"); @@ -56,6 +58,204 @@ async fn test_two_turn_nonstreaming_conversation() { assert_eq!(output_text(&p2), expected_text(t2)); } +#[tokio::test] +async fn tool_search_conversation_continuation_rebuilds_loaded_tools_from_history() { + let fixture = TestFixture::new_with_responses(vec![ + function_call_response("fc_search", "call_search", "tool_search", r#"{"query":"weather"}"#), + function_call_response("fc_weather", "call_weather", "get_weather", r#"{"city":"Paris"}"#), + text_response("conversation weather complete"), + ]) + .await; + let conversation_id = create_conversation(&fixture.exec_ctx) + .await + .expect("create conversation") + .conversation_id; + let declarations = tool_search_function_declarations("get_weather", "Get weather"); + + let first = unwrap_blocking( + execute( + tool_search_request( + "find weather", + Some(declarations), + true, + None, + Some(conversation_id.clone()), + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("conversation search turn"), + ); + assert_eq!( + serde_json::to_value(&first.output[0]).unwrap()["type"], + "tool_search_call" + ); + + let second = unwrap_blocking( + execute( + tool_search_request( + json!([tool_search_output("call_search", "get_weather", "Get weather")]), + None, + true, + None, + Some(conversation_id.clone()), + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("conversation search output"), + ); + assert_eq!(serde_json::to_value(&second.output[0]).unwrap()["name"], "get_weather"); + + let third = unwrap_blocking( + execute( + tool_search_request( + json!([{ + "type": "function_call_output", + "call_id": "call_weather", + "output": "sunny" + }]), + None, + true, + None, + Some(conversation_id), + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("conversation loaded-function output"), + ); + assert_eq!(output_text(&third), "conversation weather complete"); + + let requests = fixture.request_bodies().await; + assert_eq!(requests.len(), 3); + for request in &requests[1..] { + assert!( + request["tools"] + .as_array() + .expect("inherited tools") + .iter() + .any(|tool| { tool["name"] == "get_weather" && tool.get("defer_loading").is_none() }) + ); + assert!( + request["tools"] + .as_array() + .unwrap() + .iter() + .all(|tool| tool["name"] != "tool_search") + ); + assert!(!request.to_string().contains("tool_search_call")); + assert!(!request.to_string().contains("tool_search_output")); + } +} + +#[tokio::test] +async fn tool_search_previous_response_branch_cannot_replace_conversation_history() { + let fixture = TestFixture::new_with_responses(vec![ + function_call_response("fc_winner", "call_winner", "tool_search", r#"{"query":"winner"}"#), + text_response("winner loaded"), + text_response("winner checkpoint"), + function_call_response("fc_branch", "call_branch", "tool_search", r#"{"query":"branch"}"#), + text_response("branch loaded"), + text_response("conversation resumed"), + ]) + .await; + let conversation_id = create_conversation(&fixture.exec_ctx) + .await + .expect("create conversation") + .conversation_id; + + let winner_search = unwrap_blocking( + execute( + tool_search_request( + "find winner", + Some(tool_search_function_declarations("winner_tool", "Winner tool")), + true, + None, + Some(conversation_id.clone()), + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("conversation search call"), + ); + assert_eq!( + serde_json::to_value(&winner_search.output[0]).unwrap()["type"], + "tool_search_call" + ); + let winner = unwrap_blocking( + execute( + tool_search_request( + json!([tool_search_output("call_winner", "winner_tool", "Winner tool")]), + None, + true, + None, + Some(conversation_id.clone()), + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("conversation loaded state"), + ); + assert_eq!(output_text(&winner), "winner loaded"); + + let checkpoint = unwrap_blocking( + execute( + tool_search_request("checkpoint winner", None, true, None, Some(conversation_id.clone())), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("conversation checkpoint"), + ); + assert_eq!(output_text(&checkpoint), "winner checkpoint"); + + let branch_search = unwrap_blocking( + execute( + tool_search_request( + "find branch", + Some(tool_search_function_declarations("branch_tool", "Branch tool")), + true, + Some(checkpoint.id), + None, + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("response branch search call"), + ); + let branch = unwrap_blocking( + execute( + tool_search_request( + json!([tool_search_output("call_branch", "branch_tool", "Branch tool")]), + None, + true, + Some(branch_search.id), + None, + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("response branch loaded state"), + ); + assert_eq!(output_text(&branch), "branch loaded"); + + let resumed = unwrap_blocking( + execute( + tool_search_request("resume conversation", None, true, None, Some(conversation_id)), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("conversation continuation"), + ); + assert_eq!(output_text(&resumed), "conversation resumed"); + + let requests = fixture.request_bodies().await; + let resumed_tools = requests[5]["tools"].as_array().expect("conversation tools"); + assert!(resumed_tools.iter().any(|tool| tool["name"] == "winner_tool")); + assert!(!resumed_tools.iter().any(|tool| tool["name"] == "branch_tool")); + assert!(!requests[5].to_string().contains("branch loaded")); +} + /// Case 7 — two turns, streaming, via `conversation_id`. #[tokio::test] async fn test_two_turn_streaming_conversation() { diff --git a/crates/agentic-server-core/tests/stateful_responses_integration.rs b/crates/agentic-server-core/tests/stateful_responses_integration.rs index 3d36cce5..783822bf 100644 --- a/crates/agentic-server-core/tests/stateful_responses_integration.rs +++ b/crates/agentic-server-core/tests/stateful_responses_integration.rs @@ -12,11 +12,12 @@ use agentic_core::types::tools::{FunctionToolParam, NonEmptyToolName}; use agentic_core::{FunctionToolResultMessage, InputItem, ResponsesInput, ResponsesTool, ToolChoice}; use either::Either; use futures::StreamExt; -use serde_json::Value; +use serde_json::{Value, json}; use std::sync::Arc; use support::{ - MockResponse, TestFixture, collect_stream, expected_text, load_cassette, make_request, output_text, - request_input_texts, text_response, unwrap_blocking, + MockResponse, TestFixture, collect_stream, expected_text, function_call_response, load_cassette, make_request, + output_text, request_input_texts, text_response, tool_search_function_declarations, tool_search_output, + tool_search_request, unwrap_blocking, }; const DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/cassettes/text_only/responses"); @@ -510,7 +511,7 @@ async fn test_mcp_namespace_showcase_round_trip_rehydrates_calls_tools_and_outpu assert_flat_mcp_showcase_tools(&requests[1]["tools"]); let input = requests[1]["input"].as_array().expect("rehydrated input array"); - assert_namespaced_calls( + assert_flat_namespaced_calls( input, &["echo_text", "add_numbers", "make_slug", "repo_file_head", "search_repo"], ); @@ -562,6 +563,273 @@ async fn test_store_false_with_previous_response_id_hydrates_but_does_not_persis assert!(result.is_err(), "store=false response should not be persisted"); } +#[tokio::test] +async fn tool_search_previous_response_continuation_omits_tools_after_first_turn() { + let fixture = TestFixture::new_with_responses(vec![ + function_call_response("fc_search", "call_search", "tool_search", r#"{"query":"weather"}"#), + function_call_response("fc_weather", "call_weather", "get_weather", r#"{"city":"Paris"}"#), + text_response("weather complete"), + ]) + .await; + let declarations = tool_search_function_declarations("get_weather", "Get weather"); + + let first = unwrap_blocking( + execute( + tool_search_request("find weather", Some(declarations), true, None, None), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("stored search turn"), + ); + let search_call = serde_json::to_value(&first.output[0]).expect("public search call"); + assert_eq!(search_call["type"], "tool_search_call"); + + let second = unwrap_blocking( + execute( + tool_search_request( + json!([tool_search_output("call_search", "get_weather", "Get weather")]), + None, + true, + Some(first.id.clone()), + None, + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("search-output continuation"), + ); + let weather_call = serde_json::to_value(&second.output[0]).expect("public weather call"); + assert_eq!(weather_call["name"], "get_weather"); + + let third = unwrap_blocking( + execute( + tool_search_request( + json!([{ + "type": "function_call_output", + "call_id": "call_weather", + "output": "sunny" + }]), + None, + true, + Some(second.id.clone()), + None, + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("loaded-function continuation"), + ); + assert_eq!(output_text(&third), "weather complete"); + + let requests = fixture.request_bodies().await; + assert_eq!(requests.len(), 3); + assert_eq!(requests[1]["input"][1]["type"], "function_call"); + assert_eq!(requests[1]["input"][1]["name"], "tool_search"); + assert!( + requests[1]["tools"] + .as_array() + .expect("inherited tools") + .iter() + .any(|tool| { tool["name"] == "get_weather" && tool.get("defer_loading").is_none() }) + ); + assert!( + requests[1]["tools"] + .as_array() + .unwrap() + .iter() + .any(|tool| tool["name"] == "tool_search") + ); + assert!( + requests[2]["tools"] + .as_array() + .expect("replayed tools") + .iter() + .any(|tool| { tool["name"] == "get_weather" && tool.get("defer_loading").is_none() }) + ); + assert!( + !requests + .iter() + .any(|body| body.to_string().contains("tool_search_call")) + ); + assert!( + !requests + .iter() + .any(|body| body.to_string().contains("tool_search_output")) + ); +} + +#[tokio::test] +async fn tool_search_branch_from_earlier_response_does_not_inherit_later_loaded_definition() { + let fixture = TestFixture::new_with_responses(vec![ + function_call_response("fc_search", "call_search", "tool_search", r#"{"query":"weather"}"#), + text_response("loaded branch"), + text_response("empty branch"), + ]) + .await; + let declarations = json!([ + { + "type": "tool_search", + "execution": "client", + "description": "Find a tool", + "parameters": {"type": "object"} + }, + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + } + ]); + let first = unwrap_blocking( + execute( + tool_search_request("find weather", Some(declarations), true, None, None), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("search turn"), + ); + + let loaded_branch = unwrap_blocking( + execute( + tool_search_request( + json!([{ + "type": "tool_search_output", + "call_id": "call_search", + "tools": [{ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }] + }]), + None, + true, + Some(first.id.clone()), + None, + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("loaded branch"), + ); + assert_eq!(output_text(&loaded_branch), "loaded branch"); + + let empty_branch = unwrap_blocking( + execute( + tool_search_request( + json!([{ + "type": "tool_search_output", + "call_id": "call_search", + "tools": [] + }]), + None, + true, + Some(first.id), + None, + ), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("independent empty branch"), + ); + assert_eq!(output_text(&empty_branch), "empty branch"); + + let requests = fixture.request_bodies().await; + let loaded_tools = requests[1]["tools"].as_array().expect("loaded branch tools"); + let empty_tools = requests[2]["tools"].as_array().expect("empty branch tools"); + assert!(loaded_tools.iter().any(|tool| tool["name"] == "get_weather")); + assert!(!empty_tools.iter().any(|tool| tool["name"] == "get_weather")); +} + +#[tokio::test] +async fn tool_search_store_false_manual_replay_completes_without_reusable_response() { + let fixture = TestFixture::new_with_responses(vec![ + function_call_response("fc_search", "call_search", "tool_search", r#"{"query":"weather"}"#), + function_call_response("fc_weather", "call_weather", "get_weather", r#"{"city":"Paris"}"#), + text_response("manual replay complete"), + ]) + .await; + let declarations = json!([ + { + "type": "tool_search", + "execution": "client", + "description": "Find a tool", + "parameters": {"type": "object"} + }, + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + } + ]); + let user = json!({"type": "message", "role": "user", "content": "find weather"}); + let first = unwrap_blocking( + execute( + tool_search_request(json!([user.clone()]), Some(declarations), false, None, None), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("stateless search call"), + ); + let search_call = serde_json::to_value(&first.output[0]).unwrap(); + let search_output = json!({ + "type": "tool_search_output", + "call_id": "call_search", + "tools": [{ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }] + }); + let second_input = json!([user.clone(), search_call, search_output]); + let second = unwrap_blocking( + execute( + tool_search_request(second_input.clone(), None, false, None, None), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("stateless loaded call"), + ); + let mut final_input = second_input.as_array().unwrap().clone(); + final_input.push(serde_json::to_value(&second.output[0]).unwrap()); + final_input.push(json!({ + "type": "function_call_output", + "call_id": "call_weather", + "output": "sunny" + })); + let final_response = unwrap_blocking( + execute( + tool_search_request(Value::Array(final_input), None, false, None, None), + Arc::clone(&fixture.exec_ctx), + ) + .await + .expect("stateless final response"), + ); + assert_eq!(output_text(&final_response), "manual replay complete"); + + let lookup_ctx = RequestContext { + original_request: make_request("lookup", true, false, Some(final_response.id.clone()), None), + enriched_request: make_request("lookup", true, false, Some(final_response.id), None), + new_input_items: Vec::new(), + response_id: "resp_lookup".to_owned(), + conversation_id: None, + conversation_version: None, + }; + let error = fixture + .exec_ctx + .resp_handler + .get(&lookup_ctx) + .await + .expect_err("store:false response ID must not be reusable"); + assert!(matches!(error, agentic_core::executor::ExecutorError::Storage(source) if source.is_not_found())); +} + #[tokio::test] async fn test_previous_response_id_persists_inherited_tools_and_choice() { let fixture = @@ -795,6 +1063,20 @@ fn assert_namespaced_calls(items: &[Value], expected_names: &[&str]) { } } +fn assert_flat_namespaced_calls(items: &[Value], expected_names: &[&str]) { + for expected_name in expected_names { + let flat_name = format!("agentic_ns__mcp__agentic_fixture__{expected_name}"); + assert!( + items.iter().any(|item| { + item.get("type").and_then(Value::as_str) == Some("function_call") + && item.get("name").and_then(Value::as_str) == Some(&flat_name) + && item.get("namespace").is_none() + }), + "missing private flat function call {flat_name}" + ); + } +} + fn assert_tool_outputs(items: &[Value], expected_call_ids: &[&str]) { for expected_call_id in expected_call_ids { assert!( diff --git a/crates/agentic-server-core/tests/storage_integration.rs b/crates/agentic-server-core/tests/storage_integration.rs index 62ee2388..e60ca694 100644 --- a/crates/agentic-server-core/tests/storage_integration.rs +++ b/crates/agentic-server-core/tests/storage_integration.rs @@ -866,6 +866,190 @@ async fn test_response_store_get_after_persist() { assert_eq!(retrieved.history_item_ids.len(), 2); } +#[tokio::test] +async fn test_tool_search_conversation_metadata_matches_snapshot_version() { + let pool = setup_pool().await; + let store = ConversationStore::new(Arc::clone(&pool)); + let conversation = store.create().await.expect("create conversation"); + store + .persist( + &conversation.conversation_id, + "resp_tool_search_initial", + None, + vec![create_input_item("initial")], + &ResponseMetadata { + model: "initial-model".to_owned(), + ..ResponseMetadata::default() + }, + ) + .await + .expect("persist initial turn"); + + let loaded_tool: agentic_core::types::tools::ResponsesTool = serde_json::from_value(serde_json::json!({ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + })) + .expect("valid loaded function"); + let latest = ResponseMetadata { + model: "latest-model".to_owned(), + effective_tools: Some(vec![loaded_tool.clone()]), + tool_search_loaded_tools: Some(vec![loaded_tool]), + ..ResponseMetadata::default() + }; + store + .persist( + &conversation.conversation_id, + "resp_tool_search_latest", + None, + vec![create_input_item("latest")], + &latest, + ) + .await + .expect("persist latest turn"); + + // Simulate replicas whose clocks and process-local UUID order disagree with committed conversation-item sequence. + sqlx::query("UPDATE responses SET created_at = $1 WHERE id = $2") + .bind(9_999_i64) + .bind("resp_tool_search_initial") + .execute(pool.as_ref()) + .await + .expect("make older turn appear newer by wall clock"); + sqlx::query("UPDATE responses SET created_at = $1 WHERE id = $2") + .bind(1_i64) + .bind("resp_tool_search_latest") + .execute(pool.as_ref()) + .await + .expect("make latest turn appear older by wall clock"); + + let latest_item_id: String = + sqlx::query_scalar("SELECT id FROM items WHERE conversation_id = $1 ORDER BY seq DESC LIMIT 1") + .bind(&conversation.conversation_id) + .fetch_one(pool.as_ref()) + .await + .expect("latest conversation item"); + let branch = ResponseMetadata { + model: "branch-model".to_owned(), + ..ResponseMetadata::default() + }; + sqlx::query( + "INSERT INTO responses \ + (id, conversation_id, previous_response_id, history_item_ids, metadata, created_at) \ + VALUES ($1, $2, $3, $4, $5, $6)", + ) + .bind("resp_tool_search_branch") + .bind(&conversation.conversation_id) + .bind("resp_tool_search_latest") + .bind(serde_json::to_string(&vec![latest_item_id]).expect("branch history JSON")) + .bind(String::try_from(&branch).expect("branch metadata JSON")) + .bind(20_000_i64) + .execute(pool.as_ref()) + .await + .expect("seed conversation-tagged response branch"); + + let snapshot = store + .rehydrate_snapshot(&conversation.conversation_id) + .await + .expect("rehydrate typed snapshot"); + store + .persist( + &conversation.conversation_id, + "resp_after_snapshot", + None, + vec![create_input_item("after snapshot")], + &ResponseMetadata { + model: "after-snapshot-model".to_owned(), + ..ResponseMetadata::default() + }, + ) + .await + .expect("persist a newer conversation turn"); + let metadata = store + .response_metadata_at_version(&conversation.conversation_id, snapshot.version) + .await + .expect("load response metadata at snapshot version") + .expect("response metadata accompanies conversation items"); + assert_eq!(metadata.model, "latest-model"); + assert_eq!(metadata.tool_search_loaded_tools.as_deref().map(<[_]>::len), Some(1)); +} + +#[tokio::test] +async fn test_tool_search_conversation_conflict_does_not_persist_stale_loaded_state() { + let pool = setup_pool().await; + let store = ConversationStore::new(pool); + let conversation = store.create().await.expect("create conversation"); + let snapshot = store + .rehydrate_snapshot(&conversation.conversation_id) + .await + .expect("capture empty version"); + let winning_tool: agentic_core::types::tools::ResponsesTool = serde_json::from_value(serde_json::json!({ + "type": "function", + "name": "winning_tool", + "parameters": {"type": "object"}, + "defer_loading": true + })) + .expect("winning tool"); + let stale_tool: agentic_core::types::tools::ResponsesTool = serde_json::from_value(serde_json::json!({ + "type": "function", + "name": "stale_tool", + "parameters": {"type": "object"}, + "defer_loading": true + })) + .expect("stale tool"); + let winning = ResponseMetadata { + model: "winner".to_owned(), + effective_tools: Some(vec![winning_tool.clone()]), + tool_search_loaded_tools: Some(vec![winning_tool]), + ..ResponseMetadata::default() + }; + let stale = ResponseMetadata { + model: "stale".to_owned(), + effective_tools: Some(vec![stale_tool.clone()]), + tool_search_loaded_tools: Some(vec![stale_tool]), + ..ResponseMetadata::default() + }; + + store + .persist_if_version( + &conversation.conversation_id, + snapshot.version, + "resp_tool_search_winner", + None, + vec![create_input_item("winner")], + &winning, + ) + .await + .expect("winning turn persists"); + let error = store + .persist_if_version( + &conversation.conversation_id, + snapshot.version, + "resp_tool_search_stale", + None, + vec![create_input_item("stale")], + &stale, + ) + .await + .expect_err("stale turn conflicts"); + assert!(matches!(error, StorageError::ConversationConflict { .. })); + + let snapshot = store + .rehydrate_snapshot(&conversation.conversation_id) + .await + .expect("rehydrate winning state"); + let latest = store + .response_metadata_at_version(&conversation.conversation_id, snapshot.version) + .await + .expect("load winning metadata") + .expect("winning metadata"); + assert_eq!(latest.model, "winner"); + let serialized = serde_json::to_value(latest.tool_search_loaded_tools).unwrap(); + assert_eq!(serialized[0]["name"], "winning_tool"); + assert!(!serialized.to_string().contains("stale_tool")); +} + #[tokio::test] async fn test_conversation_get_or_create_same_id() { let pool = setup_pool().await; diff --git a/crates/agentic-server-core/tests/support/mod.rs b/crates/agentic-server-core/tests/support/mod.rs index 0885f2c9..59017ebe 100644 --- a/crates/agentic-server-core/tests/support/mod.rs +++ b/crates/agentic-server-core/tests/support/mod.rs @@ -384,6 +384,76 @@ pub fn text_response(text: &str) -> MockResponse { ) } +pub fn function_call_response(id: &str, call_id: &str, name: &str, arguments: &str) -> MockResponse { + MockResponse::Json( + serde_json::json!({ + "id": format!("resp_upstream_{id}"), + "object": "response", + "created_at": 0, + "model": "test-model", + "status": "completed", + "output": [{ + "id": id, + "type": "function_call", + "call_id": call_id, + "name": name, + "arguments": arguments, + "status": "completed" + }], + "usage": null, + "incomplete_details": null, + "error": null, + "previous_response_id": null, + "conversation_id": null, + "instructions": null + }) + .to_string(), + ) +} + +pub fn tool_search_function_declarations(name: &str, description: &str) -> Value { + serde_json::json!([ + { + "type": "tool_search", + "execution": "client", + "description": "Find a tool", + "parameters": {"type": "object"} + }, + deferred_function_tool(name, description) + ]) +} + +pub fn deferred_function_tool(name: &str, description: &str) -> Value { + serde_json::json!({ + "type": "function", + "name": name, + "description": description, + "parameters": {"type": "object"}, + "defer_loading": true + }) +} + +pub fn tool_search_output(call_id: &str, name: &str, description: &str) -> Value { + serde_json::json!({ + "type": "tool_search_output", + "call_id": call_id, + "tools": [deferred_function_tool(name, description)] + }) +} + +pub fn tool_search_request( + input: impl Serialize, + tools: Option, + store: bool, + previous_response_id: Option, + conversation_id: Option, +) -> RequestPayload { + let mut request = make_request(input, store, false, previous_response_id, conversation_id); + request.tools = tools.map(|tools| serde_json::from_value(tools).expect("valid tool-search declarations")); + request.parallel_tool_calls = Some(false); + request +} + pub fn request_input_texts(body: &Value) -> Vec { match &body["input"] { Value::String(text) => vec![text.clone()], @@ -470,6 +540,7 @@ pub fn output_text(payload: &ResponsePayload) -> String { .filter_map(|item| match item { OutputItem::Message(msg) => Some(msg.content.iter().map(|c| c.text.as_str()).collect::()), OutputItem::FunctionCall(_) + | OutputItem::ToolSearchCall(_) | OutputItem::CustomToolCall(_) | OutputItem::WebSearchCall(_) | OutputItem::McpCall(_) diff --git a/crates/agentic-server-core/tests/tool_search_characterization_test.rs b/crates/agentic-server-core/tests/tool_search_characterization_test.rs new file mode 100644 index 00000000..c7366de6 --- /dev/null +++ b/crates/agentic-server-core/tests/tool_search_characterization_test.rs @@ -0,0 +1,1712 @@ +mod support; + +use std::collections::HashMap; +use std::fs; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::path::{Path, PathBuf}; + +use agentic_core::RequestPayload; +use agentic_core::tool::{ToolSearchState, model_visible_namespace_member_name}; +use serde_json::Value; + +#[derive(Clone, Copy)] +enum Projection { + Public, + Normalized, +} + +#[derive(Debug, PartialEq, Eq)] +struct SemanticFlow { + execution: &'static str, + status: &'static str, + returned_tools: Value, + loaded_calls: Vec, + final_text: String, +} + +#[derive(Debug, PartialEq, Eq)] +struct LoadedCall { + namespace: Option, + name: String, + function_output: Value, +} + +#[derive(Debug, PartialEq, Eq)] +struct RelevantCallLifecycle { + event_type: String, + status: Option, + arguments: Option, + execution: Option, + item_id: Option, + call_id: Option, +} + +const OPENAI_BLOCKING_CASSETTE: &str = "tool-search-openai-reference-gpt-5.6-nonstreaming.yaml"; +const OPENAI_STREAMING_CASSETTE: &str = "tool-search-openai-reference-gpt-5.6-streaming.yaml"; +const DIRECT_VLLM_BLOCKING_CASSETTE: &str = "tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml"; +const DIRECT_VLLM_STREAMING_CASSETTE: &str = "tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml"; +const GATEWAY_BLOCKING_CASSETTE: &str = "tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml"; +const GATEWAY_STREAMING_CASSETTE: &str = "tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml"; +const GATEWAY_WEBSOCKET_CASSETTE: &str = "tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-websocket.yaml"; + +fn tool_search_cassette_directory() -> PathBuf { + std::env::var_os("TOOL_SEARCH_CASSETTE_DIR").map_or_else( + || Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/cassettes/tool_search"), + PathBuf::from, + ) +} + +fn one_gateway_stream_cassette(directory: &Path, suffix: &str) -> PathBuf { + let matches = fs::read_dir(directory) + .expect("tool-search cassette directory") + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + path.file_name().and_then(|name| name.to_str()).is_some_and(|name| { + name.starts_with("tool-search-gateway-") + && !name.ends_with("-nonstreaming.yaml") + && name.ends_with(suffix) + }) + }) + .collect::>(); + assert_eq!( + matches.len(), + 1, + "expected one recorder-generated gateway {suffix} cassette, found {matches:?}" + ); + matches.into_iter().next().expect("one gateway stream cassette") +} + +fn relevant_call_lifecycle_from_named_sse<'a>(lines: impl IntoIterator) -> Vec { + let mut call_ids_by_item_id = HashMap::::new(); + + support::named_sse_events(lines) + .into_iter() + .filter_map(|event| { + let item = event.get("item").and_then(Value::as_object); + let is_call_item = item.is_some_and(|item| { + matches!( + item.get("type").and_then(Value::as_str), + Some("tool_search_call" | "function_call" | "custom_tool_call") + ) + }); + if is_call_item { + let item_id = item.and_then(|item| item.get("id")).and_then(Value::as_str); + let call_id = item.and_then(|item| item.get("call_id")).and_then(Value::as_str); + if let (Some(item_id), Some(call_id)) = (item_id, call_id) { + call_ids_by_item_id.insert(item_id.to_string(), call_id.to_string()); + } + } + + let linked_item_id = event.get("item_id").and_then(Value::as_str); + if !is_call_item && !linked_item_id.is_some_and(|item_id| call_ids_by_item_id.contains_key(item_id)) { + return None; + } + + let status = item + .and_then(|item| item.get("status")) + .or_else(|| event.get("status")) + .and_then(Value::as_str) + .map(str::to_string); + let arguments = event + .get("arguments") + .or_else(|| item.and_then(|item| item.get("arguments"))) + .or_else(|| event.get("delta")) + .cloned(); + let execution = item + .and_then(|item| item.get("execution")) + .or_else(|| event.get("execution")) + .and_then(Value::as_str) + .map(str::to_string); + let call_id = item + .and_then(|item| item.get("call_id")) + .and_then(Value::as_str) + .or_else(|| event.get("call_id").and_then(Value::as_str)) + .map(str::to_string) + .or_else(|| linked_item_id.and_then(|item_id| call_ids_by_item_id.get(item_id).cloned())); + let item_id = item + .and_then(|item| item.get("id")) + .and_then(Value::as_str) + .or(linked_item_id) + .map(str::to_string); + + Some(RelevantCallLifecycle { + event_type: event["type"] + .as_str() + .expect("named SSE event should have a type") + .to_string(), + status, + arguments, + execution, + item_id, + call_id, + }) + }) + .collect() +} + +fn non_empty_call_id(item: &Value) -> &str { + let call_id = item["call_id"].as_str().expect("tool item should have a call_id"); + assert!(!call_id.trim().is_empty(), "tool item call_id should not be empty"); + call_id +} + +fn terminal_response(turn: &support::Turn) -> Value { + if let Some(body) = &turn.response.body { + return body.clone(); + } + support::recorded_named_sse_events(turn) + .into_iter() + .find(|event| event["type"] == "response.completed") + .and_then(|event| event.get("response").cloned()) + .expect("streaming characterization should contain response.completed") +} + +fn terminal_response_from_sse_chunks(chunks: &[&str]) -> Value { + support::named_sse_events(chunks.iter().flat_map(|chunk| chunk.lines())) + .into_iter() + .find(|event| event["type"] == "response.completed") + .and_then(|event| event.get("response").cloned()) + .expect("offline SSE should contain response.completed") +} + +fn assert_stream_completed(events: &[Value]) { + assert!( + events + .iter() + .all(|event| !matches!(event["type"].as_str(), Some("error" | "response.failed"))), + "recorded stream must not contain failure events" + ); + let completed = events + .iter() + .filter(|event| event["type"] == "response.completed") + .collect::>(); + assert_eq!(completed.len(), 1, "recorded stream should complete exactly once"); + assert_eq!(completed[0]["response"]["status"], "completed"); +} + +fn response_lifecycle_metadata(turn: &support::Turn) -> Vec { + support::recorded_named_sse_events(turn) + .into_iter() + .filter(|event| { + matches!( + event["type"].as_str(), + Some("response.created" | "response.in_progress" | "response.completed") + ) + }) + .collect() +} + +fn assert_post_search_response_metadata(turn: &support::Turn, expected_tools: &Value) { + let events = response_lifecycle_metadata(turn); + assert_eq!( + events + .iter() + .map(|event| event["type"].as_str().expect("lifecycle event type")) + .collect::>(), + ["response.created", "response.in_progress", "response.completed"] + ); + let expected_choice = turn + .request + .body + .tool_choice + .as_ref() + .expect("tool-search flow turn should specify tool_choice"); + for event in events { + assert_eq!( + canonical_response_tools(&event["response"]["tools"]), + *expected_tools, + "{} must expose only public callable tools", + event["type"] + ); + assert_eq!( + &event["response"]["tool_choice"], expected_choice, + "{} must preserve the public tool choice", + event["type"] + ); + } +} + +fn canonical_response_tools(tools: &Value) -> Value { + let mut tools = tools.clone(); + let Some(tool_array) = tools.as_array_mut() else { + return tools; + }; + for tool in tool_array { + canonical_response_tool(tool, false); + } + tools +} + +fn canonical_response_tool(tool: &mut Value, make_callable: bool) { + let Some(tool) = tool.as_object_mut() else { + return; + }; + if make_callable { + tool.remove("defer_loading"); + } + if tool.get("output_schema").is_some_and(Value::is_null) { + tool.remove("output_schema"); + } + if let Some(members) = tool.get_mut("tools").and_then(Value::as_array_mut) { + for member in members { + canonical_response_tool(member, make_callable); + } + } +} + +fn expected_loaded_response_tools(directory: &Path) -> Value { + let mut tools = fixture_json(directory, "returned_tools.json"); + for tool in tools.as_array_mut().expect("returned tools fixture") { + canonical_response_tool(tool, true); + } + tools +} + +fn assert_loaded_response_tool_shape(tools: &Value) { + let tools = tools.as_array().expect("loaded response tools should be an array"); + assert_eq!(tools.len(), 2); + assert_eq!(tools[0]["type"], "function"); + assert_eq!(tools[0]["name"], "get_weather"); + assert!(tools[0].get("defer_loading").is_none()); + assert_eq!(tools[1]["type"], "namespace"); + assert_eq!(tools[1]["name"], "travel"); + let members = tools[1]["tools"].as_array().expect("travel namespace members"); + assert_eq!(members.len(), 1); + assert_eq!(members[0]["type"], "function"); + assert_eq!(members[0]["name"], "get_timezone"); + assert!(members[0].get("defer_loading").is_none()); +} + +fn assert_observed_call_lifecycle(turn: &support::Turn) -> (String, String, Value) { + let events = support::recorded_named_sse_events(turn); + assert_stream_completed(&events); + + let lifecycle = relevant_call_lifecycle_from_named_sse( + turn.response + .sse + .as_ref() + .expect("streaming cassette should contain SSE") + .iter() + .flat_map(|entry| entry.lines()), + ); + assert!( + lifecycle.len() >= 4, + "call lifecycle should include added, deltas, and done events" + ); + assert_eq!(lifecycle[0].event_type, "response.output_item.added"); + assert_eq!( + lifecycle.last().expect("lifecycle should not be empty").event_type, + "response.output_item.done" + ); + assert_eq!( + lifecycle.last().and_then(|event| event.status.as_deref()), + Some("completed") + ); + + let arguments_done_indices = lifecycle + .iter() + .enumerate() + .filter_map(|(index, event)| (event.event_type == "response.function_call_arguments.done").then_some(index)) + .collect::>(); + assert_eq!( + arguments_done_indices.len(), + 1, + "call lifecycle should contain exactly one arguments.done event" + ); + let arguments_done_index = arguments_done_indices[0]; + assert!( + arguments_done_index > 1, + "call lifecycle should contain at least one argument delta" + ); + assert_eq!(arguments_done_index + 1, lifecycle.len() - 1); + assert!( + lifecycle[1..arguments_done_index] + .iter() + .all(|event| event.event_type == "response.function_call_arguments.delta"), + "only argument deltas should occur between output_item.added and arguments.done" + ); + + let aggregated_arguments = lifecycle[1..arguments_done_index] + .iter() + .map(|event| { + event + .arguments + .as_ref() + .and_then(Value::as_str) + .expect("argument delta should be text") + }) + .collect::(); + let done_arguments = lifecycle[arguments_done_index] + .arguments + .as_ref() + .and_then(Value::as_str) + .expect("arguments.done should contain final arguments"); + assert_eq!( + aggregated_arguments, done_arguments, + "aggregated deltas should be invariant to provider chunk boundaries" + ); + assert_eq!( + lifecycle.last().and_then(|event| event.arguments.as_ref()), + lifecycle[arguments_done_index].arguments.as_ref(), + "output_item.done should repeat the completed arguments" + ); + + let item_id = lifecycle[0] + .item_id + .as_deref() + .expect("output_item.added should contain an item ID"); + let call_id = lifecycle[0] + .call_id + .as_deref() + .expect("output_item.added should contain a call ID"); + assert!(!item_id.trim().is_empty()); + assert!(!call_id.trim().is_empty()); + assert!( + lifecycle.iter().all(|event| event.item_id.as_deref() == Some(item_id)), + "all call lifecycle events should link to one item ID" + ); + assert!( + lifecycle.iter().all(|event| event.call_id.as_deref() == Some(call_id)), + "all call lifecycle events should link to one call ID" + ); + + ( + item_id.to_string(), + call_id.to_string(), + serde_json::from_str(done_arguments).expect("completed arguments should be valid JSON"), + ) +} + +fn client_calls(output: &[Value]) -> Vec<&Value> { + output + .iter() + .filter(|item| { + matches!( + item["type"].as_str(), + Some("tool_search_call" | "function_call" | "custom_tool_call") + ) + }) + .collect() +} + +fn normalize_search_step(response: &Value, continuation: &Value, projection: Projection) -> Value { + let output = response["output"] + .as_array() + .expect("first response output should be an array"); + assert_eq!( + client_calls(output).len(), + 1, + "first response should contain exactly one client call" + ); + let search_calls = output + .iter() + .filter(|item| match projection { + Projection::Public => item["type"] == "tool_search_call", + Projection::Normalized => item["type"] == "function_call" && item["name"] == "tool_search", + }) + .collect::>(); + assert_eq!( + search_calls.len(), + 1, + "first response should contain exactly one search call" + ); + let search_call = search_calls[0]; + let search_call_id = non_empty_call_id(search_call); + assert_eq!(search_call["status"], "completed"); + match projection { + Projection::Public => { + assert_eq!(search_call["execution"], "client"); + assert!(search_call["arguments"].as_object().is_some_and(|arguments| { + arguments + .get("query") + .and_then(Value::as_str) + .is_some_and(|query| !query.is_empty()) + })); + } + Projection::Normalized => { + let arguments = search_call["arguments"] + .as_str() + .and_then(|arguments| serde_json::from_str::(arguments).ok()) + .expect("normalized search arguments should be valid JSON"); + assert!( + arguments["query"].as_str().is_some_and(|query| !query.is_empty()), + "normalized search arguments should contain a query" + ); + } + } + + let search_outputs = continuation + .as_array() + .expect("search continuation should be an input array") + .iter() + .filter(|item| { + item["call_id"] == search_call_id + && match projection { + Projection::Public => item["type"] == "tool_search_output", + Projection::Normalized => item["type"] == "function_call_output", + } + }) + .collect::>(); + assert_eq!( + search_outputs.len(), + 1, + "exactly one search output should link to the search call" + ); + let search_output = search_outputs[0]; + match projection { + Projection::Public => { + assert_eq!(search_output["execution"], "client"); + assert_eq!(search_output["status"], "completed"); + search_output["tools"].clone() + } + Projection::Normalized => { + let output = search_output["output"] + .as_str() + .expect("normalized search output should be JSON text"); + let decoded = serde_json::from_str::(output).expect("normalized search output should be valid JSON"); + assert_eq!( + output, + serde_json::to_string(&decoded).expect("normalized search output should serialize canonically"), + "normalized search output should use canonical compact JSON" + ); + decoded["tools"].clone() + } + } +} + +fn returned_identity_for_call( + loaded_call: &Value, + returned_tools: &Value, + projection: Projection, +) -> (Option, String) { + let raw_name = loaded_call["name"] + .as_str() + .expect("loaded function call should have a name"); + let raw_namespace = loaded_call.get("namespace").and_then(Value::as_str); + let returned_tools = returned_tools + .as_array() + .expect("search output tools should be an array"); + + if let Some(namespace) = raw_namespace { + assert!( + matches!(projection, Projection::Public), + "direct-vLLM calls should use a flattened model-visible name" + ); + let namespace_tool = returned_tools + .iter() + .find(|tool| tool["type"] == "namespace" && tool["name"] == namespace) + .expect("public namespace call should come from the search output"); + assert!( + namespace_tool["tools"] + .as_array() + .is_some_and(|members| members.iter().any(|member| member["name"] == raw_name)), + "public namespace member should come from the search output" + ); + return (Some(namespace.to_owned()), raw_name.to_owned()); + } + + if returned_tools + .iter() + .any(|tool| tool["type"] == "function" && tool["name"] == raw_name) + { + return (None, raw_name.to_owned()); + } + + assert!( + matches!(projection, Projection::Normalized), + "public namespace calls should preserve their namespace" + ); + for namespace_tool in returned_tools.iter().filter(|tool| tool["type"] == "namespace") { + let namespace = namespace_tool["name"] + .as_str() + .expect("returned namespace should have a name"); + for member in namespace_tool["tools"] + .as_array() + .expect("returned namespace should contain members") + { + let member_name = member["name"] + .as_str() + .expect("returned namespace member should have a name"); + if model_visible_namespace_member_name(namespace, member_name) == raw_name { + return (Some(namespace.to_owned()), member_name.to_owned()); + } + } + } + panic!("called function should come from the search output: {raw_name}"); +} + +fn normalize_loaded_step( + response: &Value, + continuation: &Value, + returned_tools: &Value, + projection: Projection, +) -> LoadedCall { + let output = response["output"] + .as_array() + .expect("loaded-tool response output should be an array"); + assert_eq!( + client_calls(output).len(), + 1, + "each loaded-tool response should contain exactly one client call" + ); + let loaded_calls = output + .iter() + .filter(|item| item["type"] == "function_call" && item["name"] != "tool_search") + .collect::>(); + assert_eq!( + loaded_calls.len(), + 1, + "loaded-tool response should call exactly one loaded function" + ); + let loaded_call = loaded_calls[0]; + let loaded_call_id = non_empty_call_id(loaded_call); + assert_eq!(loaded_call["status"], "completed"); + let (namespace, name) = returned_identity_for_call(loaded_call, returned_tools, projection); + let loaded_arguments = loaded_call["arguments"] + .as_str() + .and_then(|arguments| serde_json::from_str::(arguments).ok()) + .expect("loaded function arguments should be valid JSON"); + assert_eq!(loaded_arguments, serde_json::json!({"city": "Paris"})); + + let function_outputs = continuation + .as_array() + .expect("function continuation should be an input array") + .iter() + .filter(|item| item["type"] == "function_call_output" && item["call_id"] == loaded_call_id) + .collect::>(); + assert_eq!( + function_outputs.len(), + 1, + "exactly one function output should link to the loaded call" + ); + LoadedCall { + namespace, + name, + function_output: function_outputs[0]["output"].clone(), + } +} + +fn normalized_final_text(response: &Value) -> String { + let output = response["output"] + .as_array() + .expect("final response output should be an array"); + assert!( + client_calls(output).is_empty(), + "final response must not contain tool calls" + ); + output + .iter() + .filter(|item| item["type"] == "message") + .flat_map(|message| message["content"].as_array().into_iter().flatten()) + .filter(|part| part["type"] == "output_text") + .filter_map(|part| part["text"].as_str()) + .collect::() + .trim() + .to_owned() +} + +fn normalize_flow(responses: &[Value], continuation_inputs: &[Value], projection: Projection) -> SemanticFlow { + assert_eq!(responses.len(), 4, "tool-search characterization needs four responses"); + assert_eq!( + continuation_inputs.len(), + 3, + "tool-search characterization needs three continuations" + ); + let returned_tools = normalize_search_step(&responses[0], &continuation_inputs[0], projection); + let loaded_calls = vec![ + normalize_loaded_step(&responses[1], &continuation_inputs[1], &returned_tools, projection), + normalize_loaded_step(&responses[2], &continuation_inputs[2], &returned_tools, projection), + ]; + assert_eq!( + loaded_calls + .iter() + .map(|call| (call.namespace.as_deref(), call.name.as_str())) + .collect::>(), + [(None, "get_weather"), (Some("travel"), "get_timezone")], + "the flow must call only the selected ordinary function and selected namespace member" + ); + SemanticFlow { + execution: "client", + status: "completed", + returned_tools, + loaded_calls, + final_text: normalized_final_text(&responses[3]), + } +} + +fn mixed_loaded_tool_definitions() -> Value { + serde_json::json!([ + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + }, + { + "type": "namespace", + "name": "travel", + "description": "Travel tools", + "tools": [{ + "type": "function", + "name": "get_timezone", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": true + }] + } + ]) +} + +fn fixture_json(directory: &Path, filename: &str) -> Value { + serde_json::from_str( + &fs::read_to_string(directory.join(filename)) + .unwrap_or_else(|error| panic!("{filename} should be readable: {error}")), + ) + .unwrap_or_else(|error| panic!("{filename} should be valid JSON: {error}")) +} + +fn lowered_fixture_tools(tools: &Value, input: &Value) -> Value { + let mut request: RequestPayload = serde_json::from_value(serde_json::json!({ + "model": "fixture-model", + "input": input, + "tools": tools, + "store": false, + "stream": false, + "parallel_tool_calls": false + })) + .expect("fixture should deserialize as a public request"); + let mut state = ToolSearchState::build(&request).expect("fixture should build tool-search state"); + state + .prepare_inference_request(&mut request) + .expect("fixture should prepare private inference state"); + let upstream = request + .to_upstream_request(false) + .expect("fixture should lower into an upstream request"); + serde_json::to_value(upstream).expect("upstream fixture should serialize")["tools"].clone() +} + +#[test] +fn mixed_catalog_fixtures_match_private_tool_search_lowering() { + let directory = tool_search_cassette_directory(); + let public_tools = fixture_json(&directory, "openai_tools.json"); + let returned_tools = fixture_json(&directory, "returned_tools.json"); + let expected_initial = fixture_json(&directory, "vllm_initial_tools.json"); + let expected_loaded = fixture_json(&directory, "vllm_tools_after_search.json"); + let openai_tool_choices = fixture_json(&directory, "openai_tool_choice_sequence.json"); + let gateway_tool_choices = fixture_json(&directory, "gateway_tool_choice_sequence.json"); + + for choice in [openai_tool_choices, gateway_tool_choices].iter().flat_map(|choices| { + choices + .as_array() + .expect("public tool-choice sequence should be an array") + }) { + serde_json::from_value::(serde_json::json!({ + "model": "fixture-model", + "input": "fixture input", + "tools": public_tools, + "tool_choice": choice, + "parallel_tool_calls": false + })) + .expect("every public tool-choice fixture should match the typed request model"); + } + + assert_eq!( + lowered_fixture_tools(&public_tools, &serde_json::json!("find weather and timezone tools")), + expected_initial + ); + assert_eq!( + lowered_fixture_tools( + &public_tools, + &serde_json::json!([ + { + "type": "tool_search_call", + "id": "tsc_fixture", + "call_id": "call_fixture", + "execution": "client", + "status": "completed", + "arguments": {"query": "weather and timezone"} + }, + { + "type": "tool_search_output", + "call_id": "call_fixture", + "execution": "client", + "status": "completed", + "tools": returned_tools + } + ]) + ), + expected_loaded + ); +} + +fn public_semantic_fixture(returned_tools: &Value) -> (Vec, Vec) { + let responses = vec![ + serde_json::json!({ + "id": "resp_public_search", + "created_at": 10, + "usage": {"total_tokens": 50}, + "output": [{ + "id": "tsc_public", + "type": "tool_search_call", + "call_id": "call_public_search", + "execution": "client", + "status": "completed", + "arguments": {"query": "weather tool"} + }] + }), + serde_json::json!({ + "id": "resp_public_weather", + "reasoning": {"summary": "provider noise"}, + "output": [{ + "id": "fc_public", + "type": "function_call", + "name": "get_weather", + "call_id": "call_public_function", + "status": "completed", + "arguments": "{\"city\":\"Paris\"}" + }] + }), + serde_json::json!({ + "id": "resp_public_timezone", + "output": [{ + "id": "fc_public_timezone", + "type": "function_call", + "namespace": "travel", + "name": "get_timezone", + "call_id": "call_public_timezone", + "status": "completed", + "arguments": "{\"city\":\"Paris\"}" + }] + }), + serde_json::json!({ + "id": "resp_public_final", + "output": [{ + "type": "message", + "content": [{"type": "output_text", "text": "\n\nPARIS_MIXED_TOOLS_OK"}] + }] + }), + ]; + let inputs = vec![ + serde_json::json!([{ + "type": "tool_search_output", + "call_id": "call_public_search", + "execution": "client", + "status": "completed", + "tools": returned_tools + }]), + serde_json::json!([{ + "type": "function_call_output", + "call_id": "call_public_function", + "output": "weather result" + }]), + serde_json::json!([{ + "type": "function_call_output", + "call_id": "call_public_timezone", + "output": "timezone result" + }]), + ]; + (responses, inputs) +} + +fn normalized_semantic_fixture(returned_tools: &Value) -> (Vec, Vec) { + let responses = vec![ + serde_json::json!({ + "id": "resp_vllm_search", + "created_at": 999, + "usage": null, + "output": [{ + "id": "fc_vllm_search", + "type": "function_call", + "name": "tool_search", + "call_id": "call_vllm_search", + "status": "completed", + "arguments": "{\"query\":\"weather tool\"}" + }] + }), + serde_json::json!({ + "id": "resp_vllm_weather", + "output": [{ + "id": "fc_vllm", + "type": "function_call", + "name": "get_weather", + "call_id": "call_vllm_function", + "status": "completed", + "arguments": "{\"city\":\"Paris\"}" + }] + }), + serde_json::json!({ + "id": "resp_vllm_timezone", + "output": [{ + "id": "fc_vllm_timezone", + "type": "function_call", + "name": "agentic_ns__travel__get_timezone", + "call_id": "call_vllm_timezone", + "status": "completed", + "arguments": "{\"city\":\"Paris\"}" + }] + }), + serde_json::json!({ + "id": "resp_vllm_final", + "output": [{ + "type": "message", + "content": [{"type": "output_text", "text": "PARIS_MIXED_TOOLS_OK"}] + }] + }), + ]; + let inputs = vec![ + serde_json::json!([{ + "type": "function_call_output", + "call_id": "call_vllm_search", + "output": serde_json::to_string(&serde_json::json!({"tools": returned_tools})).unwrap() + }]), + serde_json::json!([{ + "type": "function_call_output", + "call_id": "call_vllm_function", + "output": "weather result" + }]), + serde_json::json!([{ + "type": "function_call_output", + "call_id": "call_vllm_timezone", + "output": "timezone result" + }]), + ]; + (responses, inputs) +} + +fn normalized_manual_inputs(responses: &[Value], inputs: &[Value]) -> Vec { + let prompts = ["call weather", "call timezone", "finish"]; + let mut history = vec![serde_json::json!({ + "type": "message", + "role": "user", + "content": "find weather and timezone tools" + })]; + responses + .iter() + .zip(inputs) + .zip(prompts) + .map(|((response, input), prompt)| { + history.extend( + response["output"] + .as_array() + .expect("fixture response output") + .iter() + .cloned(), + ); + history.extend(input.as_array().expect("fixture continuation input").iter().cloned()); + history.push(serde_json::json!({ + "type": "message", + "role": "user", + "content": prompt + })); + Value::Array(history.clone()) + }) + .collect() +} + +fn assert_semantic_mutations_are_visible( + public: &SemanticFlow, + public_responses: &[Value], + public_inputs: &[Value], + normalized_responses: &[Value], + normalized_inputs: &[Value], +) { + let mut schema_mutation = normalized_inputs.to_vec(); + let mut decoded = serde_json::from_str::(schema_mutation[0][0]["output"].as_str().unwrap()).unwrap(); + decoded["tools"][0]["parameters"]["properties"]["city"]["type"] = Value::String("number".to_string()); + schema_mutation[0][0]["output"] = Value::String(serde_json::to_string(&decoded).unwrap()); + let mutated = normalize_flow(normalized_responses, &schema_mutation, Projection::Normalized); + assert_ne!(public, &mutated, "schema mutations must remain semantically visible"); + + let mut function_output_mutation = normalized_inputs.to_vec(); + function_output_mutation[1][0]["output"] = Value::String("different weather".to_string()); + let mutated = normalize_flow(normalized_responses, &function_output_mutation, Projection::Normalized); + assert_ne!( + public, &mutated, + "client function-output mutations must remain semantically visible" + ); + + let mut namespace_output_mutation = normalized_inputs.to_vec(); + namespace_output_mutation[2][0]["output"] = Value::String("different timezone".to_string()); + let mutated = normalize_flow(normalized_responses, &namespace_output_mutation, Projection::Normalized); + assert_ne!( + public, &mutated, + "client namespace-member output mutations must remain semantically visible" + ); + + let mut status_mutation = public_responses.to_vec(); + status_mutation[0]["output"][0]["status"] = Value::String("in_progress".to_string()); + assert!( + catch_unwind(AssertUnwindSafe(|| { + normalize_flow(&status_mutation, public_inputs, Projection::Public) + })) + .is_err(), + "public status mutation should be rejected" + ); + + for (output_index, label) in [ + (0, "normalized search"), + (1, "normalized loaded function"), + (2, "normalized loaded namespace member"), + ] { + let mut status_mutation = normalized_responses.to_vec(); + status_mutation[output_index]["output"][0]["status"] = Value::String("in_progress".to_string()); + assert!( + catch_unwind(AssertUnwindSafe(|| { + normalize_flow(&status_mutation, normalized_inputs, Projection::Normalized) + })) + .is_err(), + "{label} status mutation should be rejected" + ); + } + + let mut linkage_mutation = public_inputs.to_vec(); + linkage_mutation[0][0]["call_id"] = Value::String("call_wrong".to_string()); + assert!( + catch_unwind(AssertUnwindSafe(|| { + normalize_flow(public_responses, &linkage_mutation, Projection::Public) + })) + .is_err(), + "call linkage mutation should be rejected" + ); + + let mut namespace_linkage_mutation = public_inputs.to_vec(); + namespace_linkage_mutation[2][0]["call_id"] = Value::String("call_wrong".to_string()); + assert!( + catch_unwind(AssertUnwindSafe(|| { + normalize_flow(public_responses, &namespace_linkage_mutation, Projection::Public) + })) + .is_err(), + "namespace-member call linkage mutation should be rejected" + ); +} + +#[test] +fn raw_semantic_normalization_ignores_provider_ids_usage_and_wire_projection() { + let returned_tools = mixed_loaded_tool_definitions(); + let (public_responses, public_inputs) = public_semantic_fixture(&returned_tools); + let (normalized_responses, normalized_inputs) = normalized_semantic_fixture(&returned_tools); + let public = normalize_flow(&public_responses, &public_inputs, Projection::Public); + let normalized = normalize_flow(&normalized_responses, &normalized_inputs, Projection::Normalized); + assert_eq!(public, normalized); + + assert_eq!( + normalize_flow( + &normalized_responses, + &normalized_manual_inputs(&normalized_responses, &normalized_inputs), + Projection::Normalized, + ), + public, + "manual full-history replay should normalize to the same semantics" + ); + assert_semantic_mutations_are_visible( + &public, + &public_responses, + &public_inputs, + &normalized_responses, + &normalized_inputs, + ); +} + +#[test] +fn offline_sse_terminal_normalization_ignores_event_chunk_grouping() { + let completed = serde_json::json!({ + "type": "response.completed", + "response": { + "id": "resp_terminal", + "output": [{ + "type": "message", + "content": [{"type": "output_text", "text": "PARIS_MIXED_TOOLS_OK"}] + }] + } + }); + let completed_data = format!("data: {completed}"); + let grouped = format!( + "event: response.created\ndata: {{\"type\":\"response.created\"}}\nevent: response.completed\n{completed_data}\n" + ); + let split = [ + "event: response.created", + "data: {\"type\":\"response.created\"}", + "event: response.completed", + completed_data.as_str(), + ]; + + assert_eq!( + terminal_response_from_sse_chunks(&[grouped.as_str()]), + terminal_response_from_sse_chunks(&split) + ); +} + +#[test] +fn named_sse_call_lifecycle_projection_preserves_order_and_linkage() { + let sse = [ + "event: response.output_item.added", + r#"data: {"type":"response.output_item.added","item":{"id":"reasoning_1","type":"reasoning","status":"in_progress"}}"#, + "event: response.output_item.added", + r#"data: {"type":"response.output_item.added","item":{"id":"function_1","type":"function_call","name":"fixture_call","call_id":"call_1","status":"in_progress","arguments":""}}"#, + "event: response.function_call_arguments.delta", + r#"data: {"type":"response.function_call_arguments.delta","item_id":"function_1","delta":"{\"city\":"}"#, + "event: response.function_call_arguments.done", + r#"data: {"type":"response.function_call_arguments.done","item_id":"function_1","arguments":"{\"city\":\"Paris\"}"}"#, + "event: response.output_item.done", + r#"data: {"type":"response.output_item.done","item":{"id":"function_1","type":"function_call","name":"fixture_call","call_id":"call_1","status":"completed","arguments":"{\"city\":\"Paris\"}"}}"#, + ]; + + assert_eq!( + relevant_call_lifecycle_from_named_sse(sse), + vec![ + RelevantCallLifecycle { + event_type: "response.output_item.added".to_string(), + status: Some("in_progress".to_string()), + arguments: Some(Value::String(String::new())), + execution: None, + item_id: Some("function_1".to_string()), + call_id: Some("call_1".to_string()), + }, + RelevantCallLifecycle { + event_type: "response.function_call_arguments.delta".to_string(), + status: None, + arguments: Some(Value::String("{\"city\":".to_string())), + execution: None, + item_id: Some("function_1".to_string()), + call_id: Some("call_1".to_string()), + }, + RelevantCallLifecycle { + event_type: "response.function_call_arguments.done".to_string(), + status: None, + arguments: Some(Value::String("{\"city\":\"Paris\"}".to_string())), + execution: None, + item_id: Some("function_1".to_string()), + call_id: Some("call_1".to_string()), + }, + RelevantCallLifecycle { + event_type: "response.output_item.done".to_string(), + status: Some("completed".to_string()), + arguments: Some(Value::String("{\"city\":\"Paris\"}".to_string())), + execution: None, + item_id: Some("function_1".to_string()), + call_id: Some("call_1".to_string()), + }, + ] + ); +} + +fn assert_reference_and_blocking_response_metadata(directory: &Path) -> (Value, SemanticFlow) { + let openai_streaming = support::load_cassette( + directory + .join(OPENAI_STREAMING_CASSETTE) + .to_str() + .expect("OpenAI streaming cassette path"), + ); + let expected_loaded_tools = expected_loaded_response_tools(directory); + assert_loaded_response_tool_shape(&expected_loaded_tools); + for turn in &openai_streaming.turns[1..] { + assert_post_search_response_metadata(turn, &expected_loaded_tools); + } + + let openai_blocking = support::load_cassette( + directory + .join(OPENAI_BLOCKING_CASSETTE) + .to_str() + .expect("OpenAI blocking cassette path"), + ); + for turn in &openai_blocking.turns[1..] { + let response = terminal_response(turn); + assert_eq!(canonical_response_tools(&response["tools"]), expected_loaded_tools); + assert_eq!(response["tool_choice"], turn.request.body.tool_choice.clone().unwrap()); + } + + let blocking = support::load_cassette( + directory + .join(GATEWAY_BLOCKING_CASSETTE) + .to_str() + .expect("blocking gateway cassette path"), + ); + let blocking_responses = blocking.turns.iter().map(terminal_response).collect::>(); + for (turn, response) in blocking.turns[1..].iter().zip(&blocking_responses[1..]) { + assert_eq!(canonical_response_tools(&response["tools"]), expected_loaded_tools); + assert_eq!(response["tool_choice"], turn.request.body.tool_choice.clone().unwrap()); + } + let blocking_inputs = blocking.turns[1..] + .iter() + .map(|turn| turn.request.body.input.clone()) + .collect::>(); + let blocking_flow = normalize_flow(&blocking_responses, &blocking_inputs, Projection::Public); + (expected_loaded_tools, blocking_flow) +} + +#[test] +fn gateway_http_sse_and_websocket_cassettes_replay_the_public_lifecycle() { + let directory = tool_search_cassette_directory(); + let (expected_loaded_tools, blocking_flow) = assert_reference_and_blocking_response_metadata(&directory); + + for suffix in ["-streaming.yaml", "-websocket.yaml"] { + let path = one_gateway_stream_cassette(&directory, suffix); + let cassette = support::load_cassette(path.to_str().expect("gateway stream cassette path")); + assert_eq!(cassette.turns.len(), 4); + + for turn in &cassette.turns { + let events = support::recorded_named_sse_events(turn); + assert_eq!( + events + .iter() + .map(|event| event["sequence_number"].as_u64()) + .collect::>(), + (0..u64::try_from(events.len()).unwrap()).map(Some).collect::>() + ); + assert!( + events + .iter() + .all(|event| event["type"] != "error" && event["type"] != "response.failed") + ); + assert!(events.iter().all(|event| { + event["response"]["tools"].as_array().is_none_or(|tools| { + tools + .iter() + .all(|tool| !(tool["type"] == "function" && tool["name"] == "tool_search")) + }) + })); + } + for turn in &cassette.turns[1..] { + assert_post_search_response_metadata(turn, &expected_loaded_tools); + } + + let first_events = support::recorded_named_sse_events(&cassette.turns[0]); + assert!(first_events.iter().any(|event| { + event["response"]["tools"].as_array().is_some_and(|tools| { + tools + .iter() + .any(|tool| tool["type"] == "tool_search" && tool["execution"] == "client") + && tools + .iter() + .any(|tool| tool["name"] == "get_weather" && tool["defer_loading"] == true) + && tools + .iter() + .filter(|tool| tool["type"] == "function" && tool["defer_loading"] == true) + .count() + == 3 + && tools.iter().any(|tool| { + tool["type"] == "namespace" + && tool["name"] == "travel" + && tool["tools"].as_array().is_some_and(|members| members.len() == 3) + }) + }) + })); + assert!(first_events.iter().all(|event| { + !(matches!( + event["type"].as_str(), + Some("response.function_call_arguments.delta" | "response.function_call_arguments.done") + ) || matches!( + event["type"].as_str(), + Some("response.output_item.added" | "response.output_item.done") + ) && event["item"]["type"] == "function_call" + && event["item"]["name"] == "tool_search") + })); + let lifecycle = first_events + .iter() + .filter(|event| { + matches!( + event["type"].as_str(), + Some("response.output_item.added" | "response.output_item.done") + ) && event["item"]["type"] == "tool_search_call" + }) + .collect::>(); + assert_eq!(lifecycle.len(), 2); + assert_eq!(lifecycle[0]["item"]["status"], "in_progress"); + assert_eq!(lifecycle[0]["item"]["arguments"], serde_json::json!({})); + assert_eq!(lifecycle[1]["item"]["status"], "completed"); + assert_eq!(lifecycle[0]["item"]["id"], lifecycle[1]["item"]["id"]); + assert_eq!(lifecycle[0]["item"]["call_id"], lifecycle[1]["item"]["call_id"]); + assert_eq!(lifecycle[0]["output_index"], lifecycle[1]["output_index"]); + + let responses = cassette.turns.iter().map(terminal_response).collect::>(); + let terminal_search = responses[0]["output"] + .as_array() + .expect("first gateway output") + .iter() + .find(|item| item["type"] == "tool_search_call") + .expect("terminal public search call"); + assert_eq!(terminal_search, &lifecycle[1]["item"]); + let inputs = cassette.turns[1..] + .iter() + .map(|turn| turn.request.body.input.clone()) + .collect::>(); + assert_eq!(normalize_flow(&responses, &inputs, Projection::Public), blocking_flow); + } +} + +#[test] +fn openai_streaming_cassette_preserves_public_lifecycle_and_terminal_identity() { + let path = tool_search_cassette_directory().join(OPENAI_STREAMING_CASSETTE); + let cassette = support::load_cassette(path.to_str().expect("cassette path should be UTF-8")); + assert_eq!(cassette.turns.len(), 4); + + let search_events = support::recorded_named_sse_events(&cassette.turns[0]); + assert!( + search_events + .iter() + .all(|event| !matches!(event["type"].as_str(), Some("error" | "response.failed"))) + ); + let search_lifecycle = relevant_call_lifecycle_from_named_sse( + cassette.turns[0] + .response + .sse + .as_ref() + .expect("streaming cassette should contain SSE") + .iter() + .flat_map(|entry| entry.lines()), + ); + assert_eq!( + search_lifecycle + .iter() + .map(|event| event.event_type.as_str()) + .collect::>(), + ["response.output_item.added", "response.output_item.done"] + ); + assert_eq!(search_lifecycle[0].status.as_deref(), Some("in_progress")); + assert_eq!(search_lifecycle[1].status.as_deref(), Some("completed")); + assert_eq!(search_lifecycle[0].execution.as_deref(), Some("client")); + assert_eq!(search_lifecycle[1].execution.as_deref(), Some("client")); + assert_eq!(search_lifecycle[0].arguments, Some(serde_json::json!({}))); + assert!( + search_lifecycle[1] + .arguments + .as_ref() + .and_then(|arguments| arguments["query"].as_str()) + .is_some_and(|query| !query.trim().is_empty()) + ); + assert_eq!(search_lifecycle[0].item_id, search_lifecycle[1].item_id); + assert_eq!(search_lifecycle[0].call_id, search_lifecycle[1].call_id); + + let (loaded_item_id, loaded_call_id, loaded_arguments) = assert_observed_call_lifecycle(&cassette.turns[1]); + assert_eq!(loaded_arguments, serde_json::json!({"city": "Paris"})); + let (namespace_item_id, namespace_call_id, namespace_arguments) = + assert_observed_call_lifecycle(&cassette.turns[2]); + assert_eq!(namespace_arguments, serde_json::json!({"city": "Paris"})); + + let responses = cassette.turns.iter().map(terminal_response).collect::>(); + let terminal_search_call = responses[0]["output"] + .as_array() + .expect("turn one terminal output should be an array") + .iter() + .find(|item| item["type"] == "tool_search_call") + .expect("turn one terminal response should contain tool_search_call"); + assert_eq!( + terminal_search_call["id"].as_str(), + search_lifecycle[1].item_id.as_deref(), + "OpenAI preserves the search item ID into terminal output" + ); + assert_eq!( + terminal_search_call["call_id"].as_str(), + search_lifecycle[1].call_id.as_deref(), + "OpenAI preserves the search call ID into terminal output" + ); + assert_eq!(terminal_search_call["execution"], "client"); + assert_eq!(terminal_search_call["status"], "completed"); + assert_eq!( + Some(&terminal_search_call["arguments"]), + search_lifecycle[1].arguments.as_ref() + ); + + let terminal_loaded_call = responses[1]["output"] + .as_array() + .expect("turn two terminal output should be an array") + .iter() + .find(|item| item["type"] == "function_call" && item["name"] == "get_weather") + .expect("turn two terminal response should contain get_weather"); + assert_eq!(terminal_loaded_call["id"].as_str(), Some(loaded_item_id.as_str())); + assert_eq!(terminal_loaded_call["call_id"].as_str(), Some(loaded_call_id.as_str())); + + let terminal_namespace_call = responses[2]["output"] + .as_array() + .expect("turn three terminal output should be an array") + .iter() + .find(|item| item["type"] == "function_call" && item["namespace"] == "travel" && item["name"] == "get_timezone") + .expect("turn three terminal response should contain travel.get_timezone"); + assert_eq!(terminal_namespace_call["id"].as_str(), Some(namespace_item_id.as_str())); + assert_eq!( + terminal_namespace_call["call_id"].as_str(), + Some(namespace_call_id.as_str()) + ); + + let final_events = support::recorded_named_sse_events(&cassette.turns[3]); + assert!( + final_events + .iter() + .all(|event| !matches!(event["type"].as_str(), Some("error" | "response.failed"))) + ); + let continuation_inputs = cassette.turns[1..] + .iter() + .map(|turn| turn.request.body.input.clone()) + .collect::>(); + let semantic = normalize_flow(&responses, &continuation_inputs, Projection::Public); + assert_eq!(semantic.final_text.trim(), "PARIS_MIXED_TOOLS_OK"); +} + +#[test] +fn direct_vllm_streaming_cassette_characterizes_lifecycle_and_terminal_identity_mismatch() { + let path = tool_search_cassette_directory().join(DIRECT_VLLM_STREAMING_CASSETTE); + let cassette = support::load_cassette(path.to_str().expect("cassette path should be UTF-8")); + assert_eq!(cassette.turns.len(), 4); + + let (search_lifecycle_item_id, search_lifecycle_call_id, search_arguments) = + assert_observed_call_lifecycle(&cassette.turns[0]); + assert!( + search_arguments["query"] + .as_str() + .is_some_and(|query| !query.trim().is_empty()) + ); + let (loaded_lifecycle_item_id, loaded_lifecycle_call_id, loaded_arguments) = + assert_observed_call_lifecycle(&cassette.turns[1]); + assert_eq!(loaded_arguments, serde_json::json!({"city": "Paris"})); + let (namespace_lifecycle_item_id, namespace_lifecycle_call_id, namespace_arguments) = + assert_observed_call_lifecycle(&cassette.turns[2]); + assert_eq!(namespace_arguments, serde_json::json!({"city": "Paris"})); + + let final_events = support::recorded_named_sse_events(&cassette.turns[3]); + assert!( + final_events + .iter() + .all(|event| !matches!(event["type"].as_str(), Some("error" | "response.failed"))) + ); + let final_completed = final_events + .iter() + .filter(|event| event["type"] == "response.completed") + .collect::>(); + assert_eq!(final_completed.len(), 1); + assert_eq!(final_completed[0]["response"]["status"], "completed"); + let final_lifecycle = relevant_call_lifecycle_from_named_sse( + cassette.turns[3] + .response + .sse + .as_ref() + .expect("streaming cassette should contain SSE") + .iter() + .flat_map(|entry| entry.lines()), + ); + assert!( + final_lifecycle.is_empty(), + "final turn must not contain a client call lifecycle" + ); + + let responses = cassette.turns.iter().map(terminal_response).collect::>(); + let terminal_search_call = responses[0]["output"] + .as_array() + .expect("turn one terminal output should be an array") + .iter() + .find(|item| item["type"] == "function_call" && item["name"] == "tool_search") + .expect("turn one terminal response should contain tool_search"); + let terminal_loaded_call = responses[1]["output"] + .as_array() + .expect("turn two terminal output should be an array") + .iter() + .find(|item| item["type"] == "function_call" && item["name"] == "get_weather") + .expect("turn two terminal response should contain get_weather"); + let terminal_namespace_call = responses[2]["output"] + .as_array() + .expect("turn three terminal output should be an array") + .iter() + .find(|item| item["type"] == "function_call" && item["name"] == "agentic_ns__travel__get_timezone") + .expect("turn three terminal response should contain flattened travel.get_timezone"); + assert_ne!( + terminal_search_call["id"].as_str(), + Some(search_lifecycle_item_id.as_str()), + "observed vLLM stream regenerates the terminal search item ID" + ); + assert_ne!( + terminal_search_call["call_id"].as_str(), + Some(search_lifecycle_call_id.as_str()), + "observed vLLM stream regenerates the terminal search call ID" + ); + assert_ne!( + terminal_loaded_call["id"].as_str(), + Some(loaded_lifecycle_item_id.as_str()), + "observed vLLM stream regenerates the terminal loaded-function item ID" + ); + assert_ne!( + terminal_loaded_call["call_id"].as_str(), + Some(loaded_lifecycle_call_id.as_str()), + "observed vLLM stream regenerates the terminal loaded-function call ID" + ); + assert_ne!( + terminal_namespace_call["id"].as_str(), + Some(namespace_lifecycle_item_id.as_str()), + "observed vLLM stream regenerates the terminal namespace-member item ID" + ); + assert_ne!( + terminal_namespace_call["call_id"].as_str(), + Some(namespace_lifecycle_call_id.as_str()), + "observed vLLM stream regenerates the terminal namespace-member call ID" + ); + + let continuation_inputs = cassette.turns[1..] + .iter() + .map(|turn| turn.request.body.input.clone()) + .collect::>(); + let semantic = normalize_flow(&responses, &continuation_inputs, Projection::Normalized); + assert_eq!(semantic.final_text.trim(), "PARIS_MIXED_TOOLS_OK"); +} + +const PROVIDER_PARITY_CASSETTES: [&str; 7] = [ + OPENAI_BLOCKING_CASSETTE, + OPENAI_STREAMING_CASSETTE, + DIRECT_VLLM_BLOCKING_CASSETTE, + DIRECT_VLLM_STREAMING_CASSETTE, + GATEWAY_BLOCKING_CASSETTE, + GATEWAY_STREAMING_CASSETTE, + GATEWAY_WEBSOCKET_CASSETTE, +]; + +fn provider_projection(filename: &str) -> Projection { + if filename.contains("openai-reference") || filename.contains("gateway") { + Projection::Public + } else if filename.contains("direct-vllm") { + Projection::Normalized + } else { + panic!("unexpected tool-search characterization cassette: {filename}"); + } +} + +#[cfg(unix)] +fn assert_cassette_is_not_executable(path: &Path, filename: &str) { + use std::os::unix::fs::PermissionsExt as _; + assert_eq!( + fs::metadata(path) + .expect("characterization cassette metadata") + .permissions() + .mode() + & 0o111, + 0, + "checked-in cassette must not be executable: {filename}" + ); +} + +#[cfg(not(unix))] +fn assert_cassette_is_not_executable(_path: &Path, _filename: &str) {} + +fn assert_public_request_projection( + directory: &Path, + filename: &str, + request_bodies: &[&serde_json::Map], + responses: &[Value], +) { + let expected_initial = serde_json::from_str::( + &fs::read_to_string(directory.join("openai_tools.json")).expect("OpenAI tool fixture should be readable"), + ) + .expect("OpenAI tool fixture should be valid JSON"); + assert_eq!(request_bodies[0].get("tools"), Some(&expected_initial)); + let tool_choice_fixture = if filename.contains("openai-reference") { + "openai_tool_choice_sequence.json" + } else { + "gateway_tool_choice_sequence.json" + }; + assert_tool_choice_sequence(directory, tool_choice_fixture, request_bodies); + if filename == GATEWAY_BLOCKING_CASSETTE { + assert!( + request_bodies + .iter() + .all(|body| body.get("store") == Some(&Value::Bool(false))) + ); + assert!( + request_bodies + .iter() + .all(|body| !body.contains_key("previous_response_id")) + ); + } else { + assert!( + request_bodies + .iter() + .all(|body| body.get("store") == Some(&Value::Bool(true))) + ); + for index in 1..request_bodies.len() { + assert_eq!( + request_bodies[index].get("previous_response_id"), + responses[index - 1].get("id") + ); + } + } + assert!(request_bodies[1..].iter().all(|body| !body.contains_key("tools"))); +} + +fn assert_normalized_request_projection( + directory: &Path, + request_bodies: &[&serde_json::Map], + responses: &[Value], +) { + let expected_initial = serde_json::from_str::( + &fs::read_to_string(directory.join("vllm_initial_tools.json")) + .expect("vLLM initial tool fixture should be readable"), + ) + .expect("vLLM initial tool fixture should be valid JSON"); + let expected_next = serde_json::from_str::( + &fs::read_to_string(directory.join("vllm_tools_after_search.json")) + .expect("vLLM post-search tool fixture should be readable"), + ) + .expect("vLLM post-search tool fixture should be valid JSON"); + assert_eq!(request_bodies[0].get("tools"), Some(&expected_initial)); + assert_tool_choice_sequence(directory, "vllm_tool_choice_sequence.json", request_bodies); + assert!( + request_bodies[1..] + .iter() + .all(|body| body.get("tools") == Some(&expected_next)) + ); + assert!( + request_bodies + .iter() + .all(|body| body.get("store") == Some(&Value::Bool(false))) + ); + assert!( + request_bodies + .iter() + .all(|body| !body.contains_key("previous_response_id")) + ); + + let inputs = request_bodies + .iter() + .map(|body| { + body["input"] + .as_array() + .expect("manual replay input should be an array") + }) + .collect::>(); + let mut expected_second_prefix = inputs[0].clone(); + expected_second_prefix.extend( + responses[0]["output"] + .as_array() + .expect("turn one output should be an array") + .iter() + .cloned(), + ); + assert_eq!( + &inputs[1][..expected_second_prefix.len()], + expected_second_prefix.as_slice() + ); + let mut expected_third_prefix = inputs[1].clone(); + expected_third_prefix.extend( + responses[1]["output"] + .as_array() + .expect("turn two output should be an array") + .iter() + .cloned(), + ); + assert_eq!( + &inputs[2][..expected_third_prefix.len()], + expected_third_prefix.as_slice() + ); + let mut expected_fourth_prefix = inputs[2].clone(); + expected_fourth_prefix.extend( + responses[2]["output"] + .as_array() + .expect("turn three output should be an array") + .iter() + .cloned(), + ); + assert_eq!( + &inputs[3][..expected_fourth_prefix.len()], + expected_fourth_prefix.as_slice() + ); +} + +fn assert_tool_choice_sequence(directory: &Path, filename: &str, request_bodies: &[&serde_json::Map]) { + let expected = fixture_json(directory, filename); + let expected = expected + .as_array() + .expect("tool-choice sequence fixture should be an array"); + assert_eq!(request_bodies.len(), expected.len()); + for (body, choice) in request_bodies.iter().zip(expected) { + assert_eq!(body.get("tool_choice"), Some(choice)); + assert_eq!(body.get("parallel_tool_calls"), Some(&Value::Bool(false))); + } +} + +fn assert_request_projection( + directory: &Path, + filename: &str, + raw_document: &Value, + responses: &[Value], + projection: Projection, +) { + let request_bodies = raw_document["turns"] + .as_array() + .expect("raw cassette should contain turns") + .iter() + .map(|turn| { + turn["request"]["body"] + .as_object() + .expect("recorded request body should be an object") + }) + .collect::>(); + match projection { + Projection::Public => assert_public_request_projection(directory, filename, &request_bodies, responses), + Projection::Normalized => assert_normalized_request_projection(directory, &request_bodies, responses), + } +} + +fn normalize_provider_cassette(directory: &Path, filename: &str) -> SemanticFlow { + let path = directory.join(filename); + assert!( + path.is_file(), + "required provider parity cassette is missing: {filename}" + ); + assert_cassette_is_not_executable(&path, filename); + let raw_text = fs::read_to_string(&path).expect("characterization cassette should be readable"); + let raw_document = serde_yaml::from_str::(&raw_text).expect("characterization YAML should be valid"); + let cassette = support::load_cassette(path.to_str().expect("cassette path should be UTF-8")); + assert_eq!(cassette.turns.len(), 4, "{filename} should contain four turns"); + let responses = cassette.turns.iter().map(terminal_response).collect::>(); + let inputs = cassette.turns[1..] + .iter() + .map(|turn| turn.request.body.input.clone()) + .collect::>(); + let projection = provider_projection(filename); + assert_request_projection(directory, filename, &raw_document, &responses, projection); + normalize_flow(&responses, &inputs, projection) +} + +#[test] +fn provider_parity_recorder_generated_matrix_has_one_semantic_flow() { + let directory = tool_search_cassette_directory(); + let expected_tools = serde_json::from_str::( + &fs::read_to_string(directory.join("returned_tools.json")).expect("returned tool fixture should be readable"), + ) + .expect("returned tool fixture should be valid JSON"); + let expected_outputs = serde_json::from_str::( + &fs::read_to_string(directory.join("function_outputs.json")) + .expect("function-output fixture should be readable"), + ) + .expect("function-output fixture should be valid JSON"); + let mut reference = None; + for filename in PROVIDER_PARITY_CASSETTES { + let semantic = normalize_provider_cassette(&directory, filename); + assert_eq!( + semantic.returned_tools, expected_tools, + "returned tool drift in {filename}" + ); + assert_eq!( + semantic.loaded_calls[0].function_output, expected_outputs["get_weather"], + "ordinary function-output drift in {filename}" + ); + assert_eq!( + semantic.loaded_calls[1].function_output, expected_outputs["get_timezone"], + "namespace-member function-output drift in {filename}" + ); + assert_eq!(semantic.final_text.trim(), "PARIS_MIXED_TOOLS_OK"); + if let Some(expected) = &reference { + assert_eq!(&semantic, expected, "semantic provider drift in {filename}"); + } else { + reference = Some(semantic); + } + } +} diff --git a/crates/agentic-server-core/tests/tool_search_state_test.rs b/crates/agentic-server-core/tests/tool_search_state_test.rs new file mode 100644 index 00000000..8bc16be4 --- /dev/null +++ b/crates/agentic-server-core/tests/tool_search_state_test.rs @@ -0,0 +1,1302 @@ +use agentic_core::tool::{ToolSearchState, model_visible_namespace_member_name}; +use agentic_core::{InputItem, RequestPayload, ResponsesInput}; +use serde_json::{Value, json}; + +fn request(tools: Value, input: Value) -> RequestPayload { + let mut value = json!({ + "model": "test-model", + "store": false, + "parallel_tool_calls": false + }); + value["input"] = input; + value["tools"] = tools; + serde_json::from_value(value).expect("test request must match the public wire model") +} + +fn search_declaration() -> Value { + json!({ + "type": "tool_search", + "execution": "client", + "description": "Search the client tool catalog", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + "additionalProperties": false + } + }) +} + +fn function(name: &str, description: &str, city_type: &str, deferred: bool) -> Value { + json!({ + "type": "function", + "name": name, + "description": description, + "parameters": { + "type": "object", + "properties": {"city": {"type": city_type}}, + "required": ["city"], + "additionalProperties": false + }, + "strict": true, + "defer_loading": deferred + }) +} + +fn search_call(id: &str) -> Value { + json!({ + "type": "tool_search_call", + "id": format!("tsc_{id}"), + "call_id": id, + "arguments": {"query": "weather"} + }) +} + +fn search_output(id: &str, tools: Vec) -> Value { + let mut value = json!({ + "type": "tool_search_output", + "call_id": id + }); + value["tools"] = Value::Array(tools); + value +} + +fn tool_values(tools: Option<&[agentic_core::ResponsesTool]>) -> Value { + serde_json::to_value(tools).expect("prepared tools serialize") +} + +fn private_request(state: &mut ToolSearchState, public: &RequestPayload) -> RequestPayload { + let mut private = public.clone(); + state + .prepare_inference_request(&mut private) + .expect("prepared state materializes a private inference request"); + private +} + +fn private_tool_values(state: &mut ToolSearchState, public: &RequestPayload) -> Value { + let private = private_request(state, public); + tool_values(private.tools.as_deref()) +} + +fn private_input_value(state: &mut ToolSearchState, public: &RequestPayload) -> Value { + serde_json::to_value(private_request(state, public).input).expect("private input serializes") +} + +fn synthetic_description(state: &ToolSearchState) -> &str { + state + .synthetic_tool_search() + .and_then(|function| function.description.as_deref()) + .expect("active tool search has a synthetic description") +} + +#[test] +fn fresh_and_sequential_state_has_distinct_deterministic_views() { + let deferred = function("get_weather", "Get weather", "string", true); + let dynamic = function("get_uv", "Get UV index", "string", true); + let tools = json!([ + search_declaration(), + { + "type": "function", + "name": "current_time", + "description": "Get current time", + "parameters": {"type": "object"} + }, + deferred + ]); + let input = json!([ + search_call("call_search_1"), + search_output("call_search_1", vec![dynamic.clone()]), + search_call("call_search_2"), + search_output("call_search_2", vec![dynamic.clone()]) + ]); + let request = request(tools, input); + + let mut state = ToolSearchState::build(&request).expect("valid ordered history"); + let mut rebuilt = ToolSearchState::build(&request).expect("same request builds again"); + let private = private_tool_values(&mut state, &request); + let rebuilt_private = private_tool_values(&mut rebuilt, &request); + + assert!(state.is_active()); + assert_eq!( + serde_json::to_string(&( + tool_values(state.public_effective_tools()), + &private, + tool_values(Some(state.loaded_public_tools())), + serde_json::to_value(state.synthetic_tool_search()).expect("synthetic declaration serializes") + )) + .expect("state snapshot serializes"), + serde_json::to_string(&( + tool_values(rebuilt.public_effective_tools()), + &rebuilt_private, + tool_values(Some(rebuilt.loaded_public_tools())), + serde_json::to_value(rebuilt.synthetic_tool_search()).expect("synthetic declaration serializes") + )) + .expect("rebuilt snapshot serializes") + ); + + let public = tool_values(state.public_effective_tools()); + assert_eq!(public.as_array().map(Vec::len), Some(4)); + assert_eq!(public[2]["defer_loading"], true, "public deferral must be preserved"); + assert_eq!( + public[3], dynamic, + "a dynamic definition absent initially is appended once" + ); + + let loaded = tool_values(Some(state.loaded_public_tools())); + assert_eq!(loaded, json!([dynamic]), "an exact repeated definition is idempotent"); + + assert_eq!(private.as_array().map(Vec::len), Some(3)); + assert_eq!(private[0]["type"], "tool_search"); + assert_eq!(private[0]["execution"], "client"); + assert_eq!(private[1]["name"], "current_time"); + assert_eq!(private[2]["name"], "get_uv"); + assert!(private[2].get("defer_loading").is_none()); + assert!( + private + .as_array() + .expect("private tools") + .iter() + .all(|tool| tool["name"] != "get_weather"), + "unloaded deferred schemas must not enter the private view" + ); + + assert_eq!( + serde_json::to_value(state.synthetic_tool_search()).expect("synthetic declaration serializes"), + json!({ + "execution": "client", + "description": "Search the client tool catalog. Available catalog entry: get_weather — Get weather.", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + "additionalProperties": false + } + }) + ); +} + +#[test] +fn optional_declaration_fields_get_private_defaults_and_supplied_values_are_preserved() { + let default_parameters = json!({ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "A concise description of the needed capabilities." + } + }, + "required": ["query"], + "additionalProperties": false + }); + let minimal = request( + json!([{"type": "tool_search", "execution": "client"}]), + json!("find a weather tool"), + ); + let minimal_state = ToolSearchState::build(&minimal).expect("minimal declaration reaches state preparation"); + let minimal_synthetic = serde_json::to_value(minimal_state.synthetic_tool_search()).expect("synthetic serializes"); + assert_eq!(minimal_synthetic["description"], "Search the client tool catalog"); + assert_eq!(minimal_synthetic["parameters"], default_parameters); + + let supplied_declaration = search_declaration(); + let supplied = request(json!([supplied_declaration.clone()]), json!("find a weather tool")); + let supplied_state = ToolSearchState::build(&supplied).expect("supplied declaration reaches state preparation"); + let supplied_synthetic = + serde_json::to_value(supplied_state.synthetic_tool_search()).expect("synthetic serializes"); + assert_eq!(supplied_synthetic["description"], supplied_declaration["description"]); + assert_eq!(supplied_synthetic["parameters"], supplied_declaration["parameters"]); + + let supplied_parameters = json!({"type": "object", "properties": {"term": {"type": "string"}}}); + let blank_description = request( + json!([{ + "type": "tool_search", + "execution": "client", + "description": " ", + "parameters": supplied_parameters.clone() + }]), + json!("find a weather tool"), + ); + let state = ToolSearchState::build(&blank_description).expect("blank description is normalized privately"); + let synthetic = serde_json::to_value(state.synthetic_tool_search()).expect("synthetic serializes"); + assert_eq!(synthetic["description"], "Search the client tool catalog"); + assert_eq!(synthetic["parameters"], supplied_parameters); + + for invalid_parameters in [json!({}), json!({"type": "array"})] { + let invalid_schema = request( + json!([{ + "type": "tool_search", + "execution": "client", + "description": "Find exactly the needed tool", + "parameters": invalid_parameters + }]), + json!("find a weather tool"), + ); + let state = ToolSearchState::build(&invalid_schema).expect("invalid schema is normalized privately"); + let synthetic = serde_json::to_value(state.synthetic_tool_search()).expect("synthetic serializes"); + assert_eq!(synthetic["description"], "Find exactly the needed tool"); + assert_eq!(synthetic["parameters"], default_parameters); + } +} + +#[test] +fn only_completed_search_outputs_load_definitions() { + let deferred = function("get_weather", "Get weather", "string", true); + + for status in ["in_progress", "incomplete"] { + let mut output = search_output("call_search_1", vec![deferred.clone()]); + output["status"] = Value::String(status.to_owned()); + let public = request( + json!([search_declaration(), deferred.clone()]), + json!([search_call("call_search_1"), output]), + ); + let error = ToolSearchState::build(&public).expect_err("a non-completed output must not load definitions"); + assert!( + error + .to_string() + .contains("must be completed before it may load tool definitions") + ); + } + + let mut output = search_output("call_search_1", vec![deferred.clone()]); + output["status"] = json!("completed"); + let completed = request( + json!([search_declaration(), deferred.clone()]), + json!([search_call("call_search_1"), output]), + ); + let state = ToolSearchState::build(&completed).expect("a completed output loads definitions"); + assert_eq!(tool_values(Some(state.loaded_public_tools())), json!([deferred])); +} + +#[test] +fn replayed_search_calls_accept_documented_statuses() { + let deferred = function("get_weather", "Get weather", "string", true); + + for status in ["in_progress", "completed", "incomplete"] { + let mut call = search_call("call_search_1"); + call["status"] = Value::String(status.to_owned()); + let public = request( + json!([search_declaration(), deferred.clone()]), + json!([call, search_output("call_search_1", vec![deferred.clone()])]), + ); + + let state = ToolSearchState::build(&public).expect("documented replayed call status is accepted"); + assert_eq!(tool_values(Some(state.loaded_public_tools())), json!([deferred])); + } +} + +#[test] +fn one_history_pass_prepares_canonical_private_input_without_mutating_public_input() { + let returned = function("get_weather", "Get weather", "string", true); + let request = request( + json!([search_declaration()]), + json!([ + {"role": "user", "content": "find weather"}, + { + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "arguments": {"z": 2, "a": 1} + }, + search_output("call_search_1", vec![returned.clone()]) + ]), + ); + let public_before = serde_json::to_value(&request.input).expect("public input serializes"); + + let mut state = ToolSearchState::build(&request).expect("matching history prepares once"); + + assert_eq!( + serde_json::to_value(&request.input).expect("public input remains serializable"), + public_before, + "the public request must not be rewritten" + ); + assert_eq!( + private_input_value(&mut state, &request), + json!([ + {"type": "message", "role": "user", "content": "find weather"}, + { + "type": "function_call", + "id": "tsc_1", + "call_id": "call_search_1", + "name": "tool_search", + "arguments": "{\"a\":1,\"z\":2}", + "status": "completed" + }, + { + "type": "function_call_output", + "call_id": "call_search_1", + "output": format!("{{\"tools\":[{}]}}", serde_json::to_string(&returned).unwrap()) + } + ]) + ); +} + +#[test] +fn invalid_history_order_and_linkage_are_rejected() { + let deferred = function("get_weather", "Get weather", "string", true); + let base_tools = json!([search_declaration(), deferred.clone()]); + let mut blank_call = request(base_tools.clone(), json!([search_call("call_search_1")])); + let ResponsesInput::Items(items) = &mut blank_call.input else { + panic!("test request uses item input") + }; + let InputItem::ToolSearchCall(call) = &mut items[0] else { + panic!("test request starts with search call") + }; + call.call_id.clear(); + let mut blank_output = request( + base_tools.clone(), + json!([search_call("call_search_1"), search_output("call_search_1", vec![])]), + ); + let ResponsesInput::Items(items) = &mut blank_output.input else { + panic!("test request uses item input") + }; + let InputItem::ToolSearchOutput(output) = &mut items[1] else { + panic!("test request ends with search output") + }; + output.call_id.clear(); + + let cases = [ + ( + "orphan output", + request(base_tools.clone(), json!([search_output("call_search_1", vec![])])), + ), + ( + "output before call", + request( + base_tools.clone(), + json!([search_output("call_search_1", vec![]), search_call("call_search_1")]), + ), + ), + ( + "mismatched call id", + request( + base_tools.clone(), + json!([search_call("call_search_1"), search_output("call_search_2", vec![])]), + ), + ), + ( + "ambiguous nested call", + request( + base_tools.clone(), + json!([search_call("call_search_1"), search_call("call_search_2")]), + ), + ), + ( + "duplicate output", + request( + base_tools.clone(), + json!([ + search_call("call_search_1"), + search_output("call_search_1", vec![]), + search_output("call_search_1", vec![]) + ]), + ), + ), + ( + "unresolved call", + request(base_tools.clone(), json!([search_call("call_search_1")])), + ), + ("empty call id", blank_call), + ("empty output call id", blank_output), + ]; + + for (case, request) in cases { + assert!(ToolSearchState::build(&request).is_err(), "{case} must be rejected"); + } +} + +#[test] +fn invalid_loaded_definitions_and_normalized_collisions_are_rejected() { + let deferred = function("get_weather", "Get weather", "string", true); + let changed_schema = function("get_weather", "Get weather", "integer", true); + let base_tools = json!([search_declaration(), deferred]); + let cases = [ + ( + "schema conflict", + request( + base_tools.clone(), + json!([ + search_call("call_search_1"), + search_output("call_search_1", vec![changed_schema]) + ]), + ), + ), + ( + "cross-kind identity conflict", + request( + base_tools.clone(), + json!([ + search_call("call_search_1"), + search_output( + "call_search_1", + vec![json!({ + "type": "mcp", + "server_label": "get_weather", + "server_url": "https://mcp.example.test/mcp", + "defer_loading": true + })] + ) + ]), + ), + ), + ( + "reserved synthetic name", + request( + base_tools, + json!([ + search_call("call_search_1"), + search_output( + "call_search_1", + vec![function("tool_search", "Conflict", "string", true)] + ) + ]), + ), + ), + ( + "normalized namespace member collision", + request( + json!([ + search_declaration(), + {"type": "function", "name": "agentic_ns__weather__forecast"} + ]), + json!([ + search_call("call_search_1"), + search_output( + "call_search_1", + vec![json!({ + "type": "namespace", + "name": "weather", + "tools": [{ + "type": "function", + "name": "forecast", + "defer_loading": true + }] + })] + ) + ]), + ), + ), + ( + "unsupported dynamically loaded custom tool", + request( + json!([search_declaration()]), + json!([ + search_call("call_search_1"), + search_output("call_search_1", vec![json!({"type": "custom", "name": "unsupported"})]) + ]), + ), + ), + ]; + + for (case, request) in cases { + assert!(ToolSearchState::build(&request).is_err(), "{case} must be rejected"); + } +} + +#[test] +fn initial_and_dynamic_namespace_collisions_are_rejected() { + let initial = json!([ + search_declaration(), + {"type": "namespace", "name": "a__b", "tools": [{"type": "function", "name": "c"}]}, + {"type": "namespace", "name": "a", "tools": [{"type": "function", "name": "b__c"}]} + ]); + assert!( + ToolSearchState::build(&request(initial, json!("find a tool"))).is_err(), + "initial namespace collisions must fail" + ); + + let tools = json!([{ + "type": "namespace", + "name": "weather", + "tools": [{"type": "function", "name": "forecast"}] + }, search_declaration()]); + let input = json!([ + search_call("call_search_1"), + search_output( + "call_search_1", + vec![function( + "agentic_ns__weather__forecast", + "Dynamic function", + "string", + true + )] + ) + ]); + assert!( + ToolSearchState::build(&request(tools, input)).is_err(), + "dynamic namespace collisions must fail" + ); +} + +#[test] +fn loaded_namespace_model_output_is_identity_only_while_private_tools_retain_members() { + let namespace = json!({ + "type": "namespace", + "name": "weather", + "description": "Weather tools", + "namespace_private_extra": "namespace-extra-sentinel", + "tools": [{ + "type": "function", + "name": "forecast", + "description": "Forecast member sentinel", + "parameters": { + "type": "object", + "properties": {"member-schema-sentinel": {"type": "string"}} + }, + "defer_loading": true + }] + }); + let public = request( + json!([search_declaration()]), + json!([ + search_call("call_search_namespace"), + search_output("call_search_namespace", vec![namespace]) + ]), + ); + let mut state = ToolSearchState::build(&public).expect("loaded namespace prepares without transport behavior"); + + let private = private_request(&mut state, &public); + let private_input = serde_json::to_value(&private.input).expect("private input serializes"); + assert_eq!( + private_input[1]["output"], + r#"{"tools":[{"description":"Weather tools","name":"weather","type":"namespace"}]}"# + ); + let private_tools = serde_json::to_string(&private.tools).expect("private tools serialize"); + assert!( + private_tools.contains("forecast"), + "loaded member must remain available to request lowering" + ); + assert!(private_tools.contains("member-schema-sentinel")); + let private_input = private_input.to_string(); + assert!(!private_input.contains("forecast")); + assert!(!private_input.contains("member-schema-sentinel")); + assert!(!private_input.contains("namespace-extra-sentinel")); +} + +#[test] +fn namespace_partial_load_merges_members_and_is_idempotent() { + let namespace = json!({ + "type": "namespace", + "name": "weather", + "description": "Weather tools", + "tools": [ + { + "type": "function", + "name": "current", + "description": "Current conditions", + "parameters": {"type": "object"} + }, + { + "type": "function", + "name": "forecast", + "description": "Weather forecast", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}} + }, + "defer_loading": true + }, + { + "type": "function", + "name": "alerts", + "description": "Weather alerts", + "parameters": {"type": "object"}, + "defer_loading": true + } + ] + }); + let loaded_subset = json!({ + "type": "namespace", + "name": "weather", + "description": "Weather tools", + "tools": [namespace["tools"][1].clone()] + }); + let public_request = request( + json!([search_declaration(), namespace.clone()]), + json!([ + search_call("call_namespace_1"), + search_output("call_namespace_1", vec![loaded_subset.clone()]), + search_call("call_namespace_2"), + search_output("call_namespace_2", vec![loaded_subset.clone()]) + ]), + ); + let mut state = + ToolSearchState::build(&public_request).expect("same-name partial namespace output merges exact members"); + + let public = tool_values(state.public_effective_tools()); + assert_eq!( + public[1], namespace, + "public declaration and member deferral are preserved" + ); + let loaded = tool_values(Some(state.loaded_public_tools())); + assert_eq!(loaded, json!([loaded_subset]), "exact member reload is idempotent"); + let private = private_tool_values(&mut state, &public_request); + assert_eq!(private[1]["tools"].as_array().map(Vec::len), Some(2)); + assert!(private[1]["tools"][0].get("defer_loading").is_none()); + assert!(private[1]["tools"][1].get("defer_loading").is_none()); + assert_eq!( + synthetic_description(&state), + "Search the client tool catalog. Available catalog entry: weather — Weather tools.", + "the remaining deferred member keeps only namespace identity in the catalog" + ); + assert!(!synthetic_description(&state).contains("alerts")); + + assert_namespace_availability_counter_transitions(&namespace, &loaded_subset); + + let mut conflicting_subset = loaded_subset; + conflicting_subset["tools"][0]["parameters"]["properties"]["city"]["type"] = json!("integer"); + assert!( + ToolSearchState::build(&request( + json!([search_declaration(), namespace]), + json!([ + search_call("call_namespace_conflict"), + search_output("call_namespace_conflict", vec![conflicting_subset]) + ]), + )) + .is_err(), + "same member identity with changed schema must conflict" + ); +} + +fn assert_namespace_availability_counter_transitions(namespace: &Value, loaded_subset: &Value) { + let alerts_subset = json!({ + "type": "namespace", + "name": "weather", + "description": "Weather tools", + "tools": [namespace["tools"][2].clone()] + }); + let fully_loaded = ToolSearchState::build(&request( + json!([search_declaration(), namespace]), + json!([ + search_call("call_forecast"), + search_output("call_forecast", vec![(*loaded_subset).clone()]), + search_call("call_forecast_repeat"), + search_output("call_forecast_repeat", vec![(*loaded_subset).clone()]), + search_call("call_alerts"), + search_output("call_alerts", vec![alerts_subset.clone()]), + search_call("call_alerts_repeat"), + search_output("call_alerts_repeat", vec![alerts_subset]) + ]), + )) + .expect("each deferred member transition is counted once"); + assert_eq!(synthetic_description(&fully_loaded), "Search the client tool catalog"); + assert_eq!( + fully_loaded.loaded_public_tools().len(), + 2, + "an exact repeat neither decrements availability twice nor duplicates the loaded subset" + ); +} + +#[test] +fn active_namespace_validation_reserves_catalog_identity_and_rejects_unsupported_shapes() { + let invalid = [ + json!({ + "type": "namespace", + "name": "tool_search", + "description": "Reserved catalog identity", + "tools": [{"type": "function", "name": "run", "defer_loading": true}] + }), + json!({"type": "namespace", "name": "empty", "tools": []}), + json!({ + "type": "namespace", + "name": "unknown_member", + "tools": [{"type": "future_member", "opaque": true}] + }), + ]; + for namespace in invalid { + assert!( + ToolSearchState::build(&request(json!([search_declaration(), namespace]), json!("find a tool"),)).is_err() + ); + } + + assert!( + ToolSearchState::build(&request( + json!([search_declaration()]), + json!([ + search_call("call_reserved_namespace"), + search_output( + "call_reserved_namespace", + vec![json!({ + "type": "namespace", + "name": "tool_search", + "tools": [{"type": "function", "name": "run"}] + })] + ) + ]), + )) + .is_err(), + "a dynamically returned namespace catalog identity is also reserved" + ); + + let ordinary = request( + json!([{ + "type": "namespace", + "name": "tool_search", + "tools": [{"type": "function", "name": "run"}] + }]), + json!("ordinary namespace"), + ); + assert!( + ToolSearchState::build(&ordinary).is_ok(), + "the namespace identity is reserved only while tool search is active" + ); +} + +#[test] +fn namespace_tool_choice_rejects_withheld_member_and_accepts_loaded_member() { + let namespace = json!({ + "type": "namespace", + "name": "weather", + "tools": [{ + "type": "function", + "name": "forecast", + "parameters": {"type": "object"}, + "defer_loading": true + }] + }); + let mut withheld = request(json!([search_declaration(), namespace.clone()]), json!("find weather")); + withheld.tool_choice = Some( + serde_json::from_value(json!({"type": "function", "namespace": "weather", "name": "forecast"})) + .expect("namespaced choice"), + ); + let mut state = ToolSearchState::build(&withheld).expect("state preparation succeeds before readiness check"); + let mut private = withheld.clone(); + let error = state + .prepare_inference_request(&mut private) + .expect_err("withheld namespace member cannot be forced"); + assert!(error.to_string().contains("before its definition is loaded")); + + let mut loaded = request( + json!([search_declaration(), namespace.clone()]), + json!([ + search_call("call_namespace"), + search_output("call_namespace", vec![namespace]) + ]), + ); + loaded.tool_choice.clone_from(&withheld.tool_choice); + let mut state = ToolSearchState::build(&loaded).expect("exact namespace member loads"); + let private = private_request(&mut state, &loaded); + let upstream = serde_json::to_value(private.to_upstream_request(false).expect("private request lowers")) + .expect("upstream request serializes"); + assert_eq!( + upstream["tool_choice"]["name"], + model_visible_namespace_member_name("weather", "forecast") + ); + assert!(upstream["tool_choice"].get("namespace").is_none()); +} + +#[test] +fn namespace_history_rejects_exact_withheld_calls_and_lowers_loaded_calls() { + let namespace = json!({ + "type": "namespace", + "name": "weather", + "tools": [{ + "type": "function", + "name": "forecast", + "parameters": {"type": "object"}, + "defer_loading": true + }] + }); + let flat_name = model_visible_namespace_member_name("weather", "forecast"); + for call in [ + json!({ + "type": "function_call", "id": "fc_public", "call_id": "call_public", + "namespace": "weather", "name": "forecast", "arguments": "{}", "status": "completed" + }), + json!({ + "type": "function_call", "id": "fc_flat", "call_id": "call_flat", + "name": flat_name, "arguments": "{}", "status": "completed" + }), + ] { + assert!( + ToolSearchState::build(&request( + json!([search_declaration(), namespace.clone()]), + json!([call]), + )) + .is_err(), + "an exact known-but-withheld history call must fail state preparation" + ); + } + + let loaded_call = json!({ + "type": "function_call", "id": "fc_loaded", "call_id": "call_loaded", + "namespace": "weather", "name": "forecast", "arguments": "{}", "status": "completed" + }); + let loaded_request = request( + json!([search_declaration(), namespace.clone()]), + json!([ + search_call("call_load_namespace"), + search_output("call_load_namespace", vec![namespace.clone()]), + loaded_call + ]), + ); + let mut state = ToolSearchState::build(&loaded_request).expect("loaded known history call remains valid"); + let private = private_request(&mut state, &loaded_request); + let upstream = serde_json::to_value(private.to_upstream_request(false).expect("loaded history lowers")) + .expect("upstream request serializes"); + assert_eq!(upstream["input"][2]["name"], flat_name); + assert!(upstream["input"][2].get("namespace").is_none()); + + let unknown = request( + json!([search_declaration(), namespace]), + json!([{ + "type": "function_call", "id": "fc_unknown", "call_id": "call_unknown", + "namespace": "other", "name": "ordinary", "arguments": "{}", "status": "completed" + }]), + ); + ToolSearchState::build(&unknown).expect("unknown ordinary calls retain existing behavior"); +} + +#[test] +fn top_level_function_availability_follows_ordered_search_history() { + let deferred = function("get_weather", "Get weather", "string", true); + let call = json!({ + "type": "function_call", "id": "fc_weather", "call_id": "call_weather", + "name": "get_weather", "arguments": "{}", "status": "completed" + }); + + assert!( + ToolSearchState::build(&request( + json!([search_declaration(), deferred.clone()]), + json!([call.clone()]), + )) + .is_err(), + "an initially deferred function cannot be called before it is loaded" + ); + + assert!( + ToolSearchState::build(&request( + json!([search_declaration()]), + json!([ + call.clone(), + search_call("call_dynamic"), + search_output("call_dynamic", vec![deferred.clone()]), + ]), + )) + .is_err(), + "a dynamically returned function cannot resolve an earlier call" + ); + + ToolSearchState::build(&request( + json!([search_declaration(), deferred.clone()]), + json!([ + search_call("call_load"), + search_output("call_load", vec![deferred]), + call, + ]), + )) + .expect("the same function is available after its ordered load point"); + + ToolSearchState::build(&request( + json!([search_declaration()]), + json!([{ + "type": "function_call", "id": "fc_unknown", "call_id": "call_unknown", + "name": "ordinary_client_function", "arguments": "{}", "status": "completed" + }]), + )) + .expect("unrelated unknown client functions retain existing behavior"); + + let immediate = function("get_weather", "Get weather", "string", false); + ToolSearchState::build(&request( + json!([search_declaration(), immediate.clone()]), + json!([ + { + "type": "function_call", "id": "fc_immediate", "call_id": "call_immediate", + "name": "get_weather", "arguments": "{}", "status": "completed" + }, + search_call("call_identical"), + search_output("call_identical", vec![immediate]), + ]), + )) + .expect("an immediate function stays available before an identical search result"); +} + +#[test] +fn top_level_function_tool_choices_require_the_definition_to_be_loaded() { + let deferred = function("get_weather", "Get weather", "string", true); + for choice in [ + json!({"type": "function", "name": "get_weather"}), + json!({ + "type": "allowed_tools", + "mode": "required", + "tools": [{"type": "function", "name": "get_weather"}] + }), + ] { + let mut withheld = request(json!([search_declaration(), deferred.clone()]), json!("find weather")); + withheld.tool_choice = Some(serde_json::from_value(choice).expect("function tool choice")); + let mut state = ToolSearchState::build(&withheld).expect("state preparation succeeds before choice validation"); + let mut private = withheld.clone(); + state + .prepare_inference_request(&mut private) + .expect_err("a withheld function cannot be selected"); + } + + let mut loaded = request( + json!([search_declaration(), deferred.clone()]), + json!([ + search_call("call_load_choice"), + search_output("call_load_choice", vec![deferred]), + ]), + ); + loaded.tool_choice = + Some(serde_json::from_value(json!({"type": "function", "name": "get_weather"})).expect("function tool choice")); + let mut state = ToolSearchState::build(&loaded).expect("loaded state"); + private_request(&mut state, &loaded); +} + +#[test] +fn dynamically_returned_namespace_members_start_loaded_without_catalog_debt() { + let dynamic_namespace = json!({ + "type": "namespace", + "name": "dynamic_weather", + "description": "Dynamically returned weather tools", + "tools": [{ + "type": "function", + "name": "forecast", + "parameters": {"type": "object"}, + "defer_loading": true + }] + }); + let public = request( + json!([search_declaration()]), + json!([ + search_call("call_dynamic_namespace"), + search_output("call_dynamic_namespace", vec![dynamic_namespace]) + ]), + ); + let mut state = ToolSearchState::build(&public).expect("dynamically returned namespace members are already loaded"); + + assert_eq!(synthetic_description(&state), "Search the client tool catalog"); + let private = private_tool_values(&mut state, &public); + assert_eq!(private[1]["tools"].as_array().map(Vec::len), Some(1)); + assert!(private[1]["tools"][0].get("defer_loading").is_none()); +} + +#[test] +fn dynamic_namespace_history_rejects_forward_references_but_accepts_valid_order_and_unknowns() { + let dynamic_namespace = json!({ + "type": "namespace", + "name": "dynamic_weather", + "tools": [{ + "type": "function", "name": "forecast", "parameters": {"type": "object"}, + "defer_loading": true + }] + }); + let flat_name = model_visible_namespace_member_name("dynamic_weather", "forecast"); + let public_call = json!({ + "type": "function_call", "id": "fc_public", "call_id": "call_public", + "namespace": "dynamic_weather", "name": "forecast", "arguments": "{}", "status": "completed" + }); + let flat_call = json!({ + "type": "function_call", "id": "fc_flat", "call_id": "call_flat", + "name": flat_name, "arguments": "{}", "status": "completed" + }); + for call in [public_call.clone(), flat_call.clone()] { + assert!( + ToolSearchState::build(&request( + json!([search_declaration()]), + json!([ + call, + search_call("call_dynamic"), + search_output("call_dynamic", vec![dynamic_namespace.clone()]) + ]), + )) + .is_err(), + "a call cannot forward-reference a namespace member loaded later in ordered history" + ); + } + + for call in [public_call, flat_call] { + let valid = request( + json!([search_declaration()]), + json!([ + search_call("call_dynamic"), + search_output("call_dynamic", vec![dynamic_namespace.clone()]), + call + ]), + ); + let mut state = ToolSearchState::build(&valid).expect("output-before-call order is valid"); + let private = private_request(&mut state, &valid); + let upstream = serde_json::to_value(private.to_upstream_request(false).expect("valid history lowers")) + .expect("upstream request serializes"); + assert_eq!(upstream["input"][2]["name"], flat_name); + assert!(upstream["input"][2].get("namespace").is_none()); + } + + for unknown in [ + json!({ + "type": "function_call", "id": "fc_unknown_public", "call_id": "call_unknown_public", + "namespace": "never_loaded", "name": "ordinary", "arguments": "{}", "status": "completed" + }), + json!({ + "type": "function_call", "id": "fc_unknown_flat", "call_id": "call_unknown_flat", + "name": "agentic_ns__never_loaded__ordinary", "arguments": "{}", "status": "completed" + }), + ] { + ToolSearchState::build(&request(json!([search_declaration()]), json!([unknown]))) + .expect("a call that remains unknown retains existing behavior"); + } +} + +#[test] +fn deferred_declarations_do_not_implicitly_synthesize_search() { + let request = request( + json!([function("get_weather", "Get weather", "string", true)]), + json!("find weather"), + ); + + assert!( + ToolSearchState::build(&request).is_err(), + "defer_loading activates routing safety but requires a declaration or replayed search state" + ); +} + +#[test] +fn declaration_free_manual_replay_builds_loaded_views_without_a_synthetic_declaration() { + let dynamic = function("get_weather", "Get weather", "string", true); + let public_request = request( + json!([]), + json!([ + search_call("call_search_1"), + search_output("call_search_1", vec![dynamic.clone()]) + ]), + ); + let mut state = + ToolSearchState::build(&public_request).expect("manual public replay is valid without redeclaring tool_search"); + + assert!(state.is_active()); + assert!(state.synthetic_tool_search().is_none()); + assert_eq!(tool_values(Some(state.loaded_public_tools())), json!([dynamic])); + let public = tool_values(state.public_effective_tools()); + let private = private_tool_values(&mut state, &public_request); + assert_eq!(public[0]["defer_loading"], true); + assert!(private[0].get("defer_loading").is_none()); +} + +#[test] +fn prepare_inference_request_consumes_prepared_views_without_mutating_public_source() { + let deferred = function("get_weather", "Get weather", "string", true); + let public = request( + json!([search_declaration(), deferred.clone()]), + json!([ + {"type": "message", "role": "user", "content": "find weather"}, + search_call("call_search_1"), + search_output("call_search_1", vec![deferred.clone()]) + ]), + ); + let public_before = serde_json::to_value(&public).expect("public request serializes"); + let mut state = ToolSearchState::build(&public).expect("valid function-only state"); + assert_eq!(tool_values(Some(state.loaded_public_tools())), json!([deferred])); + assert_eq!(synthetic_description(&state), "Search the client tool catalog"); + + let private = private_request(&mut state, &public); + + assert_eq!( + serde_json::to_value(&public).expect("public request still serializes"), + public_before, + "private lowering must not mutate the public request" + ); + let private_value = serde_json::to_value(&private).expect("private request serializes"); + assert_eq!(private_value["input"][1]["type"], "function_call"); + assert_eq!(private_value["input"][1]["name"], "tool_search"); + assert_eq!(private_value["input"][1]["call_id"], "call_search_1"); + assert_eq!(private_value["input"][2]["type"], "function_call_output"); + assert_eq!(private_value["input"][2]["call_id"], "call_search_1"); + assert_eq!(private_value["tools"].as_array().map(Vec::len), Some(2)); + assert_eq!(private_value["tools"][0]["type"], "tool_search"); + assert_eq!(private_value["tools"][0]["execution"], "client"); + assert_eq!(private_value["tools"][1]["name"], "get_weather"); + assert!(private_value["tools"][1].get("defer_loading").is_none()); + + let upstream = serde_json::to_value(private.to_upstream_request(false).expect("prepared request lowers")) + .expect("upstream request serializes"); + assert_eq!(upstream["tools"][0]["type"], "function"); + assert_eq!(upstream["tools"][0]["name"], "tool_search"); +} + +#[test] +fn safe_catalog_is_minimal_and_never_exposes_deferred_configuration() { + let request = request( + json!([ + search_declaration(), + { + "type": "function", + "name": "hidden_function", + "description": "Safe function description", + "parameters": { + "type": "object", + "properties": {"secret_parameter": {"type": "string"}} + }, + "defer_loading": true + }, + { + "type": "namespace", + "name": "hidden_namespace", + "description": "Safe namespace description", + "tools": [{ + "type": "function", + "name": "secret_member", + "description": "secret member description", + "parameters": {"type": "object", "properties": {"secret": {"type": "string"}}}, + "defer_loading": true + }] + } + ]), + json!("find a tool"), + ); + + let public_before = serde_json::to_value(&request).expect("public request serializes"); + let mut state = ToolSearchState::build(&request).expect("catalog construction is pure"); + drop(private_request(&mut state, &request)); + assert_eq!( + serde_json::to_value(&request).expect("public request still serializes"), + public_before, + "private materialization must preserve public deferred configuration" + ); + assert_eq!( + synthetic_description(&state), + "Search the client tool catalog. Available catalog entries: hidden_function — Safe function description; \ +hidden_namespace — Safe namespace description." + ); + + let model_visible = + serde_json::to_string(&state.synthetic_tool_search()).expect("synthetic declaration serializes"); + for secret in ["secret_parameter", "secret_member", "secret member description"] { + assert!(!model_visible.contains(secret), "catalog leaked {secret}"); + } +} + +#[test] +fn replay_restores_loaded_deferred_tool_after_compaction_removed_search_pair() { + let deferred = json!({ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }); + let request = request( + json!([search_declaration(), deferred]), + json!([{ + "type": "compaction", + "encrypted_content": "The weather tool was loaded earlier." + }]), + ); + let restored: Vec = serde_json::from_value(json!([{ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }])) + .expect("valid restored loaded definitions"); + + let mut state = ToolSearchState::build_with_loaded_tools(&request, &restored, false) + .expect("typed metadata restores compaction-lost loaded state"); + + assert_eq!(state.loaded_public_tools().len(), 1); + assert_eq!(synthetic_description(&state), "Search the client tool catalog"); + let private_request = private_request(&mut state, &request); + let private = private_request.tools.as_deref().expect("private tools"); + let loaded = private + .iter() + .find_map(|tool| match tool { + agentic_core::types::tools::ResponsesTool::Function(function) + if function.name.as_str() == "get_weather" => + { + Some(function) + } + _ => None, + }) + .expect("loaded function is effective upstream"); + assert_eq!(loaded.defer_loading, None); +} + +#[test] +fn compacted_replay_does_not_reload_definition_omitted_by_explicit_tools() { + let request = request( + json!([search_declaration()]), + json!([ + { + "type": "tool_search_call", + "id": "tsc_obsolete", + "call_id": "call_obsolete", + "arguments": {"query": "weather"} + }, + { + "type": "tool_search_output", + "call_id": "call_obsolete", + "tools": [{ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }] + }, + { + "type": "compaction", + "encrypted_content": "The old search pair is superseded." + }, + { + "type": "message", + "role": "user", + "content": "Continue without weather" + } + ]), + ); + let restored: Vec = serde_json::from_value(json!([{ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + }])) + .expect("stored loaded definition"); + + let mut state = ToolSearchState::build_with_loaded_tools(&request, &restored, true) + .expect("explicit tools define the post-compaction catalog"); + assert!(state.loaded_public_tools().is_empty()); + assert!(state + .public_effective_tools() + .unwrap() + .iter() + .all(|tool| !matches!(tool, agentic_core::types::tools::ResponsesTool::Function(function) if function.name.as_str() == "get_weather"))); + assert!(private_request(&mut state, &request) + .tools + .as_deref() + .expect("private tools") + .iter() + .all(|tool| !matches!(tool, agentic_core::types::tools::ResponsesTool::Function(function) if function.name.as_str() == "get_weather"))); +} + +#[test] +fn replayed_loaded_marker_rejects_explicit_cross_kind_identity_collision() { + let request = request( + json!([ + search_declaration(), + { + "type": "namespace", + "name": "shared_identity", + "description": "Replacement namespace", + "tools": [{ + "type": "function", + "name": "member", + "parameters": {"type": "object"} + }] + } + ]), + json!([{ + "type": "compaction", + "encrypted_content": "A function with this name was loaded earlier." + }]), + ); + let restored: Vec = serde_json::from_value(json!([{ + "type": "function", + "name": "shared_identity", + "description": "Original function", + "parameters": {"type": "object"}, + "defer_loading": true + }])) + .expect("stored function marker"); + + assert!(ToolSearchState::build_with_loaded_tools(&request, &restored, true).is_err()); +} diff --git a/crates/agentic-server-core/tests/tool_search_test.rs b/crates/agentic-server-core/tests/tool_search_test.rs new file mode 100644 index 00000000..2bb5e0cd --- /dev/null +++ b/crates/agentic-server-core/tests/tool_search_test.rs @@ -0,0 +1,1352 @@ +mod support; + +use std::collections::HashSet; +use std::collections::VecDeque; +use std::fmt::Write as _; +use std::fs; +use std::future::Future; +use std::path::Path; +use std::pin::Pin; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use agentic_core::executor::{ConversationHandler, ExecuteRequest, ExecutionContext, ResponseHandler}; +use agentic_core::storage::{ConversationStore, ResponseStore, create_pool_with_schema}; +use agentic_core::tool::{ + GatewayExecutor, ToolError, ToolHandler, ToolOutput, ToolType, model_visible_namespace_member_name, +}; +use agentic_core::types::io::{FunctionTool, OutputItem}; +use agentic_core::types::request_response::{RequestPayload, ResponsePayload}; +use axum::Router; +use axum::body::Bytes; +use axum::response::IntoResponse; +use axum::routing::post; +use either::Either; +use futures::StreamExt; +use serde_json::{Value, json}; +use tokio::net::TcpListener; +use tokio::sync::Mutex; + +#[derive(Debug)] +struct CountingWebSearch { + calls: Arc, +} + +impl ToolHandler for CountingWebSearch { + fn tool_type(&self) -> ToolType { + ToolType::WebSearch + } + + fn validate(&self, _param: &Value) -> Result<(), ToolError> { + Ok(()) + } + + fn normalize(&self, _param: &Value) -> Vec { + Vec::new() + } +} + +impl GatewayExecutor for CountingWebSearch { + fn execute( + &self, + call_id: &str, + _tool_name: &str, + _arguments: &str, + _config: &Value, + ) -> Pin> + Send + '_>> { + self.calls.fetch_add(1, Ordering::SeqCst); + Box::pin(std::future::ready(Ok(ToolOutput { + call_id: call_id.to_owned(), + output: "must not execute".to_owned(), + }))) + } +} + +async fn spawn_sequenced_llm(responses: Vec) -> (String, Arc>>, tokio::task::JoinHandle<()>) { + let responses = Arc::new(Mutex::new(VecDeque::from(responses))); + let requests = Arc::new(Mutex::new(Vec::new())); + let route_responses = Arc::clone(&responses); + let route_requests = Arc::clone(&requests); + let app = Router::new().route( + "/v1/responses", + post(move |body: Bytes| { + let route_responses = Arc::clone(&route_responses); + let route_requests = Arc::clone(&route_requests); + async move { + route_requests + .lock() + .await + .push(serde_json::from_slice(&body).expect("captured request JSON")); + let response = route_responses + .lock() + .await + .pop_front() + .expect("one prepared response per request"); + axum::response::Response::builder() + .status(200) + .header("Content-Type", "application/json") + .body(axum::body::Body::from(response.to_string())) + .expect("mock response") + .into_response() + } + }), + ); + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind mock LLM"); + let address = listener.local_addr().expect("mock address"); + let handle = tokio::spawn(async move { axum::serve(listener, app).await.expect("mock server") }); + (format!("http://{address}"), requests, handle) +} + +async fn spawn_sequenced_streaming_llm( + responses: Vec, +) -> (String, Arc>>, tokio::task::JoinHandle<()>) { + let responses = Arc::new(Mutex::new(VecDeque::from(responses))); + let requests = Arc::new(Mutex::new(Vec::new())); + let route_responses = Arc::clone(&responses); + let route_requests = Arc::clone(&requests); + let app = Router::new().route( + "/v1/responses", + post(move |body: Bytes| { + let route_responses = Arc::clone(&route_responses); + let route_requests = Arc::clone(&route_requests); + async move { + route_requests + .lock() + .await + .push(serde_json::from_slice(&body).expect("captured request JSON")); + let response = route_responses + .lock() + .await + .pop_front() + .expect("one prepared response per request"); + axum::response::Response::builder() + .status(200) + .header("Content-Type", "text/event-stream") + .body(axum::body::Body::from(response)) + .expect("mock response") + .into_response() + } + }), + ); + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind mock LLM"); + let address = listener.local_addr().expect("mock address"); + let handle = tokio::spawn(async move { axum::serve(listener, app).await.expect("mock server") }); + (format!("http://{address}"), requests, handle) +} + +fn streaming_search_response(arguments: &str) -> String { + let events = [ + json!({ + "type": "response.created", "sequence_number": 0, + "response": { + "id": "upstream_search", "status": "in_progress", + "tools": [{ + "type": "function", "name": "tool_search", + "description": "private normalized catalog", + "parameters": {"type": "object"} + }] + } + }), + json!({ + "type": "response.in_progress", "sequence_number": 1, + "response": { + "id": "upstream_search", "status": "in_progress", + "tools": [{ + "type": "function", "name": "tool_search", + "description": "private normalized catalog", + "parameters": {"type": "object"} + }] + } + }), + json!({ + "type": "response.output_item.added", "sequence_number": 2, "output_index": 0, + "item": {"id": "fc_search_1", "type": "function_call", "call_id": "call_search_1", + "name": "tool_search", "arguments": "", "status": "in_progress"} + }), + json!({ + "type": "response.function_call_arguments.delta", "sequence_number": 3, "output_index": 0, + "item_id": "fc_search_1", "delta": arguments + }), + json!({ + "type": "response.function_call_arguments.done", "sequence_number": 4, "output_index": 0, + "item_id": "fc_search_1", "name": "tool_search", "arguments": arguments + }), + json!({ + "type": "response.output_item.done", "sequence_number": 5, "output_index": 0, + "item": {"id": "fc_search_1", "type": "function_call", "call_id": "call_search_1", + "name": "tool_search", "arguments": arguments, "status": "completed"} + }), + json!({ + "type": "response.completed", "sequence_number": 6, + "response": { + "id": "upstream_search", "status": "completed", "usage": null, + "output": [{ + "type": "function_call", "id": "fc_provider_terminal_regenerated", + "call_id": "call_search_1", "name": "tool_search", + "arguments": arguments, "status": "completed" + }] + } + }), + ]; + streaming_response(events) +} + +fn streaming_response(events: impl IntoIterator) -> String { + let mut response = String::new(); + for event in events { + writeln!(&mut response, "data: {event}\n").expect("writing to String cannot fail"); + } + response.push_str("data: [DONE]\n\n"); + response +} + +fn streaming_partial_search_failure_response() -> String { + let events = [ + json!({ + "type": "response.created", + "response": {"id": "upstream_failed", "status": "in_progress"} + }), + json!({ + "type": "response.output_item.added", "output_index": 0, + "item": {"id": "fc_partial", "type": "function_call", "call_id": "call_partial", + "arguments": "", "status": "in_progress"} + }), + json!({ + "type": "response.function_call_arguments.delta", "output_index": 0, + "item_id": "fc_partial", "delta": "{\"query\":\"weather" + }), + json!({ + "type": "response.failed", + "response": { + "id": "upstream_failed", "status": "failed", "usage": null, + "error": {"code": "provider_failure", "message": "provider stopped"}, + "incomplete_details": {"reason": "upstream_error"} + } + }), + ]; + streaming_response(events) +} + +fn streaming_gateway_call_then_malformed_search_response() -> String { + let events = [ + json!({"type":"response.created","response":{"id":"up_mixed","status":"in_progress"}}), + json!({ + "type":"response.output_item.added","output_index":0, + "item":{"id":"fc_web","type":"function_call","status":"in_progress", + "name":"web_search","call_id":"call_web","arguments":""} + }), + json!({ + "type":"response.output_item.done","output_index":0, + "item":{"id":"fc_web","type":"function_call","status":"completed", + "name":"web_search","call_id":"call_web","arguments":"{\"query\":\"weather\"}"} + }), + json!({ + "type":"response.output_item.added","output_index":1, + "item":{"id":"fc_search_bad","type":"function_call","status":"in_progress", + "name":"tool_search","call_id":"call_search_bad","arguments":""} + }), + json!({ + "type":"response.function_call_arguments.delta","output_index":1, + "item_id":"fc_search_bad","delta":"[]" + }), + json!({ + "type":"response.function_call_arguments.done","output_index":1, + "item_id":"fc_search_bad","name":"tool_search","arguments":"[]" + }), + ]; + streaming_response(events) +} + +fn streaming_named_function_response(name: &str) -> String { + streaming_response([ + json!({ + "type": "response.created", "sequence_number": 0, + "response": {"id": "upstream_withheld", "status": "in_progress"} + }), + json!({ + "type": "response.output_item.added", "sequence_number": 1, "output_index": 0, + "item": {"id": "fc_withheld", "type": "function_call", "call_id": "call_withheld", + "name": name, "arguments": "", "status": "in_progress"} + }), + ]) +} + +fn streaming_late_named_function_response(name: &str) -> String { + streaming_response([ + json!({ + "type": "response.created", "sequence_number": 0, + "response": {"id": "upstream_withheld", "status": "in_progress"} + }), + json!({ + "type": "response.output_item.added", "sequence_number": 1, "output_index": 0, + "item": {"id": "fc_withheld", "type": "function_call", "call_id": "call_withheld", + "arguments": "", "status": "in_progress"} + }), + json!({ + "type": "response.function_call_arguments.done", "sequence_number": 2, "output_index": 0, + "item_id": "fc_withheld", "name": name, "arguments": "{}" + }), + ]) +} + +fn streaming_terminal_function_response(name: &str) -> String { + streaming_response([ + json!({ + "type": "response.created", "sequence_number": 0, + "response": {"id": "upstream_withheld", "status": "in_progress"} + }), + json!({ + "type": "response.completed", "sequence_number": 1, + "response": { + "id": "upstream_withheld", "status": "completed", "usage": null, + "output": [{ + "type": "function_call", "id": "fc_withheld", "call_id": "call_withheld", + "name": name, "arguments": "{}", "status": "completed" + }] + } + }), + ]) +} + +async fn run_streaming(request: RequestPayload, context: Arc) -> Vec { + let Either::Right(mut stream) = ExecuteRequest::new(request, context) + .run() + .await + .expect("streaming execution") + else { + panic!("streaming request must return a stream") + }; + let mut events = Vec::new(); + while let Some(chunk) = stream.next().await { + for line in chunk.lines() { + let Some(data) = line.strip_prefix("data: ") else { + continue; + }; + if data != "[DONE]" { + events.push(serde_json::from_str(data).expect("stream event JSON")); + } + } + } + events +} + +fn request(input: &Value, tools: &Value) -> RequestPayload { + serde_json::from_value(json!({ + "model": "test", + "input": input, + "tools": tools, + "store": false, + "stream": false, + "parallel_tool_calls": false + })) + .expect("public request") +} + +async fn run(request: RequestPayload, context: Arc) -> ResponsePayload { + match Box::pin(ExecuteRequest::new(request, context).run()) + .await + .expect("blocking execution") + { + Either::Left(response) => response, + Either::Right(_) => panic!("blocking helper received a streaming response"), + } +} + +fn assert_public_search_call(response: &ResponsePayload) -> Value { + let OutputItem::ToolSearchCall(search_call) = &response.output[0] else { + panic!("normalized search call must be public") + }; + assert_eq!(search_call.id, "tsc_search_1"); + assert_eq!(search_call.call_id, "call_search_1"); + assert_eq!( + search_call.arguments, + serde_json::from_value(json!({"query": "weather"})).unwrap() + ); + let public_search_call = serde_json::to_value(&response.output[0]).expect("public search call serializes"); + assert_eq!(public_search_call["execution"], "client"); + assert_eq!(public_search_call["status"], "completed"); + public_search_call +} + +fn assert_private_request_sequence(requests: &[Value]) { + assert_eq!(requests.len(), 3); + assert_eq!(requests[0]["tools"].as_array().map(Vec::len), Some(1)); + assert_eq!(requests[0]["tools"][0]["name"], "tool_search"); + for request in &requests[1..] { + assert!(request.get("previous_response_id").is_none()); + assert_eq!(request["input"][1]["type"], "function_call"); + assert_eq!(request["input"][1]["name"], "tool_search"); + assert_eq!(request["input"][1]["call_id"], "call_search_1"); + assert_eq!(request["input"][2]["type"], "function_call_output"); + assert_eq!(request["input"][2]["call_id"], "call_search_1"); + assert_eq!(request["tools"].as_array().map(Vec::len), Some(1)); + assert_eq!(request["tools"][0]["name"], "get_weather"); + assert!(request["tools"][0].get("defer_loading").is_none()); + } +} + +fn search_declaration() -> Value { + json!({ + "type": "tool_search", + "execution": "client", + "description": "Search the client tool catalog", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"] + } + }) +} + +fn deferred_weather() -> Value { + json!({ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"] + }, + "strict": true, + "defer_loading": true + }) +} + +fn completed_search_replay() -> Value { + json!([ + { + "type": "tool_search_call", + "id": "tsc_prior", + "call_id": "call_prior", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + }, + { + "type": "tool_search_output", + "call_id": "call_prior", + "execution": "client", + "status": "completed", + "tools": [deferred_weather()] + } + ]) +} + +fn weather_namespace() -> Value { + json!({ + "type": "namespace", + "name": "weather_namespace_with_a_name_long_enough_to_require_bounded_flattening", + "description": "Weather tools", + "tools": [ + { + "type": "function", + "name": "current", + "parameters": {"type": "object"} + }, + { + "type": "function", + "name": "forecast_member_with_a_name_long_enough_to_require_bounded_flattening", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}} + }, + "defer_loading": true + } + ] + }) +} + +fn loaded_weather_namespace_subset() -> Value { + let namespace = weather_namespace(); + json!({ + "type": "namespace", + "name": namespace["name"], + "description": namespace["description"], + "tools": [namespace["tools"][1].clone()] + }) +} + +#[tokio::test] +async fn function_only_streaming_search_has_public_lifecycle_terminal_and_private_lowering() { + let (llm_url, requests, _server) = + spawn_sequenced_streaming_llm(vec![streaming_search_response(r#"{"query":"weather"}"#)]).await; + let context = Arc::new(ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + )); + let mut payload = request( + &json!("find weather"), + &json!([search_declaration(), deferred_weather()]), + ); + payload.stream = true; + + let events = run_streaming(payload, context).await; + + let response_envelopes = events + .iter() + .filter_map(|event| event.get("response")) + .filter(|response| response.get("tools").is_some()) + .collect::>(); + assert!(!response_envelopes.is_empty()); + for response in response_envelopes { + assert_eq!( + response["tools"], + json!([search_declaration(), deferred_weather()]), + "stream response envelopes must restore public tool declarations" + ); + assert!(!response["tools"].to_string().contains("\"name\":\"tool_search\"")); + } + + assert_eq!( + events + .iter() + .map(|event| event["sequence_number"].as_u64()) + .collect::>(), + (0..u64::try_from(events.len()).unwrap()).map(Some).collect::>() + ); + assert!(events.iter().all(|event| { + !matches!( + event["type"].as_str(), + Some("response.function_call_arguments.delta" | "response.function_call_arguments.done" | "error") + ) + })); + let lifecycle = events + .iter() + .filter(|event| { + matches!( + event["type"].as_str(), + Some("response.output_item.added" | "response.output_item.done") + ) + }) + .collect::>(); + assert_eq!(lifecycle.len(), 2); + assert_eq!(lifecycle[0]["item"]["type"], "tool_search_call"); + assert_eq!(lifecycle[0]["item"]["status"], "in_progress"); + assert_eq!(lifecycle[0]["item"]["arguments"], json!({})); + assert_eq!(lifecycle[1]["item"]["type"], "tool_search_call"); + assert_eq!(lifecycle[1]["item"]["status"], "completed"); + assert_eq!(lifecycle[1]["item"]["arguments"], json!({"query": "weather"})); + assert_eq!(lifecycle[0]["item"]["id"], lifecycle[1]["item"]["id"]); + assert_eq!(lifecycle[0]["item"]["call_id"], lifecycle[1]["item"]["call_id"]); + + let terminal = events + .iter() + .find(|event| event["type"] == "response.completed") + .expect("terminal response"); + let terminal_call = &terminal["response"]["output"][0]; + assert_eq!(terminal_call, &lifecycle[1]["item"]); + + let captured = requests.lock().await; + assert_eq!(captured.len(), 1); + assert_eq!(captured[0]["stream"], true); + assert_eq!(captured[0]["tools"].as_array().map(Vec::len), Some(1)); + assert_eq!(captured[0]["tools"][0]["name"], "tool_search"); + assert!(captured[0].to_string().contains("get_weather")); + assert!(!captured[0].to_string().contains("\"city\"")); +} + +#[tokio::test] +async fn malformed_streaming_search_finishes_with_response_failed_not_completed() { + let (llm_url, _requests, _server) = spawn_sequenced_streaming_llm(vec![streaming_search_response("[]")]).await; + let context = Arc::new(ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + )); + let mut payload = request( + &json!("find weather"), + &json!([search_declaration(), deferred_weather()]), + ); + payload.stream = true; + + let events = run_streaming(payload, context).await; + + assert!(events.iter().all(|event| event["type"] != "response.completed")); + assert!(events.iter().all(|event| { + !matches!( + event["type"].as_str(), + Some("response.function_call_arguments.delta" | "response.function_call_arguments.done" | "error") + ) + })); + let failed = events.last().expect("response.failed"); + assert_eq!(failed["type"], "response.failed"); + assert_eq!(failed["response"]["status"], "failed"); + assert_eq!(failed["response"]["error"]["type"], "tool_error"); + assert_eq!(failed["response"]["error"]["code"], "tool_error"); + assert_eq!( + events + .iter() + .map(|event| event["sequence_number"].as_u64()) + .collect::>(), + (0..u64::try_from(events.len()).unwrap()).map(Some).collect::>() + ); +} + +#[tokio::test] +async fn withheld_function_streams_fail_without_public_call_or_persistence() { + let namespace = weather_namespace(); + let flat_name = model_visible_namespace_member_name( + namespace["name"].as_str().expect("namespace name"), + namespace["tools"][1]["name"].as_str().expect("member name"), + ); + let cases = [ + ( + "get_weather".to_owned(), + json!([search_declaration(), deferred_weather()]), + ), + (flat_name, json!([search_declaration(), namespace])), + ]; + + for (name, tools) in cases { + for response in [ + streaming_named_function_response(&name), + streaming_late_named_function_response(&name), + streaming_terminal_function_response(&name), + ] { + let (llm_url, requests, _server) = spawn_sequenced_streaming_llm(vec![response]).await; + let pool = create_pool_with_schema(Some("sqlite://?mode=memory")) + .await + .expect("storage schema"); + let context = Arc::new(ExecutionContext::new( + ConversationHandler::new(ConversationStore::new(Arc::clone(&pool))), + ResponseHandler::new(ResponseStore::new(Arc::clone(&pool))), + Arc::new(reqwest::Client::new()), + llm_url, + )); + let mut payload = request(&json!("find a tool"), &tools); + payload.stream = true; + payload.store = true; + + let events = run_streaming(payload, context).await; + + let failed = events.last().expect("response.failed"); + assert_eq!(failed["type"], "response.failed", "{name}"); + assert_eq!(failed["response"]["error"]["code"], "tool_error", "{name}"); + assert!(events.iter().all(|event| event["type"] != "response.completed")); + assert!(events.iter().all(|event| { + !matches!( + event["type"].as_str(), + Some("response.function_call_arguments.delta" | "response.function_call_arguments.done") + ) && event + .get("item") + .and_then(|item| item.get("name")) + .and_then(Value::as_str) + != Some(name.as_str()) + })); + let response_id = failed["response"]["id"].as_str().expect("failed response ID"); + let error = ResponseStore::new(pool) + .get(response_id) + .await + .expect_err("invalid streamed response must not persist"); + assert!(error.is_not_found()); + assert_eq!(requests.lock().await.len(), 1, "only inference may run"); + } + } +} + +#[tokio::test] +async fn upstream_failure_after_partial_search_preserves_provider_error_without_normalized_output() { + let (llm_url, _requests, _server) = + spawn_sequenced_streaming_llm(vec![streaming_partial_search_failure_response()]).await; + let context = Arc::new(ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + )); + let mut immediate_weather = deferred_weather(); + immediate_weather + .as_object_mut() + .expect("function tool") + .remove("defer_loading"); + let mut payload = request( + &json!("find weather"), + &json!([search_declaration(), immediate_weather]), + ); + payload.stream = true; + + let events = run_streaming(payload, context).await; + + let failed = events.last().expect("response.failed"); + assert_eq!(failed["type"], "response.failed"); + assert_eq!(failed["response"]["error"]["code"], "provider_failure"); + assert_eq!(failed["response"]["error"]["message"], "provider stopped"); + assert_eq!(failed["response"]["incomplete_details"]["reason"], "upstream_error"); + assert_eq!(failed["response"]["output"], json!([])); + assert!(events.iter().all(|event| event["type"] != "response.completed")); + assert!(events.iter().all(|event| { + !matches!( + event["type"].as_str(), + Some("response.function_call_arguments.delta" | "response.function_call_arguments.done") + ) + })); +} + +#[tokio::test] +async fn malformed_streaming_search_is_not_dispatched_or_persisted_after_start() { + let (llm_url, _requests, _server) = + spawn_sequenced_streaming_llm(vec![streaming_gateway_call_then_malformed_search_response()]).await; + let pool = create_pool_with_schema(Some("sqlite://?mode=memory")) + .await + .expect("storage schema"); + let calls = Arc::new(AtomicUsize::new(0)); + let context = Arc::new( + ExecutionContext::new( + ConversationHandler::new(ConversationStore::new(Arc::clone(&pool))), + ResponseHandler::new(ResponseStore::new(Arc::clone(&pool))), + Arc::new(reqwest::Client::new()), + llm_url, + ) + .with_gateway_executor(Arc::new(CountingWebSearch { + calls: Arc::clone(&calls), + })), + ); + let mut payload = request( + &json!("find weather"), + &json!([search_declaration(), {"type":"web_search_preview"}]), + ); + payload.stream = true; + payload.store = true; + + let events = run_streaming(payload, context).await; + + let failed = events.last().expect("response.failed"); + assert_eq!(failed["type"], "response.failed"); + assert!(events.iter().all(|event| event["type"] != "response.completed")); + assert_eq!(calls.load(Ordering::SeqCst), 0, "gateway call must not dispatch"); + let response_id = failed["response"]["id"].as_str().expect("failed response ID"); + let error = ResponseStore::new(pool) + .get(response_id) + .await + .expect_err("malformed streamed response must not persist"); + assert!(error.is_not_found()); +} + +#[tokio::test] +async fn function_only_nonstreaming_manual_three_request_flow() { + let (llm_url, requests, _server) = spawn_sequenced_llm(vec![ + json!({ + "id": "upstream_search", + "object": "response", + "status": "completed", + "model": "test", + "created_at": 0, + "output": [{ + "type": "function_call", + "id": "fc_search_1", + "call_id": "call_search_1", + "name": "tool_search", + "arguments": "{\"query\":\"weather\"}", + "status": "completed" + }] + }), + json!({ + "id": "upstream_weather", + "object": "response", + "status": "completed", + "model": "test", + "created_at": 0, + "output": [{ + "type": "function_call", + "id": "fc_weather_1", + "call_id": "call_weather_1", + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}", + "status": "completed" + }] + }), + json!({ + "id": "upstream_final", + "object": "response", + "status": "completed", + "model": "test", + "created_at": 0, + "output": [{ + "type": "message", + "id": "msg_final", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": "PARIS_WEATHER_OK", "annotations": []}] + }] + }), + ]) + .await; + let context = Arc::new(ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + )); + let user = json!({"type": "message", "role": "user", "content": "find weather"}); + + let first = run( + request( + &json!([user.clone()]), + &json!([search_declaration(), deferred_weather()]), + ), + Arc::clone(&context), + ) + .await; + let public_search_call = assert_public_search_call(&first); + + let public_search_output = json!({ + "type": "tool_search_output", + "call_id": "call_search_1", + "tools": [deferred_weather()] + }); + let second_input = json!([user.clone(), public_search_call.clone(), public_search_output.clone()]); + let second = run(request(&second_input, &json!([])), Arc::clone(&context)).await; + let OutputItem::FunctionCall(weather_call) = &second.output[0] else { + panic!("loaded client function remains an ordinary public function call") + }; + assert_eq!(weather_call.name, "get_weather"); + assert_eq!(weather_call.call_id, "call_weather_1"); + + let public_weather_call = serde_json::to_value(&second.output[0]).expect("function call serializes"); + let mut third_items = second_input.as_array().expect("item history").clone(); + third_items.push(public_weather_call); + third_items.push(json!({ + "type": "function_call_output", + "call_id": "call_weather_1", + "output": "sunny" + })); + let third = run(request(&Value::Array(third_items), &json!([])), context).await; + assert!(matches!(&third.output[0], OutputItem::Message(_))); + assert_eq!( + serde_json::to_value(&third.output[0]).unwrap()["content"][0]["text"], + "PARIS_WEATHER_OK" + ); + + assert_private_request_sequence(&requests.lock().await); +} + +#[tokio::test] +async fn function_only_nonstreaming_malformed_search_is_atomic_before_gateway_side_effects() { + for (case, malformed_search) in [ + ( + "non-object arguments", + json!({"arguments": "null", "namespace": null, "status": "completed"}), + ), + ( + "nonterminal status", + json!({"arguments": "{}", "namespace": null, "status": "in_progress"}), + ), + ( + "unexpected namespace", + json!({"arguments": "{}", "namespace": "tools", "status": "completed"}), + ), + ] { + let (llm_url, requests, _server) = spawn_sequenced_llm(vec![json!({ + "id": "upstream_invalid_mixed", + "object": "response", + "status": "completed", + "model": "test", + "created_at": 0, + "output": [ + { + "type": "function_call", + "id": "fc_web", + "call_id": "call_web", + "name": "web_search", + "arguments": "{\"query\":\"weather\"}", + "status": "completed" + }, + { + "type": "function_call", + "id": "fc_search", + "call_id": "call_search", + "name": "tool_search", + "arguments": malformed_search["arguments"], + "namespace": malformed_search["namespace"], + "status": malformed_search["status"] + } + ] + })]) + .await; + let calls = Arc::new(AtomicUsize::new(0)); + let context = Arc::new( + ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + ) + .with_gateway_executor(Arc::new(CountingWebSearch { + calls: Arc::clone(&calls), + })), + ); + let request = request( + &json!("find weather"), + &json!([search_declaration(), {"type": "web_search_preview"}]), + ); + + let Err(error) = ExecuteRequest::new(request, context).run().await else { + panic!("{case}: malformed reserved call must reject the whole response") + }; + + assert_eq!(error.http_status(), http::StatusCode::BAD_GATEWAY, "{case}"); + assert_eq!(error.error_type(), "tool_error", "{case}"); + assert_eq!(calls.load(Ordering::SeqCst), 0, "{case}: gateway call must not execute"); + assert_eq!(requests.lock().await.len(), 1, "{case}: only inference may run"); + } +} + +#[tokio::test] +async fn declaration_free_replay_malformed_search_is_atomic_before_gateway_side_effects() { + let (llm_url, requests, _server) = spawn_sequenced_llm(vec![json!({ + "id": "upstream_invalid_replay", + "object": "response", + "status": "completed", + "model": "test", + "created_at": 0, + "output": [ + { + "type": "function_call", "id": "fc_web", "call_id": "call_web", + "name": "web_search", "arguments": "{\"query\":\"weather\"}", "status": "completed" + }, + { + "type": "function_call", "id": "fc_search", "call_id": "call_search", + "name": "tool_search", "namespace": null, "arguments": "{}", "status": "in_progress" + } + ] + })]) + .await; + let calls = Arc::new(AtomicUsize::new(0)); + let context = Arc::new( + ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + ) + .with_gateway_executor(Arc::new(CountingWebSearch { + calls: Arc::clone(&calls), + })), + ); + let request = request(&completed_search_replay(), &json!([{"type": "web_search_preview"}])); + + let Err(error) = Box::pin(ExecuteRequest::new(request, context).run()).await else { + panic!("malformed declaration-free replay call must reject the whole response") + }; + assert_eq!(error.http_status(), http::StatusCode::BAD_GATEWAY); + assert_eq!(calls.load(Ordering::SeqCst), 0, "gateway call must not execute"); + assert_eq!(requests.lock().await.len(), 1, "only inference may run"); +} + +#[tokio::test] +async fn namespace_nonstreaming_manual_flow_reuses_flattening_and_restoration() { + let namespace = weather_namespace(); + let namespace_name = namespace["name"].as_str().expect("namespace name"); + let member_name = namespace["tools"][1]["name"].as_str().expect("member name"); + let flat_name = model_visible_namespace_member_name(namespace_name, member_name); + assert!(flat_name.len() <= 64); + let (llm_url, requests, _server) = spawn_sequenced_llm(vec![ + json!({ + "id": "upstream_search", "object": "response", "status": "completed", "model": "test", + "created_at": 0, "output": [{ + "type": "function_call", "id": "fc_search", "call_id": "call_search", + "name": "tool_search", "namespace": null, "arguments": "{\"query\":\"forecast\"}", + "status": "completed" + }] + }), + json!({ + "id": "upstream_namespace", "object": "response", "status": "completed", "model": "test", + "created_at": 0, "output": [{ + "type": "function_call", "id": "fc_forecast", "call_id": "call_forecast", + "name": flat_name, "namespace": null, "arguments": "{\"city\":\"Paris\"}", + "status": "completed" + }] + }), + json!({ + "id": "upstream_final", "object": "response", "status": "completed", "model": "test", + "created_at": 0, "output": [{ + "type": "message", "id": "msg_final", "role": "assistant", "status": "completed", + "content": [{"type": "output_text", "text": "NAMESPACE_OK", "annotations": []}] + }] + }), + ]) + .await; + let context = Arc::new(ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + )); + let user = json!({"type": "message", "role": "user", "content": "find forecast"}); + let first = run( + request( + &json!([user.clone()]), + &json!([search_declaration(), namespace.clone()]), + ), + Arc::clone(&context), + ) + .await; + let public_search_call = serde_json::to_value(&first.output[0]).expect("search call serializes"); + let public_search_output = json!({ + "type": "tool_search_output", "call_id": "call_search", "execution": "client", + "status": "completed", "tools": [loaded_weather_namespace_subset()] + }); + let second_input = json!([user.clone(), public_search_call, public_search_output]); + let public_choice = json!({"type": "function", "namespace": namespace_name, "name": member_name}); + let mut second_request = request(&second_input, &json!([namespace.clone()])); + second_request.tool_choice = Some(serde_json::from_value(public_choice.clone()).expect("public namespace choice")); + let second = run(second_request, Arc::clone(&context)).await; + let OutputItem::FunctionCall(call) = &second.output[0] else { + panic!("loaded namespace member remains a client function call") + }; + assert_eq!(call.namespace.as_deref(), Some(namespace_name)); + assert_eq!(call.name, member_name); + assert_ne!(call.name, flat_name, "private flat name must not leak publicly"); + let mut available_namespace = namespace.clone(); + available_namespace["tools"][1] + .as_object_mut() + .expect("loaded namespace member") + .remove("defer_loading"); + assert_eq!( + serde_json::to_value(second.tools.as_ref().expect("response tools")).expect("response tools serialize"), + json!([available_namespace]) + ); + assert_eq!( + serde_json::to_value(second.tool_choice.as_ref().expect("response tool choice")) + .expect("response tool choice serializes"), + public_choice + ); + + let mut third_items = second_input.as_array().expect("history").clone(); + third_items.push(serde_json::to_value(&second.output[0]).expect("namespace call serializes")); + third_items.push(json!({ + "type": "function_call_output", "call_id": "call_forecast", "output": "sunny" + })); + let third = run(request(&Value::Array(third_items), &json!([namespace])), context).await; + assert!(matches!(&third.output[0], OutputItem::Message(_))); + + let requests = requests.lock().await; + assert_eq!(requests.len(), 3); + assert_eq!(requests[1]["tool_choice"]["name"], flat_name); + assert!(requests[1]["tool_choice"].get("namespace").is_none()); + assert_eq!(requests[1]["tools"].as_array().map(Vec::len), Some(2)); + assert!( + requests[1]["tools"] + .as_array() + .unwrap() + .iter() + .any(|tool| tool["name"] == flat_name) + ); + assert_eq!(requests[2]["input"][3]["name"], flat_name); + assert!(requests[2]["input"][3].get("namespace").is_none()); +} + +#[tokio::test] +async fn withheld_function_calls_fail_before_gateway_side_effects() { + let namespace = weather_namespace(); + let flat_name = model_visible_namespace_member_name( + namespace["name"].as_str().unwrap(), + namespace["tools"][1]["name"].as_str().unwrap(), + ); + for (name, tool) in [("get_weather".to_owned(), deferred_weather()), (flat_name, namespace)] { + let (llm_url, requests, _server) = spawn_sequenced_llm(vec![json!({ + "id": "upstream_withheld", "object": "response", "status": "completed", "model": "test", + "created_at": 0, "output": [ + { + "type": "function_call", "id": "fc_web", "call_id": "call_web", "name": "web_search", + "arguments": "{\"query\":\"weather\"}", "status": "completed" + }, + { + "type": "function_call", "id": "fc_withheld", "call_id": "call_withheld", "name": name, + "namespace": null, "arguments": "{}", "status": "completed" + } + ] + })]) + .await; + let calls = Arc::new(AtomicUsize::new(0)); + let context = Arc::new( + ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + ) + .with_gateway_executor(Arc::new(CountingWebSearch { + calls: Arc::clone(&calls), + })), + ); + let payload = request( + &json!("find weather"), + &json!([search_declaration(), tool, {"type": "web_search_preview"}]), + ); + + let Err(error) = Box::pin(ExecuteRequest::new(payload, context).run()).await else { + panic!("known withheld function call must reject the whole response") + }; + assert_eq!(error.http_status(), http::StatusCode::BAD_GATEWAY); + assert_eq!(calls.load(Ordering::SeqCst), 0, "gateway call must not execute"); + assert_eq!(requests.lock().await.len(), 1, "only inference may run"); + } +} + +#[tokio::test] +async fn withheld_namespace_history_calls_fail_before_inference() { + let namespace = weather_namespace(); + let namespace_name = namespace["name"].as_str().expect("namespace name"); + let member_name = namespace["tools"][1]["name"].as_str().expect("member name"); + let flat_name = model_visible_namespace_member_name(namespace_name, member_name); + let (llm_url, requests, _server) = spawn_sequenced_llm(Vec::new()).await; + let context = Arc::new(ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + )); + + for call in [ + json!({ + "type": "function_call", "id": "fc_public", "call_id": "call_public", + "namespace": namespace_name, "name": member_name, "arguments": "{}", "status": "completed" + }), + json!({ + "type": "function_call", "id": "fc_flat", "call_id": "call_flat", + "name": flat_name, "arguments": "{}", "status": "completed" + }), + ] { + let payload = request(&json!([call]), &json!([search_declaration(), namespace.clone()])); + let Err(error) = Box::pin(ExecuteRequest::new(payload, Arc::clone(&context)).run()).await else { + panic!("withheld history call must fail before inference") + }; + assert_eq!(error.http_status(), http::StatusCode::BAD_REQUEST); + } + assert!(requests.lock().await.is_empty(), "inference must not run"); +} + +#[tokio::test] +async fn dynamic_namespace_forward_references_fail_before_inference() { + let namespace = weather_namespace(); + let namespace_name = namespace["name"].as_str().expect("namespace name"); + let member_name = namespace["tools"][1]["name"].as_str().expect("member name"); + let flat_name = model_visible_namespace_member_name(namespace_name, member_name); + let loaded_subset = loaded_weather_namespace_subset(); + let (llm_url, requests, _server) = spawn_sequenced_llm(Vec::new()).await; + let context = Arc::new(ExecutionContext::new( + ConversationHandler::new(ConversationStore::disabled()), + ResponseHandler::new(ResponseStore::disabled()), + Arc::new(reqwest::Client::new()), + llm_url, + )); + + for call in [ + json!({ + "type": "function_call", "id": "fc_public", "call_id": "call_public", + "namespace": namespace_name, "name": member_name, "arguments": "{}", "status": "completed" + }), + json!({ + "type": "function_call", "id": "fc_flat", "call_id": "call_flat", + "name": flat_name, "arguments": "{}", "status": "completed" + }), + ] { + let payload = request( + &json!([ + call, + { + "type": "tool_search_call", "id": "tsc_dynamic", "call_id": "call_dynamic", + "arguments": {"query": "forecast"} + }, + { + "type": "tool_search_output", "call_id": "call_dynamic", + "tools": [loaded_subset.clone()] + } + ]), + &json!([search_declaration()]), + ); + let Err(error) = Box::pin(ExecuteRequest::new(payload, Arc::clone(&context)).run()).await else { + panic!("namespace forward reference must fail before inference") + }; + assert_eq!(error.http_status(), http::StatusCode::BAD_REQUEST); + } + assert!(requests.lock().await.is_empty(), "inference must not run"); +} + +fn visit_recorded_values(value: &Value, visitor: &mut impl FnMut(&serde_json::Map)) { + match value { + Value::Object(object) => { + visitor(object); + for child in object.values() { + visit_recorded_values(child, visitor); + } + } + Value::Array(array) => { + for child in array { + visit_recorded_values(child, visitor); + } + } + Value::String(text) => { + if matches!(text.trim().as_bytes().first(), Some(b'{' | b'[')) + && let Ok(decoded) = serde_json::from_str::(text) + { + visit_recorded_values(&decoded, visitor); + } + } + Value::Null | Value::Bool(_) | Value::Number(_) => {} + } +} + +fn validate_gateway_cassette_has_no_private_search_projection(raw: &Value) -> Result<(), String> { + let mut public_search_call_ids = HashSet::new(); + let mut search_item_ids = HashSet::new(); + visit_recorded_values(raw, &mut |object| { + let item_type = object.get("type").and_then(Value::as_str); + if item_type == Some("tool_search_call") { + if let Some(call_id) = object.get("call_id").and_then(Value::as_str) { + public_search_call_ids.insert(call_id.to_owned()); + } + if let Some(item_id) = object.get("id").and_then(Value::as_str) { + search_item_ids.insert(item_id.to_owned()); + } + } else if item_type == Some("function_call") + && object.get("name").and_then(Value::as_str) == Some("tool_search") + && let Some(item_id) = object.get("id").and_then(Value::as_str) + { + search_item_ids.insert(item_id.to_owned()); + } + }); + + let mut violation = None; + visit_recorded_values(raw, &mut |object| { + let item_type = object.get("type").and_then(Value::as_str); + let name = object.get("name").and_then(Value::as_str); + if matches!(item_type, Some("function" | "function_call")) && name == Some("tool_search") { + violation.get_or_insert_with(|| { + format!("public gateway cassette leaked the private synthetic search projection: {object:?}") + }); + } else if item_type == Some("function_call_output") { + let call_id = object.get("call_id").and_then(Value::as_str); + if call_id.is_some_and(|call_id| public_search_call_ids.contains(call_id)) { + violation.get_or_insert_with(|| { + format!("public gateway cassette leaked a normalized search function output: {object:?}") + }); + } + } else if matches!( + item_type, + Some("response.function_call_arguments.delta" | "response.function_call_arguments.done") + ) { + let item_id = object.get("item_id").and_then(Value::as_str); + if item_id.is_some_and(|item_id| search_item_ids.contains(item_id)) { + violation.get_or_insert_with(|| { + format!("public gateway cassette leaked normalized search argument events: {object:?}") + }); + } + } + }); + violation.map_or(Ok(()), Err) +} + +#[test] +fn provider_parity_matrix_is_exact_and_gateway_has_no_private_search_leaks() { + const FLOW_CASSETTES: [(&str, bool); 7] = [ + ("tool-search-openai-reference-gpt-5.6-nonstreaming.yaml", false), + ("tool-search-openai-reference-gpt-5.6-streaming.yaml", false), + ( + "tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml", + false, + ), + ("tool-search-direct-vllm-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml", false), + ("tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-nonstreaming.yaml", true), + ("tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-streaming.yaml", true), + ("tool-search-gateway-Qwen-Qwen3.6-35B-A3B-FP8-websocket.yaml", true), + ]; + + let directory = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/cassettes/tool_search"); + let expected_names = FLOW_CASSETTES + .iter() + .map(|(filename, _)| (*filename).to_owned()) + .collect::>(); + let actual_names = fs::read_dir(&directory) + .expect("tool-search cassette directory") + .filter_map(Result::ok) + .filter_map(|entry| { + let path = entry.path(); + if path.extension().and_then(|extension| extension.to_str()) != Some("yaml") { + return None; + } + let cassette = support::load_cassette(path.to_str().expect("cassette path")); + (cassette.turns.len() == 4).then(|| { + path.file_name() + .and_then(|filename| filename.to_str()) + .expect("cassette filename") + .to_owned() + }) + }) + .collect::>(); + assert_eq!( + actual_names, expected_names, + "the final seven-cassette flow matrix must be exact" + ); + + for (filename, gateway) in FLOW_CASSETTES { + let path = directory.join(filename); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + assert_eq!( + fs::metadata(&path).expect("cassette metadata").permissions().mode() & 0o111, + 0, + "checked-in cassette must not be executable: {filename}" + ); + } + if gateway { + let raw = + serde_yaml::from_str::(&fs::read_to_string(&path).expect("gateway cassette should be readable")) + .expect("gateway cassette YAML"); + let raw_turns = raw["turns"].as_array().expect("raw gateway cassette turns"); + let cassette = support::load_cassette(path.to_str().expect("gateway cassette path")); + let mut decoded_surfaces = Vec::new(); + for (raw_turn, turn) in raw_turns.iter().zip(&cassette.turns) { + decoded_surfaces.push(raw_turn["request"]["body"].clone()); + if let Some(body) = &turn.response.body { + decoded_surfaces.push(body.clone()); + } + if turn.response.sse.is_some() { + decoded_surfaces.extend(support::recorded_named_sse_events(turn)); + } + if let Some(websocket) = raw_turn["response"]["websocket"].as_array() { + decoded_surfaces.extend(websocket.iter().cloned()); + } + } + validate_gateway_cassette_has_no_private_search_projection(&Value::Array(decoded_surfaces)) + .unwrap_or_else(|error| panic!("{error}")); + } + } +} + +#[test] +fn provider_parity_non_leak_detector_rejects_nested_private_shapes() { + let cases = [ + json!({"response": {"tools": [{"type": "function", "name": "tool_search"}]}}), + json!({"response": {"output": [{ + "type": "function_call", "id": "fc_private", "call_id": "call_search", "name": "tool_search" + }]}}), + json!({ + "request": {"input": [{ + "type": "tool_search_call", "id": "tsc_public", "call_id": "call_search" + }, { + "type": "function_call_output", "call_id": "call_search", "output": "{}" + }]} + }), + json!([{ + "type": "response.output_item.added", + "item": {"type": "tool_search_call", "id": "tsc_public", "call_id": "call_search"} + }, { + "type": "response.function_call_arguments.delta", "item_id": "tsc_public", "delta": "{}" + }]), + json!([r#"{"type":"function_call","id":"fc_ws","call_id":"call_ws","name":"tool_search"}"#]), + ]; + + for case in cases { + assert!( + validate_gateway_cassette_has_no_private_search_projection(&case).is_err(), + "private nested shape should be rejected: {case}" + ); + } +} diff --git a/crates/agentic-server/src/handler/http/responses.rs b/crates/agentic-server/src/handler/http/responses.rs index a27daf71..f6e7213a 100644 --- a/crates/agentic-server/src/handler/http/responses.rs +++ b/crates/agentic-server/src/handler/http/responses.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use agentic_core::executor::{ExecuteRequest, compact_response as execute_compaction}; use agentic_core::proxy::{ProxyRequest, proxy_request}; +use agentic_core::tool::ToolRegistry; use agentic_core::types::request_response::{CompactRequest, RequestPayload}; use agentic_core::types::tools::ResponsesTool; @@ -53,11 +54,13 @@ pub async fn responses(State(state): State, req: Request) -> Response Err(e) => return e, }; + let has_tool_search_state = ToolRegistry::request_has_tool_search_state(&payload); let should_execute = payload.store || payload.previous_response_id.is_some() || payload.conversation_id.is_some() || payload.input.contains_compaction() || payload.input.has_compaction_trigger() + || has_tool_search_state || payload .context_management .as_ref() @@ -71,6 +74,7 @@ pub async fn responses(State(state): State, req: Request) -> Response has_conversation_id = payload.conversation_id.is_some(), has_compaction = payload.input.contains_compaction(), has_compaction_trigger = payload.input.has_compaction_trigger(), + has_tool_search_state, context_management = payload.context_management.as_ref().map_or(0, Vec::len), tools = payload.tools.as_ref().map_or(0, Vec::len), "routing HTTP responses request" diff --git a/crates/agentic-server/tests/responses_test.rs b/crates/agentic-server/tests/responses_test.rs index e79108e4..fe6ea3d3 100644 --- a/crates/agentic-server/tests/responses_test.rs +++ b/crates/agentic-server/tests/responses_test.rs @@ -6,7 +6,9 @@ use axum::http::header; use axum::response::IntoResponse; use axum::routing::post; use http::StatusCode; +use std::collections::VecDeque; use std::convert::Infallible; +use std::fmt::Write as _; use std::future::Future; use std::path::PathBuf; use std::pin::Pin; @@ -360,22 +362,35 @@ async fn spawn_mock_vllm_json() -> (String, tokio::task::JoinHandle<()>) { } async fn spawn_mock_vllm_json_capture() -> (String, Arc>>, tokio::task::JoinHandle<()>) { + spawn_mock_vllm_json_capture_body(serde_json::json!({ + "id": "mock_id", + "object": "response", + "status": "completed", + "model": "test", + "output": [], + "created_at": 0 + })) + .await +} + +async fn spawn_mock_vllm_json_capture_body( + response_body: serde_json::Value, +) -> (String, Arc>>, tokio::task::JoinHandle<()>) { let requests = Arc::new(Mutex::new(Vec::new())); let route_requests = Arc::clone(&requests); + let response_body = Arc::new(response_body.to_string()); let app = Router::new().route( "/v1/responses", post(move |body: Bytes| { let route_requests = Arc::clone(&route_requests); + let response_body = Arc::clone(&response_body); async move { let body = serde_json::from_slice::(&body).unwrap(); route_requests.lock().await.push(body); axum::response::Response::builder() .status(200) .header("Content-Type", "application/json") - .body(axum::body::Body::from( - r#"{"id":"mock_id","object":"response","status":"completed", - "model":"test","output":[],"created_at":0}"#, - )) + .body(axum::body::Body::from(response_body.as_str().to_owned())) .unwrap() .into_response() } @@ -408,6 +423,284 @@ async fn spawn_mock_vllm_sse() -> (String, tokio::task::JoinHandle<()>) { (format!("http://{addr}"), handle) } +async fn spawn_tool_search_sse_sequence( + responses: Vec, +) -> (String, Arc>>, tokio::task::JoinHandle<()>) { + let responses = Arc::new(Mutex::new(VecDeque::from(responses))); + let requests = Arc::new(Mutex::new(Vec::new())); + let route_responses = Arc::clone(&responses); + let route_requests = Arc::clone(&requests); + let app = Router::new().route( + "/v1/responses", + post(move |body: Bytes| { + let route_responses = Arc::clone(&route_responses); + let route_requests = Arc::clone(&route_requests); + async move { + route_requests + .lock() + .await + .push(serde_json::from_slice(&body).expect("request JSON")); + let response = route_responses.lock().await.pop_front().expect("prepared SSE response"); + axum::response::Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "text/event-stream; charset=utf-8") + .body(axum::body::Body::from(response)) + .unwrap() + .into_response() + } + }), + ); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let handle = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() }); + (format!("http://{addr}"), requests, handle) +} + +fn tool_search_sse() -> String { + let events = [ + serde_json::json!({ + "type":"response.created", + "response":{ + "id":"up_search","status":"in_progress", + "tools":[{"type":"function","name":"tool_search","parameters":{"type":"object"}}] + } + }), + serde_json::json!({ + "type":"response.output_item.added","output_index":0, + "item":{"id":"fc_search","type":"function_call","status":"in_progress", + "name":"tool_search","call_id":"call_search","arguments":""} + }), + serde_json::json!({ + "type":"response.function_call_arguments.delta","output_index":0, + "item_id":"fc_search","delta":"{\"query\":\"weather\"}" + }), + serde_json::json!({ + "type":"response.function_call_arguments.done","output_index":0, + "item_id":"fc_search","name":"tool_search","arguments":"{\"query\":\"weather\"}" + }), + serde_json::json!({ + "type":"response.output_item.done","output_index":0, + "item":{"id":"fc_search","type":"function_call","status":"completed", + "name":"tool_search","call_id":"call_search","arguments":"{\"query\":\"weather\"}"} + }), + serde_json::json!({"type":"response.completed","response":{"id":"up_search","status":"completed","usage":null}}), + ]; + encode_sse_events(events) +} + +fn encode_sse_events(events: impl IntoIterator) -> String { + let mut response = String::new(); + for event in events { + writeln!(&mut response, "data: {event}\n").expect("writing to String cannot fail"); + } + response.push_str("data: [DONE]\n\n"); + response +} + +fn function_call_sse(name: &str, item_id: &str, call_id: &str, arguments: &str) -> String { + let events = [ + serde_json::json!({ + "type":"response.created", + "response":{ + "id":"up_call","status":"in_progress", + "tools":[{"type":"function","name":name,"parameters":{"type":"object"}}] + } + }), + serde_json::json!({ + "type":"response.output_item.added","output_index":0, + "item":{"id":item_id,"type":"function_call","status":"in_progress", + "name":name,"call_id":call_id,"arguments":""} + }), + serde_json::json!({ + "type":"response.output_item.done","output_index":0, + "item":{"id":item_id,"type":"function_call","status":"completed", + "name":name,"call_id":call_id,"arguments":arguments} + }), + serde_json::json!({"type":"response.completed","response":{"id":"up_call","status":"completed","usage":null}}), + ]; + encode_sse_events(events) +} + +fn final_message_sse() -> String { + let events = [ + serde_json::json!({"type":"response.created","response":{"id":"up_final","status":"in_progress"}}), + serde_json::json!({ + "type":"response.output_item.added","output_index":0, + "item":{"id":"msg_final","type":"message","role":"assistant","status":"in_progress","content":[]} + }), + serde_json::json!({ + "type":"response.output_text.delta","output_index":0,"content_index":0, + "item_id":"msg_final","delta":"PARIS_WEATHER_OK" + }), + serde_json::json!({"type":"response.completed","response":{"id":"up_final","status":"completed","usage":null}}), + ]; + encode_sse_events(events) +} + +fn assert_public_search_sse( + first_events: &[serde_json::Value], + deferred_weather: &serde_json::Value, +) -> serde_json::Value { + let public_tools = serde_json::json!([ + { + "type":"tool_search","execution":"client","description":"Search tools", + "parameters":{"type":"object","properties":{"query":{"type":"string"}}} + }, + deferred_weather + ]); + let response_tool_envelopes = first_events + .iter() + .filter_map(|event| event.get("response")) + .filter_map(|response| response.get("tools")) + .collect::>(); + assert!(!response_tool_envelopes.is_empty()); + assert!(response_tool_envelopes.iter().all(|tools| *tools == &public_tools)); + assert!(first_events.iter().all(|event| { + event["response"]["tools"].as_array().is_none_or(|tools| { + tools + .iter() + .all(|tool| !(tool["type"] == "function" && tool["name"] == "tool_search")) + }) + })); + assert_eq!( + first_events + .iter() + .map(|event| event["sequence_number"].as_u64()) + .collect::>(), + (0..u64::try_from(first_events.len()).unwrap()) + .map(Some) + .collect::>() + ); + assert!(first_events.iter().all(|event| { + !matches!( + event["type"].as_str(), + Some("response.function_call_arguments.delta" | "response.function_call_arguments.done" | "error") + ) + })); + let search_lifecycle = first_events + .iter() + .filter(|event| { + matches!( + event["type"].as_str(), + Some("response.output_item.added" | "response.output_item.done") + ) + }) + .collect::>(); + assert_eq!(search_lifecycle.len(), 2); + assert_eq!(search_lifecycle[0]["item"]["type"], "tool_search_call"); + assert_eq!(search_lifecycle[0]["item"]["status"], "in_progress"); + assert_eq!(search_lifecycle[0]["item"]["arguments"], serde_json::json!({})); + assert_eq!(search_lifecycle[1]["item"]["type"], "tool_search_call"); + assert_eq!(search_lifecycle[1]["item"]["status"], "completed"); + assert_eq!( + search_lifecycle[1]["item"]["arguments"], + serde_json::json!({"query":"weather"}) + ); + assert_eq!(search_lifecycle[0]["item"]["id"], search_lifecycle[1]["item"]["id"]); + assert_eq!( + search_lifecycle[0]["item"]["call_id"], + search_lifecycle[1]["item"]["call_id"] + ); + assert_eq!(search_lifecycle[0]["output_index"], search_lifecycle[1]["output_index"]); + let search_call = first_events.last().expect("first terminal")["response"]["output"][0].clone(); + assert_eq!(search_call["type"], "tool_search_call"); + assert_eq!(search_call["id"], "tsc_search"); + assert_eq!(search_call, search_lifecycle[1]["item"]); + search_call +} + +#[tokio::test] +async fn test_http_sse_tool_search_three_request_continuation_stays_public() { + let (llm_url, requests, _llm) = spawn_tool_search_sse_sequence(vec![ + tool_search_sse(), + function_call_sse("get_weather", "fc_weather", "call_weather", "{\"city\":\"Paris\"}"), + final_message_sse(), + ]) + .await; + let (gateway_url, _gateway) = spawn_gateway(test_state(&test_config(&llm_url))).await; + let client = reqwest::Client::new(); + let deferred_weather = serde_json::json!({ + "type":"function","name":"get_weather","description":"Get weather", + "parameters":{"type":"object","properties":{"city":{"type":"string"}}}, + "defer_loading":true + }); + let first_response = client + .post(format!("{gateway_url}/v1/responses")) + .json(&serde_json::json!({ + "model":"test","input":"find weather","store":false,"stream":true,"parallel_tool_calls":false, + "tools":[ + {"type":"tool_search","execution":"client","description":"Search tools", + "parameters":{"type":"object","properties":{"query":{"type":"string"}}}}, + deferred_weather.clone() + ] + })) + .send() + .await + .expect("first response"); + assert_eq!(first_response.status(), StatusCode::OK); + let first_body = first_response.text().await.expect("first SSE body"); + let first_events = sse_events(&first_body); + let search_call = assert_public_search_sse(&first_events, &deferred_weather); + + let search_output = serde_json::json!({ + "type":"tool_search_output","call_id":"call_search","tools":[deferred_weather] + }); + let second_response = client + .post(format!("{gateway_url}/v1/responses")) + .json(&serde_json::json!({ + "model":"test","input":[search_call.clone(),search_output.clone()],"store":false,"stream":true + })) + .send() + .await + .expect("second response"); + let second_body = second_response.text().await.expect("second SSE body"); + let second_events = sse_events(&second_body); + let weather_call = second_events.last().unwrap()["response"]["output"][0].clone(); + assert_eq!(weather_call["type"], "function_call"); + assert_eq!(weather_call["name"], "get_weather"); + + let third_response = client + .post(format!("{gateway_url}/v1/responses")) + .json(&serde_json::json!({ + "model":"test", + "input":[ + search_call,search_output,weather_call, + {"type":"function_call_output","call_id":"call_weather","output":"sunny"} + ], + "store":false,"stream":true + })) + .send() + .await + .expect("third response"); + let third_events = sse_events(&third_response.text().await.expect("third SSE body")); + assert_eq!( + third_events.last().unwrap()["response"]["output"][0]["content"][0]["text"], + "PARIS_WEATHER_OK" + ); + + let requests = requests.lock().await; + assert_eq!(requests.len(), 3); + assert_eq!(requests[0]["tools"].as_array().map(Vec::len), Some(1)); + assert_eq!(requests[0]["tools"][0]["name"], "tool_search"); + assert!( + requests[1]["tools"] + .as_array() + .unwrap() + .iter() + .any(|tool| tool["name"] == "get_weather") + ); + assert!(requests[1]["input"].as_array().unwrap().iter().any(|item| { + item["type"] == "function_call" && item["name"] == "tool_search" && item["call_id"] == "call_search" + })); + assert!( + requests[2]["input"] + .as_array() + .unwrap() + .iter() + .any(|item| { item["type"] == "function_call_output" && item["call_id"] == "call_weather" }) + ); +} + #[tokio::test] async fn test_store_false_proxies_json_to_vllm() { // Arrange @@ -428,6 +721,206 @@ async fn test_store_false_proxies_json_to_vllm() { assert_eq!(body["id"], "mock_id"); } +#[tokio::test] +async fn test_store_false_manual_tool_search_replay_loads_returned_function() { + let (llm_url, requests, _llm) = spawn_mock_vllm_json_capture_body(serde_json::json!({ + "id": "upstream_loaded_call", + "object": "response", + "status": "completed", + "model": "test", + "created_at": 0, + "output": [{ + "type": "function_call", + "id": "fc_weather_1", + "call_id": "call_weather_1", + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}", + "status": "completed" + }] + })) + .await; + let (gateway_url, _gateway) = spawn_gateway(test_state(&test_config(&llm_url))).await; + + let response = reqwest::Client::new() + .post(format!("{gateway_url}/v1/responses")) + .json(&serde_json::json!({ + "model": "test", + "input": [ + { + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "arguments": {"query": "weather"} + }, + { + "type": "tool_search_output", + "call_id": "call_search_1", + "tools": [{ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"] + }, + "defer_loading": true + }] + } + ], + "tools": [], + "store": false, + "stream": false + })) + .send() + .await + .expect("gateway response"); + + assert_eq!(response.status(), StatusCode::OK); + let body: serde_json::Value = response.json().await.expect("response JSON"); + assert_eq!(body["output"][0]["type"], "function_call"); + assert_eq!(body["output"][0]["name"], "get_weather"); + + let requests = requests.lock().await; + assert_eq!(requests.len(), 1); + assert!(requests[0].get("previous_response_id").is_none()); + assert_eq!(requests[0]["input"][0]["type"], "function_call"); + assert_eq!(requests[0]["input"][0]["name"], "tool_search"); + assert_eq!(requests[0]["input"][0]["call_id"], "call_search_1"); + assert_eq!(requests[0]["input"][1]["type"], "function_call_output"); + assert_eq!(requests[0]["input"][1]["call_id"], "call_search_1"); + assert_eq!(requests[0]["tools"].as_array().map(Vec::len), Some(1)); + assert_eq!(requests[0]["tools"][0]["name"], "get_weather"); + assert!(requests[0]["tools"][0].get("defer_loading").is_none()); +} + +#[tokio::test] +async fn test_store_false_fresh_tool_search_lowers_and_translates_blocking_call() { + let (llm_url, requests, _llm) = spawn_mock_vllm_json_capture_body(serde_json::json!({ + "id": "upstream_search_call", + "object": "response", + "status": "completed", + "model": "test", + "created_at": 0, + "output": [{ + "type": "function_call", + "id": "fc_search_1", + "call_id": "call_search_1", + "name": "tool_search", + "arguments": "{\"query\":\"weather\"}", + "status": "completed" + }] + })) + .await; + let (gateway_url, _gateway) = spawn_gateway(test_state(&test_config(&llm_url))).await; + + let response = reqwest::Client::new() + .post(format!("{gateway_url}/v1/responses")) + .json(&serde_json::json!({ + "model": "test", + "input": "find a weather tool", + "tools": [ + { + "type": "tool_search", + "execution": "client", + "description": "Search the client catalog", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"] + } + }, + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object"}, + "defer_loading": true + } + ], + "store": false, + "stream": false + })) + .send() + .await + .expect("gateway response"); + + assert_eq!(response.status(), StatusCode::OK); + let body: serde_json::Value = response.json().await.expect("response JSON"); + assert_eq!( + body["output"][0], + serde_json::json!({ + "type": "tool_search_call", + "id": "tsc_search_1", + "call_id": "call_search_1", + "execution": "client", + "arguments": {"query": "weather"}, + "status": "completed" + }) + ); + + let requests = requests.lock().await; + assert_eq!(requests.len(), 1); + assert_eq!(requests[0]["tools"].as_array().map(Vec::len), Some(1)); + assert_eq!(requests[0]["tools"][0]["type"], "function"); + assert_eq!(requests[0]["tools"][0]["name"], "tool_search"); + assert!( + requests[0]["tools"] + .as_array() + .is_some_and(|tools| tools.iter().all(|tool| tool["name"] != "get_weather")), + "deferred function schema must not be a top-level private tool" + ); + assert!( + requests[0]["tools"][0]["description"] + .as_str() + .is_some_and(|description| description.contains("get_weather")), + "safe synthetic catalog keeps function identity" + ); +} + +#[tokio::test] +async fn test_blocking_tool_search_rejects_invalid_upstream_arguments_as_bad_gateway() { + let (llm_url, _requests, _llm) = spawn_mock_vllm_json_capture_body(serde_json::json!({ + "id": "upstream_bad_search_call", + "object": "response", + "status": "completed", + "model": "test", + "created_at": 0, + "output": [{ + "type": "function_call", + "id": "fc_search_bad", + "call_id": "call_search_bad", + "name": "tool_search", + "arguments": "[]", + "status": "completed" + }] + })) + .await; + let (gateway_url, _gateway) = spawn_gateway(test_state(&test_config(&llm_url))).await; + + let response = reqwest::Client::new() + .post(format!("{gateway_url}/v1/responses")) + .json(&serde_json::json!({ + "model": "test", + "input": "find a tool", + "tools": [{ + "type": "tool_search", + "execution": "client", + "description": "Search", + "parameters": {"type": "object"} + }], + "store": false, + "stream": false + })) + .send() + .await + .expect("gateway response"); + + assert_eq!(response.status(), StatusCode::BAD_GATEWAY); + let body: serde_json::Value = response.json().await.expect("error JSON"); + assert_eq!(body["error"]["type"], "tool_error"); +} + #[tokio::test] async fn test_store_false_with_web_search_reaches_executor() { // Arrange diff --git a/crates/agentic-server/tests/responses_websocket_test.rs b/crates/agentic-server/tests/responses_websocket_test.rs index e2660a08..3ac10770 100644 --- a/crates/agentic-server/tests/responses_websocket_test.rs +++ b/crates/agentic-server/tests/responses_websocket_test.rs @@ -593,6 +593,86 @@ fn sse_function_call_response(response_id: &str, call_name: &str) -> String { format!("data: {created}\n\ndata: {added}\n\ndata: {done}\n\ndata: {completed}\n\ndata: [DONE]\n\n") } +fn sse_tool_search_call_response() -> String { + let created = json!({ + "type": "response.created", + "response": { + "id": "resp_search", "status": "in_progress", + "tools": [{"type": "function", "name": "tool_search", "parameters": {"type": "object"}}] + } + }); + let in_progress = json!({ + "type": "response.in_progress", + "response": { + "id": "resp_search", "status": "in_progress", + "tools": [{"type": "function", "name": "tool_search", "parameters": {"type": "object"}}] + } + }); + let added = json!({ + "type": "response.output_item.added", "output_index": 0, + "item": {"id": "fc_search", "type": "function_call", "status": "in_progress", + "name": "tool_search", "call_id": "call_search", "arguments": ""} + }); + let delta = json!({ + "type": "response.function_call_arguments.delta", "output_index": 0, + "item_id": "fc_search", "delta": "{\"query\":\"weather\"}" + }); + let arguments_done = json!({ + "type": "response.function_call_arguments.done", "output_index": 0, + "item_id": "fc_search", "name": "tool_search", "arguments": "{\"query\":\"weather\"}" + }); + let done = json!({ + "type": "response.output_item.done", "output_index": 0, + "item": {"id": "fc_search", "type": "function_call", "status": "completed", + "name": "tool_search", "call_id": "call_search", "arguments": "{\"query\":\"weather\"}"} + }); + let completed = json!({ + "type": "response.completed", "response": {"id": "resp_search", "status": "completed", "usage": null} + }); + format!( + "data: {created}\n\ndata: {in_progress}\n\ndata: {added}\n\ndata: {delta}\n\ndata: {arguments_done}\n\ndata: {done}\n\ndata: {completed}\n\ndata: [DONE]\n\n" + ) +} + +fn sse_weather_function_call_response() -> String { + let created = json!({ + "type": "response.created", "response": {"id": "resp_weather", "status": "in_progress"} + }); + let added = json!({ + "type": "response.output_item.added", "output_index": 0, + "item": {"id": "fc_weather", "type": "function_call", "status": "in_progress", + "name": "get_weather", "call_id": "call_weather", "arguments": ""} + }); + let done = json!({ + "type": "response.output_item.done", "output_index": 0, + "item": {"id": "fc_weather", "type": "function_call", "status": "completed", + "name": "get_weather", "call_id": "call_weather", "arguments": "{\"city\":\"Paris\"}"} + }); + let completed = json!({ + "type": "response.completed", "response": {"id": "resp_weather", "status": "completed", "usage": null} + }); + format!("data: {created}\n\ndata: {added}\n\ndata: {done}\n\ndata: {completed}\n\ndata: [DONE]\n\n") +} + +fn tool_search_declaration() -> Value { + json!({ + "type": "tool_search", + "execution": "client", + "description": "Search the client catalog", + "parameters": {"type": "object", "properties": {"query": {"type": "string"}}} + }) +} + +fn deferred_weather_function() -> Value { + json!({ + "type": "function", + "name": "get_weather", + "description": "Get weather", + "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}, + "defer_loading": true + }) +} + fn sse_custom_tool_call_response() -> String { let created = json!({ "type": "response.created", @@ -825,6 +905,295 @@ async fn test_websocket_generate_false_prewarm_redacts_mcp_runtime_credentials() assert!(tool.authorization.is_none()); } +#[tokio::test] +async fn test_websocket_generate_false_rejects_mismatched_search_output_without_persistence() { + let mock = MockResponsesServer::start(vec![]).await; + let fixture = storage_backed_state(&mock.url).await; + let (gateway_url, _gateway) = spawn_gateway(fixture.state.clone()).await; + let mut ws = connect_responses_ws(&gateway_url).await; + + send_json( + &mut ws, + json!({ + "type": "response.create", + "model": "test-model", + "input": [ + { + "type": "tool_search_call", + "id": "tsc_1", + "call_id": "call_search_1", + "arguments": {"query": "weather"} + }, + { + "type": "tool_search_output", + "call_id": "call_mismatch", + "tools": [{ + "type": "mcp", + "server_label": "private", + "server_url": "https://mcp.example.test/mcp", + "headers": {"Authorization": "Bearer must-not-persist"} + }] + } + ], + "tools": [], + "generate": false, + "store": false, + "stream": true + }), + ) + .await; + + let events = recv_until_completed(&mut ws).await; + let error = events.last().expect("terminal tool-search error"); + assert_eq!(error["type"], "error"); + assert_eq!(error["status"], StatusCode::BAD_REQUEST.as_u16()); + assert_eq!(error["error"]["type"], "invalid_request_error"); + assert!(mock.request_bodies().await.is_empty()); + + let response_count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM responses") + .fetch_one(fixture.pool.as_ref()) + .await + .expect("response count"); + let item_count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM items") + .fetch_one(fixture.pool.as_ref()) + .await + .expect("item count"); + assert_eq!(response_count, 0, "fail-closed request must not persist a response"); + assert_eq!(item_count, 0, "fail-closed request must not persist tool-search items"); +} + +fn assert_public_ws_search_lifecycle(first: &[Value]) -> String { + let expected_public_tools = json!([tool_search_declaration(), deferred_weather_function()]); + let response_tool_envelopes = first + .iter() + .filter_map(|event| event.get("response")) + .filter_map(|response| response.get("tools")) + .collect::>(); + assert!(!response_tool_envelopes.is_empty()); + assert!( + response_tool_envelopes + .iter() + .all(|tools| *tools == &expected_public_tools) + ); + assert!(first.iter().all(|event| { + event["response"]["tools"].as_array().is_none_or(|tools| { + tools + .iter() + .all(|tool| !(tool["type"] == "function" && tool["name"] == "tool_search")) + }) + })); + assert_eq!( + first + .iter() + .map(|event| event["sequence_number"].as_u64()) + .collect::>(), + (0..u64::try_from(first.len()).unwrap()).map(Some).collect::>() + ); + assert!(first.iter().all(|event| { + !matches!( + event["type"].as_str(), + Some("response.function_call_arguments.delta" | "response.function_call_arguments.done" | "error") + ) + })); + let lifecycle = first + .iter() + .filter(|event| { + matches!( + event["type"].as_str(), + Some("response.output_item.added" | "response.output_item.done") + ) + }) + .collect::>(); + assert_eq!(lifecycle.len(), 2); + assert_eq!(lifecycle[0]["item"]["type"], "tool_search_call"); + assert_eq!(lifecycle[1]["item"]["type"], "tool_search_call"); + assert_eq!(lifecycle[0]["item"]["id"], lifecycle[1]["item"]["id"]); + assert_eq!(lifecycle[0]["output_index"], lifecycle[1]["output_index"]); + let first_terminal = first.last().expect("first terminal"); + assert_eq!(first_terminal["response"]["output"][0], lifecycle[1]["item"]); + first_terminal["response"]["id"].as_str().unwrap().to_owned() +} + +async fn assert_ws_search_persistence(requests: &[Value], pool: &DbPool) { + assert_eq!(requests.len(), 3); + assert_eq!(requests[0]["tools"].as_array().map(Vec::len), Some(1)); + assert_eq!(requests[0]["tools"][0]["name"], "tool_search"); + assert!( + requests[1]["tools"] + .as_array() + .unwrap() + .iter() + .any(|tool| tool["name"] == "get_weather") + ); + assert!(requests[1]["input"].as_array().unwrap().iter().any(|item| { + item["type"] == "function_call" && item["name"] == "tool_search" && item["call_id"] == "call_search" + })); + assert!( + requests[2]["input"] + .as_array() + .unwrap() + .iter() + .any(|item| { item["type"] == "function_call_output" && item["call_id"] == "call_weather" }) + ); + let stored_items = sqlx::query_scalar::<_, String>("SELECT data FROM items ORDER BY created_at, seq") + .fetch_all(pool) + .await + .expect("stored public items"); + assert!(stored_items.iter().any(|item| item.contains("tool_search_call"))); + assert!(stored_items.iter().any(|item| item.contains("tool_search_output"))); + assert!( + stored_items + .iter() + .all(|item| !item.contains("\"name\":\"tool_search\"")) + ); +} + +#[tokio::test] +async fn test_websocket_tool_search_streaming_continuation_is_public_and_persisted() { + let mock = MockResponsesServer::start(vec![ + sse_tool_search_call_response(), + sse_weather_function_call_response(), + sse_response("resp_final", "msg_final", "PARIS_WEATHER_OK"), + ]) + .await; + let fixture = storage_backed_state(&mock.url).await; + let (gateway_url, _gateway) = spawn_gateway(fixture.state.clone()).await; + let mut ws = connect_responses_ws(&gateway_url).await; + + send_json( + &mut ws, + json!({ + "type": "response.create", + "model": "test-model", + "input": "find weather", + "tools": [tool_search_declaration(), deferred_weather_function()], + "parallel_tool_calls": false, + "store": true, + "stream": true + }), + ) + .await; + let first = recv_until_completed(&mut ws).await; + let first_response_id = assert_public_ws_search_lifecycle(&first); + + send_json( + &mut ws, + json!({ + "type": "response.create", + "model": "test-model", + "previous_response_id": first_response_id, + "input": [{ + "type": "tool_search_output", + "call_id": "call_search", + "tools": [deferred_weather_function()] + }], + "store": true, + "stream": true + }), + ) + .await; + let second = recv_until_completed(&mut ws).await; + let second_terminal = second.last().expect("second terminal"); + assert_eq!(second_terminal["response"]["output"][0]["type"], "function_call"); + assert_eq!(second_terminal["response"]["output"][0]["name"], "get_weather"); + let second_response_id = second_terminal["response"]["id"].as_str().unwrap().to_owned(); + + send_json( + &mut ws, + json!({ + "type": "response.create", + "model": "test-model", + "previous_response_id": second_response_id, + "input": [{ + "type": "function_call_output", + "call_id": "call_weather", + "output": "sunny" + }], + "store": true, + "stream": true + }), + ) + .await; + let third = recv_until_completed(&mut ws).await; + assert_eq!( + third.last().unwrap()["response"]["output"][0]["content"][0]["text"], + "PARIS_WEATHER_OK" + ); + + let requests = mock.request_bodies().await; + assert_ws_search_persistence(&requests, fixture.pool.as_ref()).await; +} + +#[tokio::test] +async fn test_websocket_generate_false_persists_valid_tool_search_state_for_reuse() { + let mock = MockResponsesServer::start(vec![sse_weather_function_call_response()]).await; + let fixture = storage_backed_state(&mock.url).await; + let (gateway_url, _gateway) = spawn_gateway(fixture.state.clone()).await; + let mut ws = connect_responses_ws(&gateway_url).await; + + send_json( + &mut ws, + json!({ + "type": "response.create", + "model": "test-model", + "input": [ + { + "type": "tool_search_call", "id": "tsc_prewarm", "call_id": "call_prewarm", + "arguments": {"query": "weather"} + }, + { + "type": "tool_search_output", "call_id": "call_prewarm", + "tools": [deferred_weather_function()] + } + ], + "tools": [tool_search_declaration(), deferred_weather_function()], + "parallel_tool_calls": false, + "generate": false, + "store": false, + "stream": true + }), + ) + .await; + let prewarm = recv_until_completed(&mut ws).await; + assert!( + mock.request_bodies().await.is_empty(), + "generate:false must not run inference" + ); + let response_id = prewarm.last().unwrap()["response"]["id"].as_str().unwrap().to_owned(); + + send_json( + &mut ws, + json!({ + "type": "response.create", + "model": "test-model", + "previous_response_id": response_id, + "input": "call the loaded weather tool", + "store": true, + "stream": true + }), + ) + .await; + let response = recv_until_completed(&mut ws).await; + assert_eq!(response.last().unwrap()["response"]["output"][0]["name"], "get_weather"); + + let requests = mock.request_bodies().await; + assert_eq!(requests.len(), 1); + assert!( + requests[0]["tools"] + .as_array() + .unwrap() + .iter() + .any(|tool| tool["name"] == "get_weather") + ); + assert!( + requests[0]["input"] + .as_array() + .unwrap() + .iter() + .any(|item| item["type"] == "function_call_output") + ); +} + #[tokio::test] async fn test_websocket_first_turn_forwards_incremental_events_and_final_payload() { let mock = MockResponsesServer::start(vec![sse_response("resp_upstream_1", "msg_upstream_1", "HELLO")]).await;