diff --git a/README.md b/README.md index 46898fb..51e9767 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,34 @@ Then launch Codex: codex --disable image_generation -c model_provider=agentic-api -m Qwen/Qwen3-30B-A3B-FP8 ``` +## πŸ§‘β€πŸ’» Claude Code on your own GPUs + +Agentic API serves the Anthropic Messages protocol at `/v1/messages`, so Claude Code (CLI or Agent SDK) runs against open models. Point it at the gateway: + +```bash +export ANTHROPIC_BASE_URL="http://localhost:9000" +export ANTHROPIC_API_KEY="" +export ANTHROPIC_MODEL="Qwen/Qwen3-30B-A3B-FP8" # match the served model + +claude -p "summarize the files in this directory" +``` + +Claude Code's own tools (Bash, Edit, Read, …) stay **client-owned** β€” Claude Code runs them, as usual. + +### Running Claude Code's web search on the gateway + +Claude Code declares web search as a client tool named `WebSearch`, so by default it runs client-side. To have the gateway execute it instead β€” server-side against your configured search backend, hidden from the model like any gateway tool β€” opt in with one env var when starting the gateway: + +```bash +YOU_API_KEY= YOU_API_BASE_URL= \ +MESSAGES_GATEWAY_TOOL_ALIASES="WebSearch=web_search" \ + cargo run -p agentic-server -- --llm-api-base http://0.0.0.0:5050 +``` + +`MESSAGES_GATEWAY_TOOL_ALIASES` maps a client tool name to a gateway executor (`name=executor`, comma-separated). It is **empty by default** β€” a client function is only executed server-side when you configure it, mirroring the [tool ownership model](#-tool-ownership-model). The gateway adapts Claude Code's `WebSearch` arguments (`allowed_domains`/`blocked_domains`) to the executor's schema automatically. + +> Note: the search executor treats include/exclude domain lists as mutually exclusive, so a `WebSearch` call that sets both `allowed_domains` and `blocked_domains` returns an error result to the model. + ## 🧩 Tool Ownership Model Every tool call has exactly one execution path, so nothing runs by accident: diff --git a/crates/agentic-server-core/src/executor/messages_loop.rs b/crates/agentic-server-core/src/executor/messages_loop.rs new file mode 100644 index 0000000..bb79356 --- /dev/null +++ b/crates/agentic-server-core/src/executor/messages_loop.rs @@ -0,0 +1,193 @@ +//! Messages-native gateway tool loop. +//! +//! Runs the server-side gateway-tool loop for `/v1/messages` **natively**: the +//! client's Anthropic request is forwarded to vLLM `/v1/messages` essentially +//! untouched (preserving every Anthropic field), the assistant turn is +//! inspected, any gateway-owned `tool_use` is executed server-side and hidden, +//! the loop appends the `tool_result` and re-POSTs, until the model stops asking +//! for a gateway tool. Only the final assistant message reaches the client. +//! +//! This never touches `RequestPayload`/`ResponsePayload`; it reuses only the +//! protocol-neutral tool layer (`ToolRegistry::dispatch`) via +//! [`crate::types::messages::tool_seam`]. Non-streaming only; streaming lives in +//! `messages_stream`. + +use std::time::Duration; + +use futures::future::join_all; +use serde_json::{Value, json}; + +use crate::executor::error::{ExecutorError, ExecutorResult}; +use crate::executor::inference::fetch_response_json; +use crate::executor::request::ExecutionContext; +use crate::tool::ToolRegistry; +use crate::types::messages::tool_seam; +use crate::utils::common::{deserialize_from_str, serialize_to_string}; + +/// Max gateway rounds before the loop gives up. Each round is one upstream +/// `/v1/messages` call. Shared with the streaming loop (`messages_stream`). +/// Kept in sync with the Responses loop's `engine::MAX_GATEWAY_TOOL_ROUNDS` +/// (a future Layering-ADR consolidation would unify these). +pub(super) const MAX_GATEWAY_TOOL_ROUNDS: usize = 10; + +/// Per gateway-tool-call timeout β€” a hung tool becomes an error `tool_result` +/// fed back to the model, never a whole-request failure (edge E5). Shared with +/// the streaming loop; matches the Responses loop's `gateway::GATEWAY_TOOL_TIMEOUT`. +pub(super) const GATEWAY_TOOL_TIMEOUT: Duration = Duration::from_secs(60); + +/// The `tool_result` block for one executed gateway call, fed back next round. +/// (The model's own `tool_use` block is carried forward via the preserved +/// assistant content, not reconstructed here β€” see `append_round_to_history`.) +struct ResolvedCall { + tool_result_block: Value, +} + +/// Run the Messages-native gateway tool loop and return the final assistant +/// message (Anthropic JSON `Value`). +/// +/// `request` is the client's parsed request body as JSON β€” forwarded upstream +/// with `stream:false` forced and its `messages` extended each round. +/// +/// # Errors +/// Returns [`ExecutorError`] on upstream failure or unparseable upstream JSON. +/// Gateway-tool execution failures do **not** error β€” they become error +/// `tool_result`s fed back to the model. +pub async fn run_messages_loop( + mut request: Value, + registry: &ToolRegistry, + exec_ctx: &ExecutionContext, + auth: Option<&str>, +) -> ExecutorResult { + let url = format!("{}/v1/messages", exec_ctx.llm_base_url); + // The loop drives turns itself; force non-streaming upstream regardless of + // what the client asked (the handler routes streaming elsewhere). + request["stream"] = Value::Bool(false); + + for _round in 0..MAX_GATEWAY_TOOL_ROUNDS { + let body = serialize_to_string(&request).map_err(ExecutorError::JsonError)?; + let resp_text = fetch_response_json(body, &url, &exec_ctx.client, auth).await?; + let message: Value = deserialize_from_str(&resp_text).map_err(ExecutorError::JsonError)?; + + // Any error body from upstream is surfaced verbatim (handler maps it to + // the Anthropic error envelope). + if message.get("type").and_then(Value::as_str) == Some("error") { + return Ok(message); + } + + let content = message.get("content").and_then(Value::as_array); + let stop_reason = message.get("stop_reason").and_then(Value::as_str); + + // Split the assistant turn into gateway-owned tool_use vs everything the + // client should see. A client-owned tool_use means we cannot continue + // the loop server-side β€” return the turn to the client (edge E7). + let Some(content) = content else { + return Ok(message); + }; + let gateway_map = &exec_ctx.messages_gateway_tools; + let mut gateway_calls: Vec = Vec::new(); + let mut has_client_tool_use = false; + for block in content { + if block.get("type").and_then(Value::as_str) == Some("tool_use") { + let name = block.get("name").and_then(Value::as_str).unwrap_or_default(); + if gateway_map.is_gateway_owned(name) { + gateway_calls.push(block.clone()); + } else { + has_client_tool_use = true; + } + } + } + + // Terminal when the model didn't ask for a gateway tool, or stopped for + // another reason. A client-owned tool_use is also terminal (the client + // must run it) β€” but the gateway tool_use, if any, must still be hidden + // (F5): strip gateway blocks from the client-facing content. + if gateway_calls.is_empty() || stop_reason != Some("tool_use") { + return Ok(message); + } + if has_client_tool_use { + // Strip the gateway tool_use from the client-facing content (compute + // before mutating to end the immutable borrow of `message`). + let stripped = tool_seam::strip_gateway_tool_use(content, gateway_map); + let mut message = message; + message["content"] = Value::Array(stripped); + return Ok(message); + } + + // Pure gateway-tool round: execute the calls, then feed the model's FULL + // assistant turn (thinking/text/tool_use, order preserved β€” F3) plus the + // tool_results back for the next round. Gateway blocks stay internal. + let assistant_content = content.clone(); + let resolved = execute_gateway_calls(&gateway_calls, registry, gateway_map).await; + append_round_to_history(&mut request, &assistant_content, &resolved); + } + + // Round budget exhausted β€” re-run once more is not attempted; return the + // last message. (Open Q1: a dedicated pause_turn signal could go here.) + // Reaching here means every round emitted a gateway tool_use; surface a + // minimal terminal so the client isn't left hanging. + Ok(json!({ + "type": "error", + "error": { + "type": "api_error", + "message": format!("gateway tool loop exceeded {MAX_GATEWAY_TOOL_ROUNDS} rounds") + } + })) +} + +/// Execute the gateway-owned `tool_use` blocks concurrently, each bounded by the +/// per-call timeout. A failure or timeout becomes an error `tool_result` (E5). +async fn execute_gateway_calls( + gateway_calls: &[Value], + registry: &ToolRegistry, + gateway_map: &tool_seam::GatewayToolMap, +) -> Vec { + let futures = gateway_calls.iter().map(|block| async move { + let id = block.get("id").and_then(Value::as_str).unwrap_or_default(); + let name = block.get("name").and_then(Value::as_str).unwrap_or_default(); + + // F4: reject a malformed/absent input rather than dispatching with args + // the model never supplied. The block's `input` is already-parsed JSON + // here (non-streaming), so validate it's an object. + let input = block.get("input").cloned().unwrap_or(Value::Null); + let (output, is_error) = if input.is_object() { + let call = tool_seam::tool_use_to_call(id, name, &input, gateway_map); + match tokio::time::timeout(GATEWAY_TOOL_TIMEOUT, registry.dispatch(&call)).await { + Ok(Some(result)) => match result.output { + Ok(tool_output) => (tool_output.output, false), + Err(e) => (format!("tool execution failed: {e}"), true), + }, + Ok(None) => (format!("no handler for tool '{name}'"), true), + Err(_) => ( + format!("gateway tool '{name}' timed out after {GATEWAY_TOOL_TIMEOUT:?}"), + true, + ), + } + } else { + ( + "invalid tool arguments (not a JSON object); tool was not run".to_owned(), + true, + ) + }; + + ResolvedCall { + tool_result_block: tool_seam::tool_result_block(id, &output, is_error), + } + }); + join_all(futures).await +} + +/// Append the model's assistant turn (preserving its `thinking`/`text`/`tool_use` +/// blocks in order β€” F3) and a following user turn of `tool_result`s to the +/// request `messages`, so the next upstream round sees the full conversation +/// state. These stay internal β€” the client never sees them (hide-the-call). +fn append_round_to_history(request: &mut Value, assistant_content: &[Value], resolved: &[ResolvedCall]) { + let assistant = json!({ "role": "assistant", "content": assistant_content }); + let user = json!({ + "role": "user", + "content": resolved.iter().map(|r| r.tool_result_block.clone()).collect::>() + }); + if let Some(messages) = request.get_mut("messages").and_then(Value::as_array_mut) { + messages.push(assistant); + messages.push(user); + } +} diff --git a/crates/agentic-server-core/src/executor/messages_stream.rs b/crates/agentic-server-core/src/executor/messages_stream.rs new file mode 100644 index 0000000..88f6ab2 --- /dev/null +++ b/crates/agentic-server-core/src/executor/messages_stream.rs @@ -0,0 +1,675 @@ +//! Streaming Messages-native gateway tool loop. +//! +//! Consumes vLLM's per-round Anthropic SSE and presents the client **one** +//! logical message across all gateway rounds: +//! * `message_start` emitted once (first round only); +//! * surfaced `content_block_*` forwarded with client-visible indices rebased +//! contiguously across rounds; +//! * gateway-owned `tool_use` blocks suppressed (and their `input_json_delta` +//! buffered to reconstruct the call for dispatch); +//! * intermediate `message_delta`/`message_stop` (the per-round terminals) +//! suppressed; the final round's terminal is forwarded once. +//! +//! Structurally the Anthropic-native analogue of the Responses `GatewayStreamAccumulator` +//! (#119/#132); kept deliberately parallel for a future consolidation. Reuses +//! only the neutral tool layer via [`crate::types::messages::tool_seam`]. + +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::sync::Arc; +use std::time::Duration; + +use async_stream::stream; +use futures::StreamExt; +use serde_json::{Value, json}; + +use crate::executor::inference::{BoxStream, call_inference}; +use crate::executor::request::ExecutionContext; +use crate::tool::ToolRegistry; +use crate::types::messages::tool_seam; +use crate::utils::common::serialize_to_string; + +// Shared with the non-streaming loop so the two Messages loops can't drift. +use crate::executor::messages_loop::{GATEWAY_TOOL_TIMEOUT, MAX_GATEWAY_TOOL_ROUNDS}; +/// vLLM streaming chunk timeout (per line). Generous β€” the loop's own budget is +/// the round cap, not this. +const CHUNK_TIMEOUT: Duration = Duration::from_secs(120); + +/// Drive the streaming Messages-native loop, yielding Anthropic SSE lines for +/// the client. Owns the multi-round β†’ single-message accumulation. +#[must_use] +pub fn run_messages_stream( + mut request: Value, + registry: Arc, + exec_ctx: Arc, + auth: Option, +) -> BoxStream { + let url = format!("{}/v1/messages", exec_ctx.llm_base_url); + request["stream"] = Value::Bool(true); + + Box::pin(stream! { + let mut acc = MessagesStreamAccumulator::new(exec_ctx.messages_gateway_tools.clone()); + + for _round in 0..MAX_GATEWAY_TOOL_ROUNDS { + let body = match serialize_to_string(&request) { + Ok(b) => b, + Err(e) => { yield error_sse(&e.to_string()); return; } + }; + let mut upstream = Box::pin(call_inference( + body, url.clone(), Arc::clone(&exec_ctx.client), auth.clone(), CHUNK_TIMEOUT, + )); + + acc.begin_round(); + while let Some(line) = upstream.next().await { + let line = match line { + Ok(l) => l, + Err(e) => { yield error_sse(&e.to_string()); return; } + }; + for out in acc.push(&line) { + yield out; + } + } + + // Round finished. Continue only for a pure gateway-tool round; a + // client-owned tool_use (or any non-tool_use stop) is terminal. + if !acc.should_continue_loop() { + for out in acc.finish() { + yield out; + } + return; + } + // Reconstruct the FULL assistant turn (thinking/text/signature + + // gateway tool_use, in order) for the next round's history β€” not just + // the gateway tool_use (F3, streaming half). The gateway calls are + // derived from the same buffered blocks for dispatch. + let (assistant_content, calls) = acc.take_round(); + let resolved = execute_gateway_calls(&calls, ®istry, &exec_ctx.messages_gateway_tools).await; + append_round_to_history(&mut request, &assistant_content, &resolved); + } + + // Round budget exhausted. + yield error_sse(&format!("gateway tool loop exceeded {MAX_GATEWAY_TOOL_ROUNDS} rounds")); + }) +} + +/// A gateway `tool_use` reconstructed from the stream, ready to dispatch. +struct StreamedCall { + id: String, + name: String, + input_json: String, +} + +/// One assistant content block buffered across a round, so the full turn +/// (`thinking`/`text`/`signature`/`tool_use`, in order) can be reconstructed for +/// the next round's history β€” F3. The client-facing SSE is still forwarded live; +/// this is a parallel record for the fed-back conversation state. +struct BufferedBlock { + /// The `content_block` skeleton from `content_block_start`, mutated by deltas. + block: Value, + /// Accumulated `input_json_delta` fragments for a `tool_use` block. + input_json: String, + /// Gateway-owned `tool_use` (drives the loop; suppressed from the client). + is_gateway_tool: bool, +} + +impl BufferedBlock { + fn apply_delta(&mut self, delta: &Value) { + match delta.get("type").and_then(Value::as_str) { + Some("text_delta") => append_str(&mut self.block, "text", delta.get("text")), + Some("thinking_delta") => append_str(&mut self.block, "thinking", delta.get("thinking")), + Some("signature_delta") => append_str(&mut self.block, "signature", delta.get("signature")), + Some("input_json_delta") => { + if let Some(partial) = delta.get("partial_json").and_then(Value::as_str) { + self.input_json.push_str(partial); + } + } + _ => {} + } + } + + /// The finished assistant content block. For `tool_use`, parse the + /// accumulated arguments (best-effort β€” a malformed fragment falls back to + /// `{}`; the paired error `tool_result` records the failure). + fn to_block(&self) -> Value { + let mut block = self.block.clone(); + if block.get("type").and_then(Value::as_str) == Some("tool_use") { + block["input"] = tool_seam::parse_tool_input(&self.input_json).unwrap_or_else(|_| json!({})); + } + block + } +} + +/// Append a streamed string fragment onto a string field of `block`, creating it +/// if absent. +fn append_str(block: &mut Value, field: &str, fragment: Option<&Value>) { + let Some(fragment) = fragment.and_then(Value::as_str) else { + return; + }; + let combined = match block.get(field).and_then(Value::as_str) { + Some(existing) => format!("{existing}{fragment}"), + None => fragment.to_owned(), + }; + block[field] = Value::from(combined); +} + +/// State machine that turns per-round Anthropic SSE into one client-visible +/// message. Fed line-by-line via [`Self::push`]. +struct MessagesStreamAccumulator { + message_started: bool, + /// Next client-visible block index (contiguous across rounds). + next_index: u32, + /// Map upstream (per-round) block index β†’ client index, for the blocks we + /// forward this round. Cleared each round. + index_map: HashMap, + /// Upstream indices belonging to a suppressed gateway `tool_use` this round. + suppressed_indices: HashSet, + /// Every assistant block this round, keyed by upstream index (ordered), so + /// the full turn β€” `thinking`/`text`/`signature` + gateway `tool_use` β€” can + /// be reconstructed for the next round's history (F3). Cleared each round. + blocks: BTreeMap, + /// Did this round end with `stop_reason: tool_use`? + ended_on_tool_use: bool, + /// Did this round surface a client-owned `tool_use`? If so the loop cannot + /// continue server-side (the client must supply that tool's result), so it + /// is terminal β€” matching the non-streaming path's E7 handling. + has_client_tool_use: bool, + /// Buffered terminal `message_delta` from the final round (emitted by `finish`). + final_message_delta: Option, + /// Operator-configured client-tool β†’ gateway-executor aliases, so a client + /// tool like Claude Code's `WebSearch` is classified gateway-owned (and + /// suppressed) the same way the built-in `web_search` is. + gateway_map: tool_seam::GatewayToolMap, +} + +impl MessagesStreamAccumulator { + fn new(gateway_map: tool_seam::GatewayToolMap) -> Self { + Self { + message_started: false, + next_index: 0, + index_map: HashMap::new(), + suppressed_indices: HashSet::new(), + blocks: BTreeMap::new(), + ended_on_tool_use: false, + has_client_tool_use: false, + final_message_delta: None, + gateway_map, + } + } + + fn begin_round(&mut self) { + self.index_map.clear(); + self.suppressed_indices.clear(); + self.blocks.clear(); + self.ended_on_tool_use = false; + self.has_client_tool_use = false; + // F6: clear the previous round's terminal so a clean-EOF round can't + // re-emit a stale stop_reason. + self.final_message_delta = None; + } + + /// Number of gateway `tool_use` blocks buffered this round. + fn gateway_call_count(&self) -> usize { + self.blocks.values().filter(|b| b.is_gateway_tool).count() + } + + /// Consume this round's buffered blocks, returning (full assistant content in + /// order, gateway calls to dispatch). The assistant content preserves + /// `thinking`/`text`/`signature` and the gateway `tool_use` blocks (F3); the + /// calls are the gateway `tool_use` blocks reconstructed for dispatch. + fn take_round(&mut self) -> (Vec, Vec) { + let blocks = std::mem::take(&mut self.blocks); + let mut assistant_content = Vec::with_capacity(blocks.len()); + let mut calls = Vec::new(); + for buffered in blocks.values() { + assistant_content.push(buffered.to_block()); + if buffered.is_gateway_tool { + calls.push(StreamedCall { + id: buffered.block["id"].as_str().unwrap_or_default().to_owned(), + name: buffered.block["name"].as_str().unwrap_or_default().to_owned(), + input_json: buffered.input_json.clone(), + }); + } + } + (assistant_content, calls) + } + + /// The loop should continue only when the round asked for a gateway tool AND + /// did not also surface a client-owned tool (which the client must handle, + /// making the round terminal β€” E7). + fn should_continue_loop(&self) -> bool { + self.ended_on_tool_use && self.gateway_call_count() > 0 && !self.has_client_tool_use + } + + /// Translate one upstream SSE line into zero or more client SSE lines. + fn push(&mut self, line: &str) -> Vec { + let Some(data) = line.strip_prefix("data: ") else { + return Vec::new(); + }; + let data = data.trim(); + if data == "[DONE]" { + return Vec::new(); + } + let Ok(mut event) = serde_json::from_str::(data) else { + return Vec::new(); + }; + match event.get("type").and_then(Value::as_str) { + Some("message_start") => self.on_message_start(&event), + Some("content_block_start") => self.on_block_start(&mut event), + Some("content_block_delta") => self.on_block_delta(&mut event), + Some("content_block_stop") => self.on_block_stop(&mut event), + Some("message_delta") => { + // Buffer as the (possibly) final terminal; suppress mid-loop. + self.ended_on_tool_use = event["delta"]["stop_reason"].as_str() == Some("tool_use"); + self.final_message_delta = Some(event); + Vec::new() + } + // `message_stop` (per-round terminal) is suppressed; `finish` emits + // the single client-visible terminal. Everything else is dropped. + _ => Vec::new(), + } + } + + fn on_message_start(&mut self, event: &Value) -> Vec { + if self.message_started { + return Vec::new(); + } + self.message_started = true; + vec![sse("message_start", event)] + } + + fn on_block_start(&mut self, event: &mut Value) -> Vec { + let up_index = event.get("index").and_then(Value::as_u64).unwrap_or(0); + let block_type = event["content_block"]["type"].as_str().unwrap_or_default(); + let name = event["content_block"]["name"].as_str().unwrap_or_default(); + + // Buffer every block for history reconstruction (F3), preserving order. + let is_gateway_tool = block_type == "tool_use" && self.gateway_map.is_gateway_owned(name); + self.blocks.insert( + up_index, + BufferedBlock { + block: event["content_block"].clone(), + input_json: String::new(), + is_gateway_tool, + }, + ); + + if block_type == "tool_use" { + if is_gateway_tool { + // Suppress gateway-owned tool_use from the client; it stays in the + // buffered history only and drives the loop. + self.suppressed_indices.insert(up_index); + return Vec::new(); + } + // A client-owned tool_use: the client must execute it, so this round + // is terminal (E7). Forward it (below) and stop the loop. + self.has_client_tool_use = true; + } + + // Forward with a rebased contiguous client index. + let client_index = self.next_index; + self.next_index += 1; + self.index_map.insert(up_index, client_index); + event["index"] = Value::from(client_index); + vec![sse("content_block_start", event)] + } + + fn on_block_delta(&mut self, event: &mut Value) -> Vec { + let up_index = event.get("index").and_then(Value::as_u64).unwrap_or(0); + // Accumulate the delta into the buffered block (for history β€” F3), + // regardless of whether it is forwarded to the client. + if let Some(buffered) = self.blocks.get_mut(&up_index) { + buffered.apply_delta(&event["delta"]); + } + // A suppressed gateway tool_use is not forwarded to the client (its + // input_json_delta was just buffered above). + if self.suppressed_indices.contains(&up_index) { + return Vec::new(); + } + let Some(&client_index) = self.index_map.get(&up_index) else { + return Vec::new(); + }; + event["index"] = Value::from(client_index); + vec![sse("content_block_delta", event)] + } + + fn on_block_stop(&mut self, event: &mut Value) -> Vec { + let up_index = event.get("index").and_then(Value::as_u64).unwrap_or(0); + if self.suppressed_indices.contains(&up_index) { + return Vec::new(); + } + let Some(&client_index) = self.index_map.get(&up_index) else { + return Vec::new(); + }; + event["index"] = Value::from(client_index); + vec![sse("content_block_stop", event)] + } + + /// Emit the terminal `message_delta` + `message_stop` once, at loop end. + fn finish(&mut self) -> Vec { + let mut out = Vec::new(); + if let Some(delta) = self.final_message_delta.take() { + out.push(sse("message_delta", &delta)); + } + out.push(sse("message_stop", &json!({"type": "message_stop"}))); + out + } +} + +fn sse(event: &str, value: &Value) -> String { + let json = serialize_to_string(value).unwrap_or_default(); + format!("event: {event}\ndata: {json}\n\n") +} + +fn error_sse(message: &str) -> String { + let event = json!({"type": "error", "error": {"type": "api_error", "message": message}}); + let json = serialize_to_string(&event).unwrap_or_default(); + format!("event: error\ndata: {json}\n\n") +} + +/// Execute reconstructed gateway calls (concurrent, per-call timeout). Errors +/// become error `tool_result`s (E5). +async fn execute_gateway_calls( + calls: &[StreamedCall], + registry: &ToolRegistry, + gateway_map: &tool_seam::GatewayToolMap, +) -> Vec { + let futures = calls.iter().map(|c| async move { + // F4: reject a malformed/incomplete reconstructed input rather than + // coercing to {} and dispatching the tool with args the model never sent. + let (output, is_error) = match tool_seam::parse_tool_input(&c.input_json) { + Ok(input) => { + let call = tool_seam::tool_use_to_call(&c.id, &c.name, &input, gateway_map); + match tokio::time::timeout(GATEWAY_TOOL_TIMEOUT, registry.dispatch(&call)).await { + Ok(Some(result)) => match result.output { + Ok(o) => (o.output, false), + Err(e) => (format!("tool execution failed: {e}"), true), + }, + Ok(None) => (format!("no handler for tool '{}'", c.name), true), + Err(_) => ( + format!("gateway tool '{}' timed out after {GATEWAY_TOOL_TIMEOUT:?}", c.name), + true, + ), + } + } + Err(reason) => (format!("{reason}; tool was not run"), true), + }; + ResolvedStreamCall { + tool_result_block: tool_seam::tool_result_block(&c.id, &output, is_error), + } + }); + futures::future::join_all(futures).await +} + +/// The `tool_result` block for one executed gateway call, fed back next round. +/// (The assistant turn β€” including this call's `tool_use` block β€” is reconstructed +/// from the accumulator's buffered blocks in [`MessagesStreamAccumulator::take_round`].) +struct ResolvedStreamCall { + tool_result_block: Value, +} + +/// Append the model's full assistant turn (`thinking`/`text`/`signature` + +/// gateway `tool_use`, order preserved β€” F3) and a following user turn of `tool_result`s, +/// so the next upstream round sees the complete conversation state. These stay +/// internal β€” the client never sees the gateway call (hide-the-call). +fn append_round_to_history(request: &mut Value, assistant_content: &[Value], resolved: &[ResolvedStreamCall]) { + let assistant = json!({ "role": "assistant", "content": assistant_content }); + let user = json!({ + "role": "user", + "content": resolved.iter().map(|r| r.tool_result_block.clone()).collect::>() + }); + if let Some(messages) = request.get_mut("messages").and_then(Value::as_array_mut) { + messages.push(assistant); + messages.push(user); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn line(v: &Value) -> String { + format!("data: {v}") + } + + /// Accumulator with the default gateway map (built-in `web_search` only). + fn acc() -> MessagesStreamAccumulator { + MessagesStreamAccumulator::new(tool_seam::GatewayToolMap::default()) + } + + // A single non-tool round: message_start forwarded once, blocks pass through + // with contiguous indices, terminal emitted by finish(). + #[test] + fn single_round_text_passes_through() { + let mut acc = acc(); + acc.begin_round(); + let mut out = Vec::new(); + out.extend(acc.push(&line(&json!({"type": "message_start", "message": {"id": "m"}})))); + out.extend(acc.push(&line( + &json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}), + ))); + out.extend(acc.push(&line( + &json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "hi"}}), + ))); + out.extend(acc.push(&line(&json!({"type": "content_block_stop", "index": 0})))); + out.extend(acc.push(&line( + &json!({"type": "message_delta", "delta": {"stop_reason": "end_turn"}}), + ))); + out.extend(acc.push(&line(&json!({"type": "message_stop"})))); + assert!(!acc.should_continue_loop(), "text-only round is terminal"); + out.extend(acc.finish()); + let s = out.join(""); + assert_eq!(s.matches("event: message_start").count(), 1); + assert_eq!(s.matches("event: message_stop").count(), 1); + assert!(s.contains("text_delta")); + assert!(s.contains("end_turn")); + } + + // A gateway tool round: the tool_use block (start/delta/stop) is suppressed, + // its input reconstructed, thinking/text forwarded, and no terminal leaks. + #[test] + fn gateway_tool_round_suppresses_tool_use_and_reconstructs_call() { + let mut acc = acc(); + acc.begin_round(); + let mut out = Vec::new(); + out.extend(acc.push(&line(&json!({"type": "message_start", "message": {"id": "m"}})))); + // thinking idx0 (forward) + out.extend(acc.push(&line( + &json!({"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}}), + ))); + out.extend(acc.push(&line(&json!({"type": "content_block_stop", "index": 0})))); + // gateway tool_use idx1 (suppress + reconstruct) + out.extend(acc.push(&line(&json!({"type": "content_block_start", "index": 1, "content_block": {"type": "tool_use", "id": "tid", "name": "web_search", "input": {}}})))); + out.extend(acc.push(&line(&json!({"type": "content_block_delta", "index": 1, "delta": {"type": "input_json_delta", "partial_json": "{\"query\":"}})))); + out.extend(acc.push(&line(&json!({"type": "content_block_delta", "index": 1, "delta": {"type": "input_json_delta", "partial_json": "\"rust\"}"}})))); + out.extend(acc.push(&line(&json!({"type": "content_block_stop", "index": 1})))); + out.extend(acc.push(&line( + &json!({"type": "message_delta", "delta": {"stop_reason": "tool_use"}}), + ))); + out.extend(acc.push(&line(&json!({"type": "message_stop"})))); + + let s = out.join(""); + assert!(acc.should_continue_loop(), "pure gateway-tool round continues the loop"); + assert!(!s.contains("tool_use"), "gateway tool_use must not surface: {s}"); + assert!(!s.contains("message_stop"), "intermediate terminal suppressed"); + assert!(s.contains("thinking"), "thinking forwarded"); + let (_assistant, calls) = acc.take_round(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].name, "web_search"); + assert_eq!(calls[0].input_json, "{\"query\":\"rust\"}"); + } + + // Across two rounds, client-visible block indices stay contiguous (round 1 + // thinking=0, round 2 text=1) β€” no reset/collision. + #[test] + fn indices_are_contiguous_across_rounds() { + let mut acc = acc(); + // round 1: thinking (idx0) + suppressed tool_use (idx1) + acc.begin_round(); + acc.push(&line(&json!({"type": "message_start", "message": {"id": "m"}}))); + acc.push(&line( + &json!({"type": "content_block_start", "index": 0, "content_block": {"type": "thinking"}}), + )); + acc.push(&line(&json!({"type": "content_block_stop", "index": 0}))); + acc.push(&line(&json!({"type": "content_block_start", "index": 1, "content_block": {"type": "tool_use", "name": "web_search", "id": "t"}}))); + acc.push(&line(&json!({"type": "content_block_stop", "index": 1}))); + // round 2: text (upstream idx0) must map to client idx1 + acc.begin_round(); + let out = acc.push(&line( + &json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text"}}), + )); + let started: Value = + serde_json::from_str(out[0].lines().nth(1).unwrap().strip_prefix("data: ").unwrap()).unwrap(); + assert_eq!(started["index"], 1, "round-2 text rebased to contiguous client index 1"); + } + + // E7 (streaming): a round with a gateway tool_use AND a client-owned tool_use + // is terminal β€” the loop must NOT continue (the client owns the second tool). + // The client-owned tool_use is forwarded; the gateway one is suppressed. + #[test] + fn mixed_client_and_gateway_tool_use_stops_the_loop() { + let mut acc = acc(); + acc.begin_round(); + acc.push(&line(&json!({"type": "message_start", "message": {"id": "m"}}))); + // gateway tool_use (idx0) β€” suppressed + acc.push(&line(&json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "name": "web_search", "id": "g"}}))); + acc.push(&line(&json!({"type": "content_block_stop", "index": 0}))); + // client tool_use (idx1) β€” forwarded + let out = acc.push(&line(&json!({"type": "content_block_start", "index": 1, "content_block": {"type": "tool_use", "name": "get_weather", "id": "c"}}))); + acc.push(&line(&json!({"type": "content_block_stop", "index": 1}))); + acc.push(&line( + &json!({"type": "message_delta", "delta": {"stop_reason": "tool_use"}}), + )); + + // Client tool_use surfaces; gateway one does not. + let started: Value = + serde_json::from_str(out[0].lines().nth(1).unwrap().strip_prefix("data: ").unwrap()).unwrap(); + assert_eq!( + started["content_block"]["name"], "get_weather", + "client tool_use forwarded" + ); + // The loop must terminate despite a gateway call being present. + assert!( + !acc.should_continue_loop(), + "mixed round is terminal β€” loop must not continue" + ); + } + + // F6 (repro): begin_round() must reset final_message_delta. Round 1 ends on a + // tool_use terminal; round 2 ends WITHOUT a message_delta (clean EOF). finish() + // must NOT emit round 1's stale stop_reason: tool_use. + #[test] + fn repro_f6_begin_round_resets_stale_terminal() { + let mut acc = acc(); + // Round 1: a gateway tool round β†’ sets final_message_delta = tool_use terminal. + acc.begin_round(); + acc.push(&line(&json!({"type": "message_start", "message": {"id": "m"}}))); + acc.push(&line(&json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "name": "web_search", "id": "t"}}))); + acc.push(&line(&json!({"type": "content_block_stop", "index": 0}))); + acc.push(&line( + &json!({"type": "message_delta", "delta": {"stop_reason": "tool_use"}}), + )); + // Round 2: text, but upstream ends with NO message_delta (cut short). + acc.begin_round(); + acc.push(&line( + &json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text"}}), + )); + acc.push(&line( + &json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "hi"}}), + )); + acc.push(&line(&json!({"type": "content_block_stop", "index": 0}))); + let out = acc.finish().join(""); + assert!( + !out.contains(r#""stop_reason":"tool_use""#), + "must not emit round 1's stale tool_use terminal: {out}" + ); + } + + // F3 (repro): the assistant turn fed into the next round's history must + // preserve the model's thinking/text/signature blocks in order, not just the + // gateway tool_use. (This is the streaming half of Maral's F3 β€” "also repeated + // in messages_stream.rs".) + #[test] + fn repro_f3_stream_history_preserves_thinking_text_and_signature() { + let mut acc = acc(); + acc.begin_round(); + acc.push(&line(&json!({"type": "message_start", "message": {"id": "m"}}))); + // thinking idx0 (with a signature delta) + acc.push(&line( + &json!({"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}}), + )); + acc.push(&line(&json!({"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "let me search"}}))); + acc.push(&line(&json!({"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "SIG=="}}))); + acc.push(&line(&json!({"type": "content_block_stop", "index": 0}))); + // text idx1 + acc.push(&line( + &json!({"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}), + )); + acc.push(&line(&json!({"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "Searching..."}}))); + acc.push(&line(&json!({"type": "content_block_stop", "index": 1}))); + // gateway tool_use idx2 (suppressed from client, but must appear in history) + acc.push(&line(&json!({"type": "content_block_start", "index": 2, "content_block": {"type": "tool_use", "id": "tid", "name": "web_search", "input": {}}}))); + acc.push(&line(&json!({"type": "content_block_delta", "index": 2, "delta": {"type": "input_json_delta", "partial_json": "{\"query\":\"rust\"}"}}))); + acc.push(&line(&json!({"type": "content_block_stop", "index": 2}))); + acc.push(&line( + &json!({"type": "message_delta", "delta": {"stop_reason": "tool_use"}}), + )); + + let (assistant, _calls) = acc.take_round(); + let types: Vec<&str> = assistant.iter().filter_map(|b| b["type"].as_str()).collect(); + assert_eq!( + types, + vec!["thinking", "text", "tool_use"], + "full assistant turn preserved in order, not just the gateway tool_use: {assistant:?}" + ); + assert_eq!(assistant[0]["thinking"], "let me search", "thinking text reconstructed"); + assert_eq!( + assistant[0]["signature"], "SIG==", + "signature preserved for the next round" + ); + assert_eq!(assistant[1]["text"], "Searching...", "text reconstructed"); + assert_eq!( + assistant[2]["input"]["query"], "rust", + "gateway call input reconstructed" + ); + } + + // F4 (repro): a malformed/incomplete input_json for a gateway call must NOT + // silently become `{}` and dispatch the tool with args the model never sent. + #[tokio::test] + async fn repro_f4_malformed_partial_json_is_not_dispatched_with_empty_args() { + let mut acc = acc(); + acc.begin_round(); + acc.push(&line(&json!({"type": "message_start", "message": {"id": "m"}}))); + acc.push(&line(&json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "name": "web_search", "id": "t"}}))); + // Incomplete partial_json (stream cut mid-arguments). + acc.push(&line(&json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"query\":"}}))); + let (_assistant, calls) = acc.take_round(); + assert_eq!(calls.len(), 1); + // The reconstructed input is invalid JSON. + assert!( + serde_json::from_str::(&calls[0].input_json).is_err(), + "incomplete partial_json is invalid JSON" + ); + // After the fix, execute_gateway_calls must NOT coerce invalid input to + // {} and dispatch β€” it must produce an error tool_result. Assert the + // reconstructed call is flagged invalid rather than silently dispatchable. + let resolved = + execute_gateway_calls(&calls, &no_op_registry().await, &tool_seam::GatewayToolMap::default()).await; + let content = resolved[0].tool_result_block["content"].as_str().unwrap_or_default(); + assert!( + content.contains("invalid") || content.contains("malformed") || content.contains("could not"), + "malformed args must yield an error tool_result, not an empty-arg dispatch: {content:?}" + ); + } + + /// Registry with no gateway executors β€” dispatch of any call returns None, so + /// the ONLY way `execute_gateway_calls` can produce a non-"no handler" result + /// for a malformed input is by rejecting the args before dispatch (the fix). + async fn no_op_registry() -> ToolRegistry { + ToolRegistry::build_with_handlers( + &[], + &crate::tool::GatewayExecutors::from_env(std::sync::Arc::new(reqwest::Client::new())), + ) + .await + .unwrap() + } +} diff --git a/crates/agentic-server-core/src/executor/mod.rs b/crates/agentic-server-core/src/executor/mod.rs index 0c2b27a..daf7713 100644 --- a/crates/agentic-server-core/src/executor/mod.rs +++ b/crates/agentic-server-core/src/executor/mod.rs @@ -4,6 +4,8 @@ pub mod accumulator; pub mod engine; pub mod error; pub mod inference; +pub mod messages_loop; +pub mod messages_stream; pub mod modes; pub mod persist; pub mod rehydrate; @@ -15,6 +17,8 @@ mod upstream; pub use engine::{BoxStream, ExecuteRequest, create_conversation, execute}; pub use error::{ExecutorError, ExecutorResult}; pub use inference::call_inference; +pub use messages_loop::run_messages_loop; +pub use messages_stream::run_messages_stream; pub use modes::{ConversationHandler, ResponseHandler}; pub use persist::persist_response; pub use rehydrate::rehydrate_conversation; diff --git a/crates/agentic-server-core/src/executor/request.rs b/crates/agentic-server-core/src/executor/request.rs index bdc9618..554c021 100644 --- a/crates/agentic-server-core/src/executor/request.rs +++ b/crates/agentic-server-core/src/executor/request.rs @@ -7,8 +7,14 @@ use crate::executor::modes::{ConversationHandler, ResponseHandler}; use crate::storage::{ConversationStore, ResponseStore, create_pool_with_schema_and_sqlite_config}; use crate::tool::{GatewayExecutor, GatewayExecutors}; use crate::types::io::InputItem; +use crate::types::messages::GatewayToolMap; use crate::types::request_response::{RequestPayload, ResponsePayload}; +/// Env var configuring client-tool β†’ gateway-executor aliases for `/v1/messages` +/// (e.g. `WebSearch=web_search`). Empty/unset means no aliases β€” client +/// functions stay client-owned (the ownership doctrine's default). +const GATEWAY_TOOL_ALIASES_ENV: &str = "MESSAGES_GATEWAY_TOOL_ALIASES"; + /// Context built by `rehydrate_conversation`, threaded through the execute pipeline. #[derive(Debug)] pub struct RequestContext { @@ -48,6 +54,9 @@ pub struct ExecutionContext { pub resp_handler: ResponseHandler, pub client: Arc, pub gateway_executors: GatewayExecutors, + /// Client-tool β†’ gateway-executor aliases for the `/v1/messages` loop + /// (e.g. Claude Code's `WebSearch` β†’ `web_search`). Empty unless configured. + pub messages_gateway_tools: GatewayToolMap, /// Base URL for the LLM backend, e.g. `"http://localhost:8000"`. pub llm_base_url: String, /// Maximum wait time for the next SSE chunk. `Duration::ZERO` disables the timeout. @@ -81,6 +90,7 @@ impl ExecutionContext { resp_handler, client, gateway_executors, + messages_gateway_tools: messages_gateway_tools_from_env(), llm_base_url, streaming_timeout: Duration::from_secs(30), } @@ -117,8 +127,17 @@ impl ExecutionContext { resp_handler, client, gateway_executors, + messages_gateway_tools: messages_gateway_tools_from_env(), llm_base_url: cfg.llm_api_base.clone(), streaming_timeout: Duration::from_secs(30), }) } } + +/// Load the `/v1/messages` gateway-tool alias map from the environment. +fn messages_gateway_tools_from_env() -> GatewayToolMap { + std::env::var(GATEWAY_TOOL_ALIASES_ENV) + .ok() + .map(|raw| GatewayToolMap::from_env_str(&raw)) + .unwrap_or_default() +} diff --git a/crates/agentic-server-core/src/types/messages/mod.rs b/crates/agentic-server-core/src/types/messages/mod.rs new file mode 100644 index 0000000..7f67e96 --- /dev/null +++ b/crates/agentic-server-core/src/types/messages/mod.rs @@ -0,0 +1,16 @@ +//! Anthropic Messages API (`/v1/messages`) wire types. +//! +//! The gateway serves `/v1/messages` natively: requests that declare a +//! gateway-owned tool are driven through a Messages-native tool loop that talks +//! to vLLM `/v1/messages` upstream (see `executor::messages_loop`); everything +//! else is a transparent proxy. These are the Anthropic wire types shared by +//! the handler, the loop, and the tool seam. + +pub mod request; +pub mod tool_seam; + +pub use request::{ContentBlock, MessageContent, MessageParam, MessagesRequest, SystemPrompt, ToolParam}; +pub use tool_seam::{ + GatewayToolMap, adapt_web_search_input, call_to_tool_use_block, has_gateway_tool, parse_tool_input, registry_tools, + strip_gateway_tool_use, tool_result_block, tool_use_to_call, +}; diff --git a/crates/agentic-server-core/src/types/messages/request.rs b/crates/agentic-server-core/src/types/messages/request.rs new file mode 100644 index 0000000..2ca6927 --- /dev/null +++ b/crates/agentic-server-core/src/types/messages/request.rs @@ -0,0 +1,157 @@ +//! Anthropic Messages API request types (`POST /v1/messages`). +//! +//! These mirror the Anthropic wire shape. The gateway forwards the client's raw +//! request to vLLM `/v1/messages` untouched (the loop is Messages-native), so +//! fields the gateway does not itself act on are preserved via `extra` and pass +//! through unchanged. The loop only needs to *read* the tools and the assistant +//! turn; see [`super::tool_seam`] and [`crate::executor::messages_loop`]. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// Top-level Anthropic Messages request body. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MessagesRequest { + pub model: String, + pub max_tokens: u32, + pub messages: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub system: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tools: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_choice: Option, + #[serde(default)] + pub stream: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub temperature: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub top_p: Option, + /// Any other top-level field (e.g. `metadata`, `stop_sequences`) preserved verbatim. + #[serde(flatten)] + pub extra: HashMap, +} + +/// Anthropic `system` accepts either a bare string or an array of text blocks. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SystemPrompt { + Text(String), + Blocks(Vec), +} + +impl SystemPrompt { + /// Flatten into a single instructions string (block texts joined by newlines). + #[must_use] + pub fn to_instructions(&self) -> String { + match self { + Self::Text(text) => text.clone(), + Self::Blocks(blocks) => blocks.iter().map(|b| b.text.as_str()).collect::>().join("\n"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SystemBlock { + #[serde(default)] + pub text: String, +} + +/// One entry in the `messages` array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MessageParam { + pub role: String, + pub content: MessageContent, +} + +/// Anthropic message content is either a bare string or an array of blocks. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MessageContent { + Text(String), + Blocks(Vec), +} + +/// A content block inside a message. Internally tagged by `type`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ContentBlock { + Text { + #[serde(default)] + text: String, + }, + Thinking { + #[serde(default)] + thinking: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + signature: Option, + }, + ToolUse { + id: String, + name: String, + #[serde(default)] + input: Value, + }, + ToolResult { + tool_use_id: String, + #[serde(default)] + content: ToolResultContent, + }, + /// Any block type the gateway does not model β€” dropped on the way in. + #[serde(other)] + Unknown, +} + +/// `tool_result.content` may be a plain string or an array of blocks (Anthropic +/// allows both). Normalised to a single string by [`ToolResultContent::to_text`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ToolResultContent { + Text(String), + Blocks(Vec), +} + +impl Default for ToolResultContent { + fn default() -> Self { + Self::Text(String::new()) + } +} + +impl ToolResultContent { + /// Flatten to a single output string for the internal `function_call_output`. + #[must_use] + pub fn to_text(&self) -> String { + match self { + Self::Text(text) => text.clone(), + Self::Blocks(blocks) => blocks + .iter() + .filter_map(|b| b.text.as_deref()) + .collect::>() + .join(""), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolResultBlock { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub text: Option, +} + +/// A tool declared in the request. Anthropic's shape is `{name, description, +/// input_schema}`; server tools may additionally carry a versioned `type`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolParam { + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input_schema: Option, + /// Anthropic server tools carry a versioned `type` (e.g. `web_search_20250305`). + #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] + pub type_: Option, + #[serde(flatten)] + pub extra: HashMap, +} diff --git a/crates/agentic-server-core/src/types/messages/tool_seam.rs b/crates/agentic-server-core/src/types/messages/tool_seam.rs new file mode 100644 index 0000000..2787720 --- /dev/null +++ b/crates/agentic-server-core/src/types/messages/tool_seam.rs @@ -0,0 +1,457 @@ +//! The protocol-neutral seam between Anthropic Messages tool blocks and the +//! internal tool layer (`tool::registry`). +//! +//! The Messages-native loop never touches `RequestPayload`/`ResponsePayload`; it +//! only needs to (1) classify declared tools as gateway-owned vs client-owned so +//! it knows what to execute, (2) turn an assistant `tool_use` block into the +//! `FunctionToolCall` that `ToolRegistry::dispatch` consumes, and (3) turn the +//! resulting `ToolOutput` back into a `tool_result` block to feed the next round. +//! +//! These are pure conversions with no I/O β€” the loop (`executor::messages_loop`) +//! and the registry supply the behaviour around them. + +use std::collections::HashMap; + +use serde_json::{Map, Value, json}; + +use crate::types::event::MessageStatus; +use crate::types::io::output::FunctionToolCall; +use crate::types::tools::{FunctionToolParam, ResponsesTool, WebSearchToolParam}; + +use super::request::ToolParam; + +/// The one built-in gateway executor exposed on `/v1/messages` today. The +/// registry keys it under this exact name (`tool::web_search`). +pub const WEB_SEARCH_EXECUTOR: &str = "web_search"; + +/// Operator-configured map of client-declared tool names to gateway executors. +/// +/// Empty by default: a client `function` stays client-owned unless the operator +/// configures it here β€” this is the "unless configured as gateway-owned" clause +/// of the ownership doctrine (see `docs/design/codex-integration.md`), applied +/// structurally rather than by a hardcoded name heuristic. It lets a client +/// like Claude Code β€” which declares its web search as a client function named +/// `WebSearch` β€” have that call executed server-side by the gateway's +/// `web_search` executor, mirroring how Codex's typed `web_search` tool already +/// routes through the Responses loop. +/// +/// The canonical executor `web_search` is always recognised; the map only adds +/// operator-approved *aliases* on top. +#[derive(Clone, Debug, Default)] +pub struct GatewayToolMap { + /// client tool name (as the model calls it) β†’ canonical executor key. + aliases: HashMap, +} + +impl GatewayToolMap { + /// Build from `name=executor` pairs (e.g. `WebSearch=web_search`). Pairs + /// naming an unknown executor are skipped. Whitespace is trimmed. + #[must_use] + pub fn from_pairs<'a>(pairs: impl IntoIterator) -> Self { + let aliases = pairs + .into_iter() + .map(|(name, exec)| (name.trim(), exec.trim())) + .filter(|(name, exec)| !name.is_empty() && *exec == WEB_SEARCH_EXECUTOR) + .map(|(name, exec)| (name.to_owned(), exec.to_owned())) + .collect(); + Self { aliases } + } + + /// Parse the `MESSAGES_GATEWAY_TOOL_ALIASES` env format: + /// `"WebSearch=web_search,OtherName=web_search"`. + #[must_use] + pub fn from_env_str(raw: &str) -> Self { + let pairs: Vec<(&str, &str)> = raw.split(',').filter_map(|kv| kv.split_once('=')).collect(); + Self::from_pairs(pairs) + } + + /// The canonical executor key for a declared tool name, if it is + /// gateway-owned: the built-in `web_search`, or a configured alias. + #[must_use] + pub fn canonical_executor(&self, name: &str) -> Option<&str> { + if name == WEB_SEARCH_EXECUTOR { + Some(WEB_SEARCH_EXECUTOR) + } else { + self.aliases.get(name).map(String::as_str) + } + } + + #[must_use] + pub fn is_gateway_owned(&self, name: &str) -> bool { + self.canonical_executor(name).is_some() + } +} + +/// True if the request declares at least one gateway-owned tool β€” the routing +/// gate that decides loop vs. transparent proxy. Gateway ownership is resolved +/// against the operator-configured [`GatewayToolMap`]. +#[must_use] +pub fn has_gateway_tool(tools: Option<&Vec>, map: &GatewayToolMap) -> bool { + tools.is_some_and(|tools| tools.iter().any(|t| map.is_gateway_owned(&t.name))) +} + +/// Map declared Anthropic tools to the internal `ResponsesTool` list used to +/// build a request-scoped `ToolRegistry`. Gateway-owned tools (built-in or +/// configured alias) become the matching gateway variant β€” the registry keys +/// the `web_search` executor under its canonical name, and dispatch +/// canonicalises the call name to match ([`tool_use_to_call`]). Everything else +/// becomes a client-owned `Function`. +#[must_use] +pub fn registry_tools(tools: Option<&Vec>, map: &GatewayToolMap) -> Vec { + let Some(tools) = tools else { + return Vec::new(); + }; + tools.iter().filter_map(|t| map_tool(t, map)).collect() +} + +fn map_tool(tool: &ToolParam, map: &GatewayToolMap) -> Option { + if map.canonical_executor(&tool.name) == Some(WEB_SEARCH_EXECUTOR) { + // Defaults are fine: the client's input_schema is the model-facing + // contract (forwarded to vLLM in the raw request), not the executor's + // config. + return Some(ResponsesTool::WebSearch(WebSearchToolParam::default())); + } + let name = tool.name.clone().try_into().ok()?; + Some(ResponsesTool::Function(FunctionToolParam { + name, + description: tool.description.clone(), + parameters: tool.input_schema.clone(), + strict: None, + defer_loading: None, + extra: std::collections::HashMap::new(), + })) +} + +/// Turn an assistant `tool_use` block into the `FunctionToolCall` that +/// `ToolRegistry::dispatch` consumes. +/// +/// M1: Anthropic `input` is a JSON object; internal `arguments` is a stringified +/// JSON. M3: the Anthropic `tool_use.id` seeds both `id` and `call_id` so the +/// dispatch result and the fed-back `tool_result` pair by the same id. The name +/// is canonicalised to the executor key (so a configured alias like Claude +/// Code's `WebSearch` dispatches to the `web_search` executor), and its +/// arguments are adapted to the executor's schema ([`adapt_web_search_input`]). +#[must_use] +pub fn tool_use_to_call(id: &str, name: &str, input: &Value, map: &GatewayToolMap) -> FunctionToolCall { + let canonical = map.canonical_executor(name).unwrap_or(name); + let adapted = if canonical == WEB_SEARCH_EXECUTOR && name != WEB_SEARCH_EXECUTOR { + adapt_web_search_input(input) + } else { + input.clone() + }; + FunctionToolCall { + id: id.to_owned(), + call_id: id.to_owned(), + name: canonical.to_owned(), + namespace: None, + arguments: serde_json::to_string(&adapted).unwrap_or_else(|_| "{}".to_owned()), + status: MessageStatus::Completed, + } +} + +/// Adapt a client web-search tool's arguments to the gateway `web_search` +/// executor's schema. Claude Code's `WebSearch` uses `allowed_domains` / +/// `blocked_domains`; the executor uses `include_domains` / `exclude_domains`. +/// `query` and any executor-native fields pass through; fields the executor does +/// not model are dropped (it ignores unknown keys). +/// +/// Limitation: the executor rejects `include_domains` combined with +/// `exclude_domains` (see `tool::web_search`). Claude Code's schema allows both +/// `allowed_domains` and `blocked_domains` at once; a call that sets both yields +/// an error `tool_result` (fed back to the model β€” graceful, not a failure). +/// Not worked around here so the constraint stays in one place (the executor). +#[must_use] +pub fn adapt_web_search_input(input: &Value) -> Value { + let Some(obj) = input.as_object() else { + return input.clone(); + }; + let mut out = obj.clone(); + if let Some(v) = out.remove("allowed_domains") { + out.entry("include_domains").or_insert(v); + } + if let Some(v) = out.remove("blocked_domains") { + out.entry("exclude_domains").or_insert(v); + } + Value::Object(out) +} + +/// Build the Anthropic `tool_result` content block fed back to the model on the +/// next round, from a dispatched tool's output. `is_error` marks a failed/invalid +/// call so the model knows the tool did not run normally. +#[must_use] +pub fn tool_result_block(tool_use_id: &str, output: &str, is_error: bool) -> Value { + json!({ + "type": "tool_result", + "tool_use_id": tool_use_id, + "content": output, + "is_error": is_error, + }) +} + +/// Parse a reconstructed `tool_use` input (a JSON string) into the object the +/// tool expects. Returns `Err` for malformed/incomplete JSON or a non-object β€” +/// the caller must NOT dispatch with fabricated `{}` args the model never sent +/// (F4); it should feed back an error `tool_result` instead. +/// +/// # Errors +/// Returns a human-readable reason when the input is not a valid JSON object. +pub fn parse_tool_input(input_json: &str) -> Result { + let value: Value = + serde_json::from_str(input_json).map_err(|e| format!("could not parse tool arguments as JSON: {e}"))?; + if value.is_object() { + Ok(value) + } else { + Err("tool arguments must be a JSON object".to_owned()) + } +} + +/// Split an assistant turn's content blocks into (client-visible, gateway +/// `tool_use` present?). Gateway-owned `tool_use` blocks are removed so the +/// client never sees a call it cannot execute (hide-the-call); every other +/// block β€” text, thinking, signature, client-owned `tool_use` β€” is preserved in +/// order. Used for the client-facing response on a mixed round (F5). +#[must_use] +pub fn strip_gateway_tool_use(content: &[Value], map: &GatewayToolMap) -> Vec { + content + .iter() + .filter(|b| { + !(b.get("type").and_then(Value::as_str) == Some("tool_use") + && map.is_gateway_owned(b.get("name").and_then(Value::as_str).unwrap_or_default())) + }) + .cloned() + .collect() +} + +/// Build the assistant `tool_use` content block that mirrors a call the gateway +/// executed β€” appended to the assistant turn in the next-round history so the +/// model sees its own call paired with the `tool_result`. +/// +/// M1 reverse: internal `arguments` is a stringified JSON; parse it back to an +/// object, falling back to `{}` rather than failing. +#[must_use] +pub fn call_to_tool_use_block(call: &FunctionToolCall) -> Value { + let input: Value = serde_json::from_str(&call.arguments).unwrap_or_else(|_| Value::Object(Map::new())); + let id = if call.call_id.is_empty() { + &call.id + } else { + &call.call_id + }; + json!({ + "type": "tool_use", + "id": id, + "name": call.name, + "input": input, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::messages::request::MessagesRequest; + + fn tools_of(json_req: Value) -> Option> { + serde_json::from_value::(json_req).unwrap().tools + } + + /// Default map: only the built-in `web_search` executor, no aliases. + fn default_map() -> GatewayToolMap { + GatewayToolMap::default() + } + + /// Map with Claude Code's `WebSearch` aliased to the `web_search` executor. + fn alias_map() -> GatewayToolMap { + GatewayToolMap::from_pairs([("WebSearch", "web_search")]) + } + + #[test] + fn web_search_is_gateway_owned_and_maps_to_gateway_variant() { + let map = default_map(); + assert!(map.is_gateway_owned("web_search")); + assert!(!map.is_gateway_owned("get_weather")); + + let tools = tools_of(json!({ + "model": "m", "max_tokens": 10, "messages": [], + "tools": [{"name": "web_search", "input_schema": {"type": "object"}}] + })); + assert!(has_gateway_tool(tools.as_ref(), &map)); + let mapped = registry_tools(tools.as_ref(), &map); + assert!(matches!(mapped.as_slice(), [ResponsesTool::WebSearch(_)])); + } + + #[test] + fn custom_tool_stays_client_owned_function() { + let map = default_map(); + let tools = tools_of(json!({ + "model": "m", "max_tokens": 10, "messages": [], + "tools": [{"name": "get_weather", "description": "local", "input_schema": {"type": "object"}}] + })); + assert!(!has_gateway_tool(tools.as_ref(), &map)); + let mapped = registry_tools(tools.as_ref(), &map); + assert!(matches!(mapped.as_slice(), [ResponsesTool::Function(_)])); + } + + #[test] + fn mixed_tools_classify_independently() { + let map = default_map(); + let tools = tools_of(json!({ + "model": "m", "max_tokens": 10, "messages": [], + "tools": [ + {"name": "web_search", "input_schema": {"type": "object"}}, + {"name": "get_weather", "input_schema": {"type": "object"}} + ] + })); + assert!(has_gateway_tool(tools.as_ref(), &map)); + let mapped = registry_tools(tools.as_ref(), &map); + assert!(matches!(mapped[0], ResponsesTool::WebSearch(_))); + assert!(matches!(mapped[1], ResponsesTool::Function(_))); + } + + #[test] + fn no_tools_is_not_gateway_and_maps_empty() { + let map = default_map(); + assert!(!has_gateway_tool(None, &map)); + assert!(registry_tools(None, &map).is_empty()); + } + + // Claude Code declares its web search as a client function `WebSearch`. With + // no alias configured it stays client-owned; with the alias it becomes + // gateway-owned and maps to the WebSearch gateway variant. + #[test] + fn claude_code_websearch_is_client_owned_by_default_gateway_when_aliased() { + let tools = tools_of(json!({ + "model": "m", "max_tokens": 10, "messages": [], + "tools": [{"name": "WebSearch", "description": "Search the web.", + "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}}}] + })); + // Default: not gateway-owned (doctrine default β€” unless configured). + assert!(!has_gateway_tool(tools.as_ref(), &default_map())); + assert!(matches!( + registry_tools(tools.as_ref(), &default_map()).as_slice(), + [ResponsesTool::Function(_)] + )); + // Aliased: gateway-owned, maps to the WebSearch executor variant. + let map = alias_map(); + assert!(map.is_gateway_owned("WebSearch")); + assert_eq!(map.canonical_executor("WebSearch"), Some("web_search")); + assert!(has_gateway_tool(tools.as_ref(), &map)); + assert!(matches!( + registry_tools(tools.as_ref(), &map).as_slice(), + [ResponsesTool::WebSearch(_)] + )); + } + + #[test] + fn gateway_tool_map_from_env_str_and_rejects_unknown_executor() { + let map = GatewayToolMap::from_env_str("WebSearch=web_search, Foo=web_search"); + assert!(map.is_gateway_owned("WebSearch")); + assert!(map.is_gateway_owned("Foo")); + // An alias pointing at an unknown executor is dropped. + let bad = GatewayToolMap::from_env_str("X=not_a_real_executor"); + assert!(!bad.is_gateway_owned("X")); + } + + // A configured alias canonicalises the dispatch name AND adapts args + // (allowed_domains -> include_domains) so Claude Code's WebSearch schema + // reaches the web_search executor correctly. + #[test] + fn aliased_tool_use_canonicalises_name_and_adapts_args() { + let call = tool_use_to_call( + "toolu_cc", + "WebSearch", + &json!({"query": "rust", "allowed_domains": ["rust-lang.org"], "blocked_domains": ["spam.com"]}), + &alias_map(), + ); + assert_eq!(call.name, "web_search", "alias canonicalised to executor key"); + let args: Value = serde_json::from_str(&call.arguments).unwrap(); + assert_eq!(args["query"], "rust"); + assert_eq!( + args["include_domains"], + json!(["rust-lang.org"]), + "allowed_domains adapted" + ); + assert_eq!(args["exclude_domains"], json!(["spam.com"]), "blocked_domains adapted"); + assert!(args.get("allowed_domains").is_none(), "CC field renamed away"); + } + + #[test] + fn adapt_web_search_input_is_noop_for_native_fields() { + let out = adapt_web_search_input(&json!({"query": "x", "include_domains": ["a.com"]})); + assert_eq!(out["query"], "x"); + assert_eq!(out["include_domains"], json!(["a.com"])); + } + + // M1 + M3: tool_use object args -> stringified; id seeds id + call_id. + #[test] + fn tool_use_maps_to_function_call() { + let call = tool_use_to_call("toolu_1", "web_search", &json!({"query": "rust"}), &default_map()); + assert_eq!(call.id, "toolu_1"); + assert_eq!(call.call_id, "toolu_1"); + assert_eq!(call.name, "web_search"); + let args: Value = serde_json::from_str(&call.arguments).unwrap(); + assert_eq!(args["query"], "rust"); + } + + #[test] + fn tool_result_block_pairs_by_id() { + let block = tool_result_block("toolu_1", "the answer", false); + assert_eq!(block["type"], "tool_result"); + assert_eq!(block["tool_use_id"], "toolu_1"); + assert_eq!(block["content"], "the answer"); + assert_eq!(block["is_error"], false); + // Error results carry is_error: true (F4). + assert_eq!(tool_result_block("t", "bad args", true)["is_error"], true); + } + + #[test] + fn parse_tool_input_rejects_malformed_and_non_object() { + assert!(parse_tool_input(r#"{"query":"x"}"#).is_ok()); + assert!(parse_tool_input(r#"{"query":"#).is_err(), "incomplete JSON rejected"); + assert!(parse_tool_input(r#""just a string""#).is_err(), "non-object rejected"); + } + + #[test] + fn strip_gateway_tool_use_removes_only_gateway_calls() { + let content = vec![ + json!({"type": "text", "text": "hi"}), + json!({"type": "tool_use", "name": "web_search", "id": "g"}), + json!({"type": "tool_use", "name": "get_weather", "id": "c"}), + ]; + let out = strip_gateway_tool_use(&content, &default_map()); + let names: Vec<&str> = out + .iter() + .filter(|b| b["type"] == "tool_use") + .filter_map(|b| b["name"].as_str()) + .collect(); + assert_eq!( + names, + vec!["get_weather"], + "gateway web_search stripped, client + text kept" + ); + assert_eq!(out.len(), 2); + } + + // M1 reverse: stringified args -> object; call_id preferred as the block id. + #[test] + fn call_to_tool_use_block_round_trips() { + let call = tool_use_to_call( + "toolu_9", + "web_search", + &json!({"query": "x", "count": 2}), + &default_map(), + ); + let block = call_to_tool_use_block(&call); + assert_eq!(block["type"], "tool_use"); + assert_eq!(block["id"], "toolu_9"); + assert_eq!(block["name"], "web_search"); + assert_eq!(block["input"]["query"], "x"); + assert_eq!(block["input"]["count"], 2); + } + + #[test] + fn call_to_tool_use_block_falls_back_on_bad_args() { + let mut call = tool_use_to_call("t", "x", &json!({}), &default_map()); + call.arguments = "not json".to_owned(); + let block = call_to_tool_use_block(&call); + assert_eq!(block["input"], json!({})); + } +} diff --git a/crates/agentic-server-core/src/types/mod.rs b/crates/agentic-server-core/src/types/mod.rs index bcbc839..8421877 100644 --- a/crates/agentic-server-core/src/types/mod.rs +++ b/crates/agentic-server-core/src/types/mod.rs @@ -1,5 +1,6 @@ pub mod event; pub mod io; +pub mod messages; pub mod request_response; pub mod tools; diff --git a/crates/agentic-server-core/tests/cassettes/messages_multiround/multiround-web-search-qwen3-streaming.yaml b/crates/agentic-server-core/tests/cassettes/messages_multiround/multiround-web-search-qwen3-streaming.yaml new file mode 100644 index 0000000..b96f7e9 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/messages_multiround/multiround-web-search-qwen3-streaming.yaml @@ -0,0 +1,8111 @@ +turns: +- filename: t1 + request: + body: + max_tokens: 700 + messages: + - content: First call web_search for the latest Rust version, then a SEPARATE + web_search for its release date, one at a time, then answer briefly. + role: user + model: qwen3 + stream: true + tools: + - description: Search the web. + input_schema: + properties: + query: + type: string + required: + - query + type: object + name: web_search + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + method: POST + path: /v1/messages + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: message_start + + ' + - 'data: {"type":"message_start","message":{"id":"chatcmpl-8b7b79a8ec82657a","type":"message","role":"assistant","content":[],"model":"qwen3","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":174,"output_tokens":0}}} + + ' + - ' + + ' + - 'event: content_block_start + + ' + - 'data: {"type":"content_block_start","content_block":{"type":"thinking","thinking":""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"\n"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"Okay"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + user"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + wants"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + me"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + to"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + first"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + for"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + using"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + web"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"_search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + then"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + do"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + another"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + separate"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + web"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"_search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + for"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + its"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Let"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + me"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + start"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + with"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + first"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + query"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":".\n\n"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"For"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + first"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + query"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + should"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + be"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + \""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"\"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + That"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + should"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + get"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + me"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + information"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + on"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + what"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + current"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + stable"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Once"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + I"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + have"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + that"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + next"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + step"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + to"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + for"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + of"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + that"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + specific"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + But"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + I"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + need"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + to"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + make"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + sure"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + I"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + do"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + these"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + separate"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + searches"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + per"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + user"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''s"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + instruction"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + So"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + I"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''ll"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + call"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + web"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"_search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + function"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + with"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + first"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + query"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + then"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + wait"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + for"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + result"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + before"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + proceeding"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + to"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + second"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Let"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + me"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + structure"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + tool"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + calls"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + correctly"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + making"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + sure"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + each"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + in"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + its"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + own"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + XML"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + tags"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + and"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + in"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + sequence"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":".\n"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"signature_delta","signature":"b012540666064fc299929d8872408088"},"index":0} + + ' + - ' + + ' + - 'event: content_block_stop + + ' + - 'data: {"type":"content_block_stop","index":0} + + ' + - ' + + ' + - 'event: content_block_start + + ' + - 'data: {"type":"content_block_start","content_block":{"type":"text","text":""},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"\n\n"},"index":1} + + ' + - ' + + ' + - 'event: content_block_stop + + ' + - 'data: {"type":"content_block_stop","index":1} + + ' + - ' + + ' + - 'event: content_block_start + + ' + - 'data: {"type":"content_block_start","content_block":{"type":"tool_use","id":"chatcmpl-tool-9eb40806dc51de97","name":"web_search","input":{}},"index":2} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"{\""},"index":2} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"query"},"index":2} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"\":"},"index":2} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":" + \""},"index":2} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"latest"},"index":2} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":" + Rust"},"index":2} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":" + version"},"index":2} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"\"}"},"index":2} + + ' + - ' + + ' + - 'event: content_block_stop + + ' + - 'data: {"type":"content_block_stop","index":2} + + ' + - ' + + ' + - 'event: content_block_start + + ' + - 'data: {"type":"content_block_start","content_block":{"type":"tool_use","id":"chatcmpl-tool-953805f2e70f765d","name":"web_search","input":{}},"index":3} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"{\""},"index":3} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"query"},"index":3} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"\":"},"index":3} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":" + \""},"index":3} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"R"},"index":3} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"ust"},"index":3} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":" + release"},"index":3} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":" + date"},"index":3} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"\"}"},"index":3} + + ' + - ' + + ' + - 'event: content_block_stop + + ' + - 'data: {"type":"content_block_stop","index":3} + + ' + - ' + + ' + - 'event: message_delta + + ' + - 'data: {"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"input_tokens":174,"output_tokens":199}} + + ' + - ' + + ' + - 'event: message_stop + + ' + - 'data: {"type":"message_stop"} + + ' + - ' + + ' + status_code: 200 +- filename: t2 + request: + body: + max_tokens: 700 + messages: + - content: First call web_search for the latest Rust version, then a SEPARATE + web_search for its release date, one at a time, then answer briefly. + role: user + - content: + - id: chatcmpl-tool-9eb40806dc51de97 + input: + query: latest Rust version + name: web_search + type: tool_use + - id: chatcmpl-tool-953805f2e70f765d + input: + query: Rust release date + name: web_search + type: tool_use + role: assistant + - content: + - content: '{"metadata":{"latency":0.5584540367126465,"query":"latest Rust + version","search_uuid":"0efb64b2-4f70-4d76-8871-bd51fa070c53"},"query":"latest + Rust version","results":{"news":[{"description":"Mighty Morphin Power + Rangers #2 Reveals Its Heart, But Is Held Back From Its Potential (Review)","page_age":"2026-07-16T01:01:33","thumbnail_url":"https://comicbook.com/wp-content/uploads/sites/4/2026/07/Mighty-Morphin-Power-Rangers-2.jpg?resize=2000,1125","title":"Mighty + Morphin Power Rangers #2 Reveals Its Heart, But Is Held Back From Its + Potential (Review)","url":"https://comicbook.com/comics/review/mighty-morphin-power-rangers-2-reveals-its-heart-but-is-held-back-from-its-potential-review/"},{"description":"You''re + not the only one with a key.","page_age":"2026-07-02T19:16:23","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/f10abddbbcbcd6170a27a53c5c0de04d74d64574a1892e3ee431b5e6cea30578/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fpc_gamer_708%2F2e8f245e655fc750d4cd765499c51a76","title":"Rust + gets even more cutthroat by adding its own version of apartment rentals","url":"https://tech.yahoo.com/gaming/articles/rust-gets-even-more-cutthroat-191623715.html"},{"description":"Malicious + jscrambler 8.14.0 runs hidden binaries during npm install on Windows, + macOS, and Linux, with no fix available as of July 11, 2026.","page_age":"2026-07-13T06:46:39","thumbnail_url":"https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhodiWPXDsex6YL7v4e2n_D5MJFMht76GRRmHTdiG61tfEmsoZyow4tkiS3ORACILDl2fNP27YQkw1XLSBKGcom-DhCVzzPDdkdSgeMGdxcVL_rgO70x5LqZcjxKAPTtFOquB8LwSBPWrSpflqCzns-cFZ_EVm0WZfV7wSoPM3S-7_IGtCT3AjxY0rfITU/s1700-e365/npm-js.jpg","title":"Compromised + jscrambler 8.14.0 npm Release Drops Rust Infostealer During Install","url":"https://thehackernews.com/2026/07/compromised-jscrambler-8140-npm-release.html"},{"description":"Eagerly + anticipating the next Rust Mobile beta? Be sure to check in this August + as Level Infinite schedule their next closed beta.","page_age":"2026-07-09T15:55:00","thumbnail_url":"https://media.pocketgamer.com/artwork/na-36888-1783610707/rust-mobile-beta-two-keyart.jpg","title":"Rust + Mobile will offer even more equipment and game options in its upcoming + second beta","url":"https://www.pocketgamer.com/rust-mobile/second-closed-beta-coming-this-summer/"},{"description":"After + an opening-round 65 at the Scottish Open, Rory McIlroy explained the reason + he doesn''t feel rusty despite having played only four times since winning + the Masters","page_age":"2026-07-09T15:33:03","thumbnail_url":"https://s.yimg.com/os/en/golf_digest_articles_737/d8524d67d05a25ed4c13358404640eef","title":"Rory + McIlroy says he''s benefiting from a lighter schedule. Here''s why","url":"https://sports.yahoo.com/articles/rory-mcilroy-says-hes-benefiting-153303945.html"},{"description":"The + Atlanta Braves have two players recovering from injuries officially in + the rehab stage of their recoveries","page_age":"2026-07-13T19:27:08","thumbnail_url":"https://images2.minutemediacdn.com/image/upload/c_crop,x_0,y_0,w_3974,h_2235/c_fill,w_1440,ar_1440:810,f_auto,q_auto,g_auto/shape/cover/sport/01kxee2snea8t7jbaqgy-a7f2d87fe57f1668d64f6ddf872e9c44.webp","title":"Ha-Seong + Kim Sharp in First Braves Rehab Game, AcuΓ±a Rusty","url":"https://www.si.com/mlb/braves/onsi/ha-seong-kim-sharp-first-braves-rehab-game-acuna-rusty"},{"description":"Plans + to extend Zakynthos'' iconic Navagio Beach by 30 metres aim to preserve + its rusting shipwreck for future generations, as visitors continue to + be barred from the popular island attraction because ...","page_age":"2026-07-13T12:56:57","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/0fc2ca54e2ad166ca5429f663256f69579db7930cf4e28828ddd9ba0f023ecfd/lightyear_networkapi/resizefill_w1024_h576;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Feuronews_us_news_articles_566%2F8b2deb640dc4a5a47e8a99ec6f49105b","title":"Greece''s + famous β€˜Shipwreck Beach’ to be extended to save its iconic wreck","url":"https://www.yahoo.com/news/world/articles/greeces-famous-shipwreck-beach-extended-125657791.html"},{"description":"Ohio + tops CNBC''s rankings for the first time in 2026, with the Buckeye State + culminating a yearslong climb as the place to do business in America.","page_age":"2026-07-09T18:10:56","thumbnail_url":"https://image.cnbcfm.com/api/v1/image/108330101-1783012419076-gettyimages-2267146157-SOFTBANK_DATA_CENTER.jpeg?v=1783434396&w=1920&h=1080","title":"Ohio + is America''s Top State for Business in 2026, capping yearslong rise with + first No. 1 finish","url":"https://www.cnbc.com/2026/07/09/ohio-no-1-americas-top-states-for-business-rankings.html"},{"description":"At + the Open Source Summit in Mumbai, Torvalds discusses the pain and power + of AI in the kernel, and why Linux no longer supports ''museum'' technology.","page_age":"2026-07-08T21:09:50","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/78cc427d8d85d82723b419f8f03c12e49e1d28279ae0e60443122d1cfbde8784/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fzdnet_articles_730%2F133ed8711fa74a3a05edae71b635e7b9","title":"''I''m + not a programmer'' anymore: Linus Torvalds on the only two tools he uses + now","url":"https://tech.yahoo.com/ai/articles/im-not-programmer-anymore-linus-210950038.html"},{"description":"Sen. + Sheldon Whitehouse says whistleblowers have alleged numerous problems + with rushed reconstruction of the Kennedy Center.","page_age":"2026-07-12T21:40:39","thumbnail_url":"https://ca-times.brightspotcdn.com/dims4/default/5f6c940/2147483647/strip/true/crop/5915x3105+0+419/resize/1200x630!/quality/75/?url=https%3A%2F%2Fcalifornia-times-brightspot.s3.amazonaws.com%2F45%2F71%2Fe499b7894ff8a069cf35076ab381%2F318521025cb8478fbc006f8cdb2bbd3a.jpg","title":"Democrat + announces whistleblower allegations of construction problems at Kennedy + Center","url":"https://www.latimes.com/world-nation/story/2026-07-12/democrat-announces-whistleblower-allegations-of-construction-problems-at-kennedy-center"}],"web":[{"description":"Click + here to be redirected to the latest Rust release announcement.","favicon_url":"https://you.com/favicon?domain=blog.rust-lang.org&size=128","page_age":"2026-07-09T05:20:01","snippets":[],"title":"Announcing + Rust 1.97.0","url":"https://blog.rust-lang.org/releases/latest/"},{"description":"Rust + Versions # Stable: 1.95.0 Beta: 1.96.0 (28 May, 2026, 8 days left) Nightly: + 1.97.0 (9 July, 2026, 50 days left) Ongoing Stabilization PRs # Stabilize + core::range::{legacy, RangeFull, RangeTo} (4 days old) T-compiler - Relevant + to the compiler team, which will review and decide on the PR/issue.","favicon_url":"https://you.com/favicon?domain=releases.rs&size=128","page_age":"2026-06-02T03:45:04","snippets":["T-rustdoc + - Relevant to the rustdoc team, which will review and decide on the PR/issue.","T-rustdoc-frontend + - Relevant to the rustdoc-frontend team, which will review and decide + on the web UI/UX output.","T-rust-analyzer - Relevant to the rust-analyzer + team, which will review and decide on the PR/issue.","T-rustdoc-internals + - Relevant to the rustdoc internals team, which will review and decide + on the PR/issue."],"title":"Rust Versions | Rust Changelogs","url":"https://releases.rs/"},{"description":"Empowering + everyone to build reliable and efficient software. - Releases Β· rust-lang/rust","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/965c2fcedff6dbee97c6e81ff9e86ca7d93ba06b36c08dd84d449d801ca3195b/rust-lang/rust","snippets":["Empowering + everyone to build reliable and efficient software. - rust-lang/rust","Allow + a dependency to specify both a git repository and an alternate registry. + Just like with crates.io, the git repository will be used locally, but + the registry version will be used when published. Added target.''cfg(..)''.rustdocflags + support in configuration.","Refer to Rust''s platform support page for + more information on Rust''s tiered platform support. Relax T: Ord bound + for some BinaryHeap methods. ... Stabilize the config include key. The + top-level include config key allows loading additional config files, enabling + better organization, sharing, and management of Cargo configurations across + projects and environments. docs #16284 Β· Stabilize the pubtime field in + registry index. This records when a crate version was published and enables + time-based dependency resolution in the future.","Putting the #[test] + attribute in these places is no longer ignored, and will now result in + an error; this may also result in errors when generating rustdoc. Error + when test attribute is applied to structs Β· Cargo now sets the CARGO_CFG_DEBUG_ASSERTIONS + environment variable in more situations. This will cause crates depending + on static-init versions 1.0.1 to 1.0.3 to fail compilation with \"failed + to resolve: use of unresolved module or unlinked crate parking_lot\"."],"thumbnail_url":"https://opengraph.githubassets.com/965c2fcedff6dbee97c6e81ff9e86ca7d93ba06b36c08dd84d449d801ca3195b/rust-lang/rust","title":"Releases + Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/releases"},{"description":"Check + end-of-life, release policy and support schedule for Rust.","favicon_url":"https://you.com/favicon?domain=endoflife.date&size=128","original_thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","page_age":"2026-07-11T05:20:01","snippets":["Rust + uses editions to make backwards-incompatible changes. Editions are a mechanism + for opt-in changes that may otherwise pose a backwards compatibility risk. + Since editions are opt-in, existing crates won’t use the changes unless + they explicitly migrate to the new edition. Each crate chooses its edition + within its Cargo.toml file. Editions are skin-deep, and as a result - + you can always use an older edition alongside a supported version of the + compiler.","More information is available on the Rust website. You should + be running one of the supported release numbers listed above in the rightmost + column. ... You can submit an improvement to this page on GitHub . This + page has also a corresponding Talk Page πŸ’¬. A JSON version of this page + is available at /api/v1/products/rust/ πŸ“‘. See the API Documentation πŸ“– + for more information.","Rust is an open source multi-paradigm, general-purpose + programming language that emphasizes performance, type safety, and concurrency. + It enforces memory safety, meaning that all references point to valid + memory, without requiring the use of automated memory management techniques + such as garbage collection Β· A new stable release is produced every six + weeks. Only the latest stable release receives patches."],"thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","title":"Rust + | endoflife.date","url":"https://endoflife.date/rust"},{"description":"The + only aim in Rust is to survive. Everything wants you to die - the island’s + wildlife, other inhabitants, the environment, and other survivors. Do + whatever it takes to last another night. Rust Steam charts, data, update + history.","favicon_url":"https://you.com/favicon?domain=steamdb.info&size=128","original_thumbnail_url":"https://steamdb.info/app/252490.png?_=57609173","snippets":[],"thumbnail_url":"https://steamdb.info/app/252490.png?_=57609173","title":"Rust + Price history Β· SteamDB","url":"https://steamdb.info/app/252490/patchnotes/"},{"description":"Redirecting + to /","favicon_url":"https://you.com/favicon?domain=rust-lang.org&size=128","snippets":[],"title":"Rust + Programming Language","url":"https://rust-lang.org/en-US/"},{"description":"Allow + a dependency to specify both a git repository and an alternate registry. + Just like with crates.io, the git repository will be used locally, but + the registry version will be used when published. Added target.''cfg(..)''.rustdocflags + support in configuration.","favicon_url":"https://you.com/favicon?domain=doc.rust-lang.org&size=128","page_age":"2026-05-28T00:00:00","snippets":["Refer + to Rust’s platform support page for more information on Rust’s tiered + platform support. Relax T: Ord bound for some BinaryHeap methods. ... + Stabilize the config include key. The top-level include config key allows + loading additional config files, enabling better organization, sharing, + and management of Cargo configurations across projects and environments. + docs #16284 Β· Stabilize the pubtime field in registry index. This records + when a crate version was published and enables time-based dependency resolution + in the future.","Putting the #[test] attribute in these places is no longer + ignored, and will now result in an error; this may also result in errors + when generating rustdoc. Error when test attribute is applied to structs + Β· Cargo now sets the CARGO_CFG_DEBUG_ASSERTIONS environment variable in + more situations. This will cause crates depending on static-init versions + 1.0.1 to 1.0.3 to fail compilation with β€œfailed to resolve: use of unresolved + module or unlinked crate parking_lot”. See the linked issue for details.","Add + future incompatibility lint for temporary lifetime shortening in Rust + 1.92 ... cargo publish no longer keeps .crate tarballs as final build + artifacts when build.build-dir is set. These tarballs were previously + included due to an oversight and are now treated as intermediate artifacts. + To get .crate tarballs as final artifacts, use cargo package. In a future + version, this change will apply regardless of build.build-dir.","Usage + of #[bench] without #![feature(custom_test_frameworks)] already triggered + a deny-by-default future-incompatibility lint since Rust 1.77, but will + now become a hard error. Add warn-by-default dangerous_implicit_autorefs + lint against implicit autoref of raw pointer dereference. The lint will + be bumped to deny-by-default in the next version of Rust."],"title":"Rust + Release Notes","url":"https://doc.rust-lang.org/beta/releases.html"},{"description":"975 + votes, 67 comments. Empowering everyone to build reliable and efficient + software.","favicon_url":"https://you.com/favicon?domain=www.reddit.com&size=128","original_thumbnail_url":"https://external-preview.redd.it/rust-1-96-0-is-out-v0-CI5itE2KlGAV4vsTeO9oZsPhsFkQe2fnOR17HHynxAs.jpeg?width=1080&crop=smart&auto=webp&s=28b32b990e90e234feecb5ad39639b766655a30e","page_age":"2026-05-28T19:12:02","snippets":["How + to find out the latest stable Rust version supporting a certain target? + r/rust β€’ Β· upvote Β· Β· comments Β· What''s the best way to track when a + Rust feature has been released? r/rust β€’ Β· upvotes Β· Β· comments Β· Rust + Release Video: Rust 1.89.0 Β· r/rust β€’ Β·","If a user had use third_party_crate::*; + and the only item they use from that crate is now available in the prelude, + that use statement is dead code. Might be counterintuitive, but Rust considers + the first declaration of a symbol to be the canonical one, and anything + after it to either an error (if explicitly used) or dead code (if implicitly + through a glob pattern).","Ahh, I see what you mean now in the reference: + \"Items and named imports are allowed to shadow names from glob imports + in the same namespace. That is, if there is a name already defined by + another item in the same namespace, the glob import will be shadowed.\" + - https://doc.rust-lang.org/reference/items/use-declarations.html#r-items.use.glob.shadowing","Macros + have always been a bit of a problem in Rust; there are a lot of things + that work differently about macros than other things in the import system."],"thumbnail_url":"https://external-preview.redd.it/rust-1-96-0-is-out-v0-CI5itE2KlGAV4vsTeO9oZsPhsFkQe2fnOR17HHynxAs.jpeg?width=1080&crop=smart&auto=webp&s=28b32b990e90e234feecb5ad39639b766655a30e","title":"r/rust + on Reddit: Rust 1.96.0 is out","url":"https://www.reddit.com/r/rust/comments/1tqd97o/rust_1960_is_out/"},{"description":"The + only aim in Rust is to survive. Everything wants you to die - the island’s + wildlife and other inhabitants, the environment, other survivors. Do whatever + it takes to last another night.","favicon_url":"https://you.com/favicon?domain=rust.facepunch.com&size=128","original_thumbnail_url":"https://files.facepunch.com/lewis/1b2411b1/og-image.jpg","page_age":"2026-05-07T00:30:03","snippets":["What’s + changed on Rust in each patch.","Removed all obsolete and generic versions + of BaseEntity.ClientRPC, ClientRPCEx, ClientRPCPlayer, ServerRPC and ServerRPCList + calls - switch to existing methods or write your own via *RPCStart->*RPCWrite->*RPCSend"],"thumbnail_url":"https://files.facepunch.com/lewis/1b2411b1/og-image.jpg","title":"Changes + - Rust - facepunch","url":"https://rust.facepunch.com/changes"},{"description":"Empowering + everyone to build reliable and efficient software. - rust-lang/rust","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/48c59e4644f44114fd3ccc60b5e6d6f6d6f94f2f994d8d230822965280321e16/rust-lang/rust","snippets":["Empowering + everyone to build reliable and efficient software. - rust/RELEASES.md + at main Β· rust-lang/rust","rust-lang / rust Public Β· There was an error + while loading. Please reload this page. Notifications Β· You must be signed + in to change notification settings Β· Fork 15.2k Β· Star 114k Β· main Β· / + Copy path Β· Blame Β· More file actions Β· Blame Β· More file actions Β·"],"thumbnail_url":"https://opengraph.githubassets.com/48c59e4644f44114fd3ccc60b5e6d6f6d6f94f2f994d8d230822965280321e16/rust-lang/rust","title":"rust/RELEASES.md + at main Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/blob/main/RELEASES.md"}]}}' + tool_use_id: chatcmpl-tool-9eb40806dc51de97 + type: tool_result + - content: '{"metadata":{"latency":0.49584054946899414,"query":"Rust release + date","search_uuid":"79f792d7-5f24-4893-9598-ecafddd19e03"},"query":"Rust + release date","results":{"news":[{"description":"Malicious jscrambler + 8.14.0 runs hidden binaries during npm install on Windows, macOS, and + Linux, with no fix available as of July 11, 2026.","page_age":"2026-07-13T06:46:39","thumbnail_url":"https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhodiWPXDsex6YL7v4e2n_D5MJFMht76GRRmHTdiG61tfEmsoZyow4tkiS3ORACILDl2fNP27YQkw1XLSBKGcom-DhCVzzPDdkdSgeMGdxcVL_rgO70x5LqZcjxKAPTtFOquB8LwSBPWrSpflqCzns-cFZ_EVm0WZfV7wSoPM3S-7_IGtCT3AjxY0rfITU/s1700-e365/npm-js.jpg","title":"Compromised + jscrambler 8.14.0 npm Release Drops Rust Infostealer During Install","url":"https://thehackernews.com/2026/07/compromised-jscrambler-8140-npm-release.html"},{"description":"News, + Reviews, Release Date, Trailers, Gameplay and more for Rust of New York","page_age":"2026-06-19T07:01:46","thumbnail_url":"https://kotaku.com/app/uploads/2025/07/kotaku-1200x900-1.jpg","title":"Rust + of New York - Kotaku","url":"https://kotaku.com/games/rust-of-new-york"},{"description":"Former + Metallica bassist Jason Newsted has taken to the road with his new group + The Chophouse Band. This is the initial U.S. tour for the quintet who + will","page_age":"2026-07-13T20:17:08","thumbnail_url":"https://jambands.com/wp-content/uploads/2026/07/Untitled-5-copy-1.jpg","title":"Jason + Newsted Joins Blackberry Smoke for Thin Lizzy and Black Sabbath Covers","url":"https://jambands.com/news/2026/07/13/jason-newsted-joins-blackberry-smoke-for-thin-lizzy-and-black-sabbath-covers/"},{"description":"An + update to the markets of Kylian Mbappe has led experts to update predictions + and odds for the France-Spain match on Monday, July 13, 2026.","page_age":"2026-07-14T01:05:43","thumbnail_url":"https://www.cleveland.com/resizer/v2/6SUL5MB3LNBGDPRNBM2IGYLNCE.png?auth=df6cb126e65c0fbe7f26cc7aca2a127f7008843ca1d6ea12d89b270c8076fa3e&width=1280&smart=true&quality=90","title":"Experts + release updated France–Spain World Cup prediction following recent injury + updates","url":"https://www.cleveland.com/betting/2026/07/experts-release-updated-francespain-world-cup-prediction-following-recent-injury-updates.html"},{"description":"Eagerly + anticipating the next Rust Mobile beta? Be sure to check in this August + as Level Infinite schedule their next closed beta.","page_age":"2026-07-09T15:55:00","thumbnail_url":"https://media.pocketgamer.com/artwork/na-36888-1783610707/rust-mobile-beta-two-keyart.jpg","title":"Rust + Mobile will offer even more equipment and game options in its upcoming + second beta","url":"https://www.pocketgamer.com/rust-mobile/second-closed-beta-coming-this-summer/"},{"description":"","page_age":"2026-07-09T20:41:41","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/ccd3149e6d28d0a8011ef15a1deba35a23af9e45cc1656e5c267bff653e40949/lightyear_networkapi/resizefill_w750_h422;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fwwl_tegna_articles_542%2F88975b78afc24dced4e23500fb2c5105","title":"Ye + to perform in New Orleans for 1st time in 13 years","url":"https://www.yahoo.com/entertainment/music/articles/ye-perform-orleans-1st-time-204141739.html"},{"description":"The + fifth movie in the beloved Pixar franchise was a box-office hit. Here''s + when fans may be able to watch it at home.","page_age":"2026-07-09T23:21:00","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/162f0a7a939c253856cf784cb1c6f91706180b05782bf893007bc97cf39b3c3f/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fparade_250%2F07ed5638c77a95b6cf9eef9a846f4b99","title":"Expected + Streaming Date for ''Toy Story 5'' on Disney+ Revealed: September or October","url":"https://www.yahoo.com/entertainment/movies/articles/expected-streaming-date-toy-story-232100132.html"},{"description":"\"An + intimate story of reconciliation.\"","page_age":"2026-07-12T17:06:00","thumbnail_url":"https://hips.hearstapps.com/hmg-prod/images/a3e5a966-b321-4274-8455-9f9e1b930f6b.jpg?crop=0.577xw:0.433xh;0.309xw,0.171xh&resize=1200:*","title":"\"Extraordinary\" + Oasis movie from Peaky Blinders boss confirms earlier UK release date","url":"https://www.digitalspy.com/movies/a71909275/oasis-dont-look-back-in-anger-earlier-release/"},{"description":"A + guide to keeping your canned food fresh.","page_age":"2026-07-13T15:25:00","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/57677327d0c3507a29784df57beb50f487569cc758a4b22869c2fef681128833/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fsouthern_living_806%2Fa849fcecb19ef12d2163ebc9886c7bff","title":"That + Date On Your Canned Food Might Not Mean What You Think","url":"https://www.yahoo.com/lifestyle/articles/date-canned-food-might-not-152500031.html"},{"description":"\"Obsession,\" + the hit horror film starring Michael Johnston and Inde Navarrette, will + start streaming on Peacock following its release in May.","page_age":"2026-07-10T16:11:22","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/879a365d1acffa6e6118e648e43bb306b09c41302e1c05c2003a0a38ce91725a/lightyear_networkapi/resizefill_w900_h599;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Funited_press_international_articles_356%2F16fb0e5d150d45e45928ed12af3485ef","title":"Horror + hit ''Obsession'' to start streaming July 17 on Peacock","url":"https://www.yahoo.com/entertainment/movies/articles/horror-hit-obsession-start-streaming-161122339.html"}],"web":[{"description":"Console + versions of Rust were first announced in 2019 by Facepunch and Double + Eleven at X019, Microsoft''s Xbox announcement event. The game was set + for release in 2020, but in December 2020, the release date was pushed + back to 2021 with Double Eleven citing the COVID-19 pandemic as having + impeded ...","favicon_url":"https://you.com/favicon?domain=en.wikipedia.org&size=128","page_age":"2026-06-05T21:35:00","snippets":["Console + versions of Rust were first announced in 2019 by Facepunch and Double + Eleven at X019, Microsoft''s Xbox announcement event. The game was set + for release in 2020, but in December 2020, the release date was pushed + back to 2021 with Double Eleven citing the COVID-19 pandemic as having + impeded development.","In early March 2021, Rust''s console edition entered + a closed beta that players could participate in. However, no specific + release date was given until later in March, when it was announced that + the game would be released for PlayStation 4 and Xbox One on 21 May 2021.","On + 13 September 2023 Garry Newman announced works on Rust 2. In April 2025, + Double Eleven announced that they were working on a native version for + PlayStation 5 and Xbox Series X/S. The release date was later announced + in May, with the game launching for early access on 19 June 2025 and the + full release on 26 June 2025.","Rust is a 2018 multiplayer survival video + game developed by Facepunch Studios. It was first released in early access + in December 2013 and received its full release in February 2018. Rust + is available on Windows and macOS. Console versions for PlayStation 4 + and Xbox One developed in conjunction with Double Eleven were released + in May 2021.","Rust supports modded servers which can add additional content. + The game functions on a system of ''Wipes'' in which servers reset player + progress weekly, biweekly or monthly following a set monthly schedule + enforced by Facepunch known as ''Force Wipe''. Force Wipes reset not only + player structures but also resets player crafting blueprints effectively + fully restarting player progress on the first Thursday of each month for + the release of a new update."],"title":"Rust (video game) - Wikipedia","url":"https://en.wikipedia.org/wiki/Rust_(video_game)"},{"description":"The + only aim in Rust is to survive. Everything wants you to die - the island’s + wildlife, other inhabitants, the environment, and other survivors. Do + whatever it takes to last another night.","favicon_url":"https://you.com/favicon?domain=store.steampowered.com&size=128","original_thumbnail_url":"https://shared.fastly.steamstatic.com/store_item_assets/steam/apps/252490/c05e66e166b0fc5fc2a06711d7bfa1b4cb2f45f6/capsule_616x353.jpg?t=1781536981","page_age":"2026-07-06T13:03:04","snippets":["The + only aim in Rust is to survive. Everything wants you to die - the island’s + wildlife, other inhabitants, the environment, and other survivors. Do + whatever it takes to last another night. Recent Reviews: Very Positive + (11,427) - 81% of the 11,427 user reviews in the last 30 days are positive. + English Reviews: Very Positive (526,958) - 86% of the 526,958 user reviews + for this game are positive. Release Date: Feb 8, 2018 Β·","Title: Rust + Genre: Action, Adventure, Indie, Massively Multiplayer, RPG Β· Developer: + Facepunch Studios Β· Publisher: Facepunch Studios Release Date: Feb 8, + 2018 Β· Visit the website Discord X YouTube TikTok Facebook Twitch Instagram + Bilibili Douyin QQ Β· QQ 1043317996 Bluesky View update history Read related + news View discussions Visit the Workshop Find Community Groups Β·","Rust + now enters its 13th year of continued expansion and has had over 395 content + updates, with a guaranteed content patch every month.","There are no rules + in Rust but the ones you make for yourself. Craft tools, armour, and weapons + to defend yourself. Cooperate with friends, make alliances, and build + a settlement together."],"thumbnail_url":"https://shared.fastly.steamstatic.com/store_item_assets/steam/apps/252490/c05e66e166b0fc5fc2a06711d7bfa1b4cb2f45f6/capsule_616x353.jpg?t=1781536981","title":"Rust + on Steam","url":"https://store.steampowered.com/app/252490/Rust/"},{"description":"Rust + Versions # Stable: 1.95.0 Beta: 1.96.0 (28 May, 2026, 8 days left) Nightly: + 1.97.0 (9 July, 2026, 50 days left) Ongoing Stabilization PRs # Stabilize + core::range::{legacy, RangeFull, RangeTo} (4 days old) T-compiler - Relevant + to the compiler team, which will review and decide on the PR/issue.","favicon_url":"https://you.com/favicon?domain=releases.rs&size=128","page_age":"2026-06-02T03:45:04","snippets":["T-rustdoc + - Relevant to the rustdoc team, which will review and decide on the PR/issue.","T-rustdoc-frontend + - Relevant to the rustdoc-frontend team, which will review and decide + on the web UI/UX output.","T-rust-analyzer - Relevant to the rust-analyzer + team, which will review and decide on the PR/issue.","T-rustdoc-internals + - Relevant to the rustdoc internals team, which will review and decide + on the PR/issue."],"title":"Rust Versions | Rust Changelogs","url":"https://releases.rs/"},{"description":"Rust: + Directed by Joel Souza. With Alec Baldwin, Josh Hopkins, Patrick Scott + McDermott, Travis Fimmel. An orphaned boy of 13 left to fend for himself + and his younger brother in 1880s Wyoming is sentenced to hang for the + accidental killing of a local rancher. His estranged grandfather breaks + ...","favicon_url":"https://you.com/favicon?domain=www.imdb.com&size=128","original_thumbnail_url":"https://m.media-amazon.com/images/M/MV5BYzkzMzVmMGItYjFmOC00MzU2LThmNDItMzY4MDRkYzM1MDZjXkEyXkFqcGc@._V1_FMjpg_UX1000_.jpg","page_age":"2025-05-02T00:00:00","snippets":["Rust: + Directed by Joel Souza. With Alec Baldwin, Josh Hopkins, Patrick Scott + McDermott, Travis Fimmel. An orphaned boy of 13 left to fend for himself + and his younger brother in 1880s Wyoming is sentenced to hang for the + accidental killing of a local rancher. His estranged grandfather breaks + him out of jail and they go on the run to Mexico.","Joel Souza''s Rust, + finally seeing release in 2025 after a tumultuous and tragic production, + arrives with a weight of expectation and somber reflection. The film itself, + a Western centered on an outlaw grandfather and his estranged grandson + on the run, attempts to capture the rugged beauty and inherent dangers + of the American frontier.","This creates a complex and often uncomfortable + viewing experience, where appreciation for the filmmaking is intertwined + with a profound sense of loss and the ethical considerations surrounding + its release. Despite the inherent challenges, Rust as a film showcases + moments of genuine cinematic merit.","Ultimately, Rust is a film that + exists in a complex space, its narrative achievements forever intertwined + with the real-life tragedy that marred its production. While Joel Souza + manages to deliver a Western with moments of compelling storytelling and + strong performances, the film''s release serves as a stark reminder of + the human cost of filmmaking."],"thumbnail_url":"https://m.media-amazon.com/images/M/MV5BYzkzMzVmMGItYjFmOC00MzU2LThmNDItMzY4MDRkYzM1MDZjXkEyXkFqcGc@._V1_FMjpg_UX1000_.jpg","title":"Rust + (2024) ⭐ 5.7 | Drama, Western","url":"https://www.imdb.com/title/tt11001074/"},{"description":"Check + end-of-life, release policy and support schedule for Rust.","favicon_url":"https://you.com/favicon?domain=endoflife.date&size=128","original_thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","page_age":"2026-07-11T05:20:01","snippets":["Rust + is an open source multi-paradigm, general-purpose programming language + that emphasizes performance, type safety, and concurrency. It enforces + memory safety, meaning that all references point to valid memory, without + requiring the use of automated memory management techniques such as garbage + collection Β· A new stable release is produced every six weeks.","Only + the latest stable release receives patches. Each new stable release spends + the preceding six-week period as the Beta release. Rust uses editions + to make backwards-incompatible changes. Editions are a mechanism for opt-in + changes that may otherwise pose a backwards compatibility risk.","Each + crate chooses its edition within its Cargo.toml file. Editions are skin-deep, + and as a result - you can always use an older edition alongside a supported + version of the compiler. Editions are expected to be released every three + years. ... Rust 2015, the default edition from Rust 1.0.","More information + is available on the Rust website. You should be running one of the supported + release numbers listed above in the rightmost column."],"thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","title":"Rust + | endoflife.date","url":"https://endoflife.date/rust"},{"description":"Click + here to be redirected to the latest Rust release announcement.","favicon_url":"https://you.com/favicon?domain=blog.rust-lang.org&size=128","page_age":"2026-07-09T05:20:01","snippets":[],"title":"Announcing + Rust 1.97.0","url":"https://blog.rust-lang.org/releases/latest/"},{"description":"This + helps manage resources such ... or released automatically. Object lifetime + refers to the period of time during which a reference is valid; that is, + the time between the object creation and destruction. These lifetimes + are implicitly associated with all Rust reference ...","favicon_url":"https://you.com/favicon?domain=en.wikipedia.org&size=128","original_thumbnail_url":"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Rust_programming_language_black_logo.svg/1280px-Rust_programming_language_black_logo.svg.png","page_age":"2026-07-15T10:24:11","snippets":["This + helps manage resources such as file handles, network sockets, and locks, + since when objects are dropped, the resources associated with them are + closed or released automatically. Object lifetime refers to the period + of time during which a reference is valid; that is, the time between the + object creation and destruction. These lifetimes are implicitly associated + with all Rust reference types.","Editions are released to allow making + limited breaking changes, such as promoting await to a keyword to support + async/await features. Crates targeting different editions can interoperate + with each other, so a crate can upgrade to a new edition even if its callers + or its dependencies still target older editions. Migration to a new edition + can be assisted with automated tooling. rust-analyzer is a set of utilities + that provides integrated development environments (IDEs) and text editors + with information about a Rust project through the Language Server Protocol.","Anderson, + Brian (2012-01-24). \"[rust-dev] The Rust compiler 0.1 is unleashed\". + rust-dev (Mailing list). Archived from the original on 2012-01-24. Retrieved + 2025-01-07. ... Anthony, Sebastian (2012-01-24). \"Mozilla releases Rust + 0.1, the language that will eventually usurp Firefox''s C++\".","Rust + Magazine. Retrieved 2023-02-07. ... Sei, Mark (2018-10-10). \"Fedora 29 + new features: Startis now officially in Fedora\". Marksei, Weekly sysadmin + pills. Archived from the original on 2019-04-13. Retrieved 2019-05-13. + ... Proven, Liam (2022-07-12). \"Oracle Linux 9 released, with some interesting + additions\".","Software developer Graydon Hoare created Rust in 2006 while + working at Mozilla, which officially sponsored the project in 2009. The + first stable release, Rust 1.0, was published in May 2015."],"thumbnail_url":"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Rust_programming_language_black_logo.svg/1280px-Rust_programming_language_black_logo.svg.png","title":"Rust + (programming language) - Wikipedia","url":"https://en.wikipedia.org/wiki/Rust_(programming_language)"},{"description":"Rust + was a limited release in 2025 on Friday, May 2, 2025. There were 16 other + movies released on the same date, including Thunderbolts*, Off the Record + and A Tooth Fairy Tale. As a limited theatrical release, Rust will only + be shown in select movie theaters across major markets.","favicon_url":"https://you.com/favicon?domain=www.movieinsider.com&size=128","page_age":"2025-08-31T17:43:12","snippets":["Rust + on DVD July 22, 2025 starring Alec Baldwin, Frances Fisher, Josh Hopkins, + Travis Fimmel. Western outlaw Harland Rust (Baldwin), who has a bounty + on his head, discovers his 13 year-old grandson is convicted of an accidental + murder","Please check Fandango and Atom Tickets to see if the film is + playing in your area. Sometimes, limited theatrical releases are simulatenously + released to buy or rent on video on demand services like Prime Video and + Apple TV. ... Rust was released on DVD & Blu-ray on Tuesday, July 22, + 2025 .","Western outlaw Harland Rust (Baldwin), who has a bounty on his + head, discovers his 13 year-old grandson is convicted of an accidental + murder and sentenced to hang. Rust travels to Kansas to break his Grandson + out of prison, and the two fugitives must escape bounty hunters and a + legandary U.S.","A look at the Rust behind-the-scenes crew and production + team. The film''s director Joel Souza last directed Crown Vic and Celestia."],"title":"Everything + You Need to Know About Rust Movie (2025): Aug. 31, 2025 ...","url":"https://www.movieinsider.com/m18454/rust"},{"description":"Rust + (2024) - Movies, TV, Celebs, and more","favicon_url":"https://you.com/favicon?domain=www.imdb.com&size=128","original_thumbnail_url":"https://m.media-amazon.com/images/M/MV5BYzkzMzVmMGItYjFmOC00MzU2LThmNDItMzY4MDRkYzM1MDZjXkEyXkFqcGc@._V1_FMjpg_UX1000_.jpg","snippets":["Release + date (13) Also known as (AKA) (24)Edit Β· United Kingdom Β· July 7, 2024(DVD) + Poland Β· November 20, 2024(Camerimage International Film Festival, premiere) + Germany Β· May 1, 2025 Β· United States Β· May 2, 2025(internet) United States + Β· May 2, 2025(limited) 8 moreSee all Β· (original title) Rust Β·","Rust + (2024) - Movies, TV, Celebs, and more..."],"thumbnail_url":"https://m.media-amazon.com/images/M/MV5BYzkzMzVmMGItYjFmOC00MzU2LThmNDItMzY4MDRkYzM1MDZjXkEyXkFqcGc@._V1_FMjpg_UX1000_.jpg","title":"Rust + (2024) - Release info - IMDb","url":"https://www.imdb.com/title/tt11001074/releaseinfo/"},{"description":"Rust + Console Edition will be released on May 21, 2021, for PS4 and Xbox One. + It will also be playable on PS5 and Xbox Series X/S via backward compatibility.","favicon_url":"https://you.com/favicon?domain=www.ign.com&size=128","original_thumbnail_url":"https://assets1.ignimgs.com/2021/01/14/rust-how-to-play-with-friends-1610656889028_160w.jpg?width=1280&format=jpg&auto=webp&quality=80","page_age":"2021-03-27T16:05:58","snippets":["Rust, + the competitive online multiplayer survival game that was first released + in early access on PC in 2013, is officially coming to PlayStation 4 and + Xbox One (and PS5 and Xbox Series X/S via backward compatibility) on May + 21, 2021.","This means that Rust Console Edition will follow its own update + roadmap that differs from the PC version that will \"provide an optimal + player experience while gradually introducing players to the vast amount + of game play and content that makes Rust an incredible experience.\" While + Double Eleven isn''t quite ready to reveal the roadmap, the studio promises + some of it will be shown closer to the release of the game in May."],"thumbnail_url":"https://assets1.ignimgs.com/2021/01/14/rust-how-to-play-with-friends-1610656889028_160w.jpg?width=1280&format=jpg&auto=webp&quality=80","title":"Rust + Console Edition Release Date Announced for PS4 and Xbox One - IGN","url":"https://www.ign.com/articles/rust-console-edition-release-date-announced-for-ps4-and-xbox-one"}]}}' + tool_use_id: chatcmpl-tool-953805f2e70f765d + type: tool_result + role: user + model: qwen3 + stream: true + tools: + - description: Search the web. + input_schema: + properties: + query: + type: string + required: + - query + type: object + name: web_search + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + method: POST + path: /v1/messages + query_params: {} + response: + headers: + content-type: text/event-stream; charset=utf-8 + sse: + - 'event: message_start + + ' + - 'data: {"type":"message_start","message":{"id":"chatcmpl-804cb26caacf6e54","type":"message","role":"assistant","content":[],"model":"qwen3","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":12128,"output_tokens":0}}} + + ' + - ' + + ' + - 'event: content_block_start + + ' + - 'data: {"type":"content_block_start","content_block":{"type":"thinking","thinking":""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"\n"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"Okay"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + let"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''s"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + tackle"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + this"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + step"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + by"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + step"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + user"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + asked"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + for"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + and"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + its"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + requiring"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + two"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + separate"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + web"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + searches"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + \n\n"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"First"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + I"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + need"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + to"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + process"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + results"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + from"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + first"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + for"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + web"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + results"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + mention"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + \""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"Ann"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"ouncing"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"\""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + and"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + \""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"R"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"ust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Versions"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + |"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Ch"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"angel"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"ogs"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"\""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + which"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + lists"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"5"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + stable"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"6"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + beta"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + and"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + nightly"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + However"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + blog"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + post"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + from"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + July"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"6"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + mentions"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + So"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + stable"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + likely"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":".\n\n"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"Next"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + second"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + for"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + results"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + include"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + a"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Wikipedia"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + entry"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + stating"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + that"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + was"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + published"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + in"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + May"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"5"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + but"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + that"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''s"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + initial"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + more"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + recent"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + info"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + from"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + blog"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + post"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + \""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"Ann"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"ouncing"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"\""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + might"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + have"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + However"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + exact"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + isn"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''t"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + specified"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + in"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + snippets"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Another"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + result"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + mentions"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + \""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"R"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"ust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Console"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Edition"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"\""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + released"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + on"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + May"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + but"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + that"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''s"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + for"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + game"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + not"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + programming"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + language"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Wikipedia"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + article"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + on"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + programming"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + language"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + notes"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + that"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + was"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + in"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + May"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"5"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + but"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''s"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + specific"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + isn"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''t"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + clear"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + However"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + blog"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + post"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + from"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + July"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"6"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + indicates"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + that"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + but"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + exact"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + isn"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''t"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + provided"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + \n\n"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"Wait"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + user"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + might"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + be"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + referring"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + to"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + programming"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + language"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + not"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + game"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + game"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + has"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + a"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + different"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + user"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + probably"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + wants"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + of"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + programming"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + language"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + from"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + but"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + isn"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''t"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + explicitly"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + stated"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + However"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + \""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"R"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"ust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Versions"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + |"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Ch"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"angel"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"ogs"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"\""},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + page"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + shows"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + that"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + nightly"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + of"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + July"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"6"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + stable"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + might"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + be"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"5"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + but"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + blog"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + post"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + mentions"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + There"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''s"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + some"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + confusion"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + here"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + \n\n"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"To"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + clarify"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + stable"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + of"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + search"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + results"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + released"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + on"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + July"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"6"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + for"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + programming"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + language"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''s"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + July"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"6"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + The"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + game"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + Rust"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"''s"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + is"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + different"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + but"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + user"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + likely"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + meant"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + language"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + So"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + answer"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + should"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + state"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + the"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + latest"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + version"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"1"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"7"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"."},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + and"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + its"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + release"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + date"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + as"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + July"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"9"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":","},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":" + "},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"0"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"2"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"6"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":".\n"},"index":0} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"signature_delta","signature":"1beffd2c452945a2bb6ca613ebe0a61f"},"index":0} + + ' + - ' + + ' + - 'event: content_block_stop + + ' + - 'data: {"type":"content_block_stop","index":0} + + ' + - ' + + ' + - 'event: content_block_start + + ' + - 'data: {"type":"content_block_start","content_block":{"type":"text","text":""},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"\n\n"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"The"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" latest"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" Rust"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" version"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" is"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" **"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"1"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"."},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"9"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"7"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"."},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"0"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"**,"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" released"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" on"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" **"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"July"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" "},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"9"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":","},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" "},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"2"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"0"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"2"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"6"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"**"},"index":1} + + ' + - ' + + ' + - 'event: content_block_delta + + ' + - 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"."},"index":1} + + ' + - ' + + ' + - 'event: content_block_stop + + ' + - 'data: {"type":"content_block_stop","index":1} + + ' + - ' + + ' + - 'event: message_delta + + ' + - 'data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":12128,"output_tokens":612}} + + ' + - ' + + ' + - 'event: message_stop + + ' + - 'data: {"type":"message_stop"} + + ' + - ' + + ' + status_code: 200 diff --git a/crates/agentic-server-core/tests/cassettes/messages_multiround/no-tool-call-qwen3-nonstreaming.yaml b/crates/agentic-server-core/tests/cassettes/messages_multiround/no-tool-call-qwen3-nonstreaming.yaml new file mode 100644 index 0000000..7392fbe --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/messages_multiround/no-tool-call-qwen3-nonstreaming.yaml @@ -0,0 +1,61 @@ +turns: +- filename: t1 + request: + body: + max_tokens: 300 + messages: + - content: What is 2+2? Answer directly, do not use any tools. + role: user + model: qwen3 + stream: false + tools: + - description: Search the web. + input_schema: + properties: + query: + type: string + required: + - query + type: object + name: web_search + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + method: POST + path: /v1/messages + query_params: {} + response: + body: + content: + - signature: c1d6d688f8bc4af68d663d69116b7451 + thinking: ' + + Okay, the user is asking "What is 2+2?" and they want the answer directly + without using any tools. Let me think. + + + First, this is a basic arithmetic problem. 2 plus 2 equals 4. I don''t need + to use any functions or search the web because it''s a straightforward question. + The user specified not to use any tools, so I should just provide the answer + immediately. No need to call the web_search function here. I''ll make sure + to state the answer clearly and concisely. + + ' + type: thinking + - text: ' + + + 2 + 2 equals 4.' + type: text + id: chatcmpl-987513aee54aaf68 + model: qwen3 + role: assistant + stop_reason: end_turn + type: message + usage: + input_tokens: 159 + output_tokens: 121 + headers: + content-type: application/json + status_code: 200 diff --git a/crates/agentic-server-core/tests/cassettes/messages_multiround/parallel-web-search-qwen3-nonstreaming.yaml b/crates/agentic-server-core/tests/cassettes/messages_multiround/parallel-web-search-qwen3-nonstreaming.yaml new file mode 100644 index 0000000..832df29 --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/messages_multiround/parallel-web-search-qwen3-nonstreaming.yaml @@ -0,0 +1,502 @@ +turns: +- filename: t1 + request: + body: + max_tokens: 1024 + messages: + - content: Use web_search to find the latest Rust version, then use web_search + again to find the latest Python version, then state both. + role: user + model: qwen3 + stream: false + tools: + - description: Search the web for current information. + input_schema: + properties: + query: + type: string + required: + - query + type: object + name: web_search + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + method: POST + path: /v1/messages + query_params: {} + response: + body: + content: + - signature: 0cc32e0f10bf4334b6fe996d239343f7 + thinking: "\nOkay, the user wants me to find the latest Rust version and the\ + \ latest Python version using web_search. Let me start by figuring out how\ + \ to structure the queries.\n\nFirst, for Rust. The latest version is probably\ + \ announced on their official website or maybe a news blog. So I should\ + \ search for \"latest Rust version\" or \"Rust programming language latest\ + \ release\". Maybe the official Rust website has a \"Downloads\" section\ + \ that lists the current stable version. Alternatively, checking a reliable\ + \ tech news site might have the latest info.\n\nNext, Python. Similarly,\ + \ the Python website's \"Downloads\" page would have the latest version.\ + \ But I should confirm if there's a recent release. Maybe search for \"\ + latest Python version\" or \"Python programming language latest release\"\ + . Also, checking the Python Software Foundation's official site would be\ + \ the most accurate source.\n\nWait, the user wants me to use web_search\ + \ twice, once for each language. So I need to make two separate function\ + \ calls. The first one for Rust, the second for Python. Each time, the query\ + \ should be specific enough to get the latest version. Let me make sure\ + \ the search terms are clear. Maybe \"Rust latest stable version\" and \"\ + Python latest version\". \n\nI should also consider that sometimes the latest\ + \ version might be a development release, but the user probably wants the\ + \ stable one. So including \"stable\" in the search might help. But maybe\ + \ the official sites automatically show the latest stable. Hmm. Alternatively,\ + \ check for the most recent major or minor version. \n\nOnce I get the results\ + \ from the web searches, I can extract the version numbers and present them.\ + \ But since I can't execute the searches, I need to simulate the process.\ + \ However, in a real scenario, the function would return the search results,\ + \ and I'd parse them to find the versions. \n\nSo the steps are: first call\ + \ web_search with \"latest Rust version\", then call web_search again with\ + \ \"latest Python version\", then compile both results and state them. Alright,\ + \ that's the plan.\n" + type: thinking + - id: chatcmpl-tool-b6ddcc2891299003 + input: + query: latest Rust version + name: web_search + type: tool_use + - id: chatcmpl-tool-a5945cf518d40532 + input: + query: latest Python version + name: web_search + type: tool_use + id: chatcmpl-9507c0d7a9ac4107 + model: qwen3 + role: assistant + stop_reason: tool_use + type: message + usage: + input_tokens: 172 + output_tokens: 459 + headers: + content-type: application/json + status_code: 200 +- filename: t2 + request: + body: + max_tokens: 1024 + messages: + - content: Use web_search to find the latest Rust version, then use web_search + again to find the latest Python version, then state both. + role: user + - content: + - id: chatcmpl-tool-b6ddcc2891299003 + input: + query: latest Rust version + name: web_search + type: tool_use + - id: chatcmpl-tool-a5945cf518d40532 + input: + query: latest Python version + name: web_search + type: tool_use + role: assistant + - content: + - content: '{"metadata":{"latency":0.4433350563049317,"query":"latest Rust + version","search_uuid":"57f85555-8b1c-4f25-83af-39f502deb78a"},"query":"latest + Rust version","results":{"news":[{"description":"Mighty Morphin Power + Rangers #2 Reveals Its Heart, But Is Held Back From Its Potential (Review)","page_age":"2026-07-16T01:01:33","thumbnail_url":"https://comicbook.com/wp-content/uploads/sites/4/2026/07/Mighty-Morphin-Power-Rangers-2.jpg?resize=2000,1125","title":"Mighty + Morphin Power Rangers #2 Reveals Its Heart, But Is Held Back From Its + Potential (Review)","url":"https://comicbook.com/comics/review/mighty-morphin-power-rangers-2-reveals-its-heart-but-is-held-back-from-its-potential-review/"},{"description":"You''re + not the only one with a key.","page_age":"2026-07-02T19:16:23","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/f10abddbbcbcd6170a27a53c5c0de04d74d64574a1892e3ee431b5e6cea30578/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fpc_gamer_708%2F2e8f245e655fc750d4cd765499c51a76","title":"Rust + gets even more cutthroat by adding its own version of apartment rentals","url":"https://tech.yahoo.com/gaming/articles/rust-gets-even-more-cutthroat-191623715.html"},{"description":"Malicious + jscrambler 8.14.0 runs hidden binaries during npm install on Windows, + macOS, and Linux, with no fix available as of July 11, 2026.","page_age":"2026-07-13T06:46:39","thumbnail_url":"https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhodiWPXDsex6YL7v4e2n_D5MJFMht76GRRmHTdiG61tfEmsoZyow4tkiS3ORACILDl2fNP27YQkw1XLSBKGcom-DhCVzzPDdkdSgeMGdxcVL_rgO70x5LqZcjxKAPTtFOquB8LwSBPWrSpflqCzns-cFZ_EVm0WZfV7wSoPM3S-7_IGtCT3AjxY0rfITU/s1700-e365/npm-js.jpg","title":"Compromised + jscrambler 8.14.0 npm Release Drops Rust Infostealer During Install","url":"https://thehackernews.com/2026/07/compromised-jscrambler-8140-npm-release.html"},{"description":"Eagerly + anticipating the next Rust Mobile beta? Be sure to check in this August + as Level Infinite schedule their next closed beta.","page_age":"2026-07-09T15:55:00","thumbnail_url":"https://media.pocketgamer.com/artwork/na-36888-1783610707/rust-mobile-beta-two-keyart.jpg","title":"Rust + Mobile will offer even more equipment and game options in its upcoming + second beta","url":"https://www.pocketgamer.com/rust-mobile/second-closed-beta-coming-this-summer/"},{"description":"After + an opening-round 65 at the Scottish Open, Rory McIlroy explained the reason + he doesn''t feel rusty despite having played only four times since winning + the Masters","page_age":"2026-07-09T15:33:03","thumbnail_url":"https://s.yimg.com/os/en/golf_digest_articles_737/d8524d67d05a25ed4c13358404640eef","title":"Rory + McIlroy says he''s benefiting from a lighter schedule. Here''s why","url":"https://sports.yahoo.com/articles/rory-mcilroy-says-hes-benefiting-153303945.html"},{"description":"The + Atlanta Braves have two players recovering from injuries officially in + the rehab stage of their recoveries","page_age":"2026-07-13T19:27:08","thumbnail_url":"https://images2.minutemediacdn.com/image/upload/c_crop,x_0,y_0,w_3974,h_2235/c_fill,w_1440,ar_1440:810,f_auto,q_auto,g_auto/shape/cover/sport/01kxee2snea8t7jbaqgy-a7f2d87fe57f1668d64f6ddf872e9c44.webp","title":"Ha-Seong + Kim Sharp in First Braves Rehab Game, AcuΓ±a Rusty","url":"https://www.si.com/mlb/braves/onsi/ha-seong-kim-sharp-first-braves-rehab-game-acuna-rusty"},{"description":"Plans + to extend Zakynthos'' iconic Navagio Beach by 30 metres aim to preserve + its rusting shipwreck for future generations, as visitors continue to + be barred from the popular island attraction because ...","page_age":"2026-07-13T12:56:57","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/0fc2ca54e2ad166ca5429f663256f69579db7930cf4e28828ddd9ba0f023ecfd/lightyear_networkapi/resizefill_w1024_h576;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Feuronews_us_news_articles_566%2F8b2deb640dc4a5a47e8a99ec6f49105b","title":"Greece''s + famous β€˜Shipwreck Beach’ to be extended to save its iconic wreck","url":"https://www.yahoo.com/news/world/articles/greeces-famous-shipwreck-beach-extended-125657791.html"},{"description":"Ohio + tops CNBC''s rankings for the first time in 2026, with the Buckeye State + culminating a yearslong climb as the place to do business in America.","page_age":"2026-07-09T18:10:56","thumbnail_url":"https://image.cnbcfm.com/api/v1/image/108330101-1783012419076-gettyimages-2267146157-SOFTBANK_DATA_CENTER.jpeg?v=1783434396&w=1920&h=1080","title":"Ohio + is America''s Top State for Business in 2026, capping yearslong rise with + first No. 1 finish","url":"https://www.cnbc.com/2026/07/09/ohio-no-1-americas-top-states-for-business-rankings.html"},{"description":"At + the Open Source Summit in Mumbai, Torvalds discusses the pain and power + of AI in the kernel, and why Linux no longer supports ''museum'' technology.","page_age":"2026-07-08T21:09:50","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/78cc427d8d85d82723b419f8f03c12e49e1d28279ae0e60443122d1cfbde8784/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fzdnet_articles_730%2F133ed8711fa74a3a05edae71b635e7b9","title":"''I''m + not a programmer'' anymore: Linus Torvalds on the only two tools he uses + now","url":"https://tech.yahoo.com/ai/articles/im-not-programmer-anymore-linus-210950038.html"},{"description":"Sen. + Sheldon Whitehouse says whistleblowers have alleged numerous problems + with rushed reconstruction of the Kennedy Center.","page_age":"2026-07-12T21:40:39","thumbnail_url":"https://ca-times.brightspotcdn.com/dims4/default/5f6c940/2147483647/strip/true/crop/5915x3105+0+419/resize/1200x630!/quality/75/?url=https%3A%2F%2Fcalifornia-times-brightspot.s3.amazonaws.com%2F45%2F71%2Fe499b7894ff8a069cf35076ab381%2F318521025cb8478fbc006f8cdb2bbd3a.jpg","title":"Democrat + announces whistleblower allegations of construction problems at Kennedy + Center","url":"https://www.latimes.com/world-nation/story/2026-07-12/democrat-announces-whistleblower-allegations-of-construction-problems-at-kennedy-center"}],"web":[{"description":"Click + here to be redirected to the latest Rust release announcement.","favicon_url":"https://you.com/favicon?domain=blog.rust-lang.org&size=128","page_age":"2026-07-09T05:20:01","snippets":[],"title":"Announcing + Rust 1.97.0","url":"https://blog.rust-lang.org/releases/latest/"},{"description":"Rust + Versions # Stable: 1.95.0 Beta: 1.96.0 (28 May, 2026, 8 days left) Nightly: + 1.97.0 (9 July, 2026, 50 days left) Ongoing Stabilization PRs # Stabilize + core::range::{legacy, RangeFull, RangeTo} (4 days old) T-compiler - Relevant + to the compiler team, which will review and decide on the PR/issue.","favicon_url":"https://you.com/favicon?domain=releases.rs&size=128","page_age":"2026-06-02T03:45:04","snippets":["T-rustdoc + - Relevant to the rustdoc team, which will review and decide on the PR/issue.","T-rustdoc-frontend + - Relevant to the rustdoc-frontend team, which will review and decide + on the web UI/UX output.","T-rust-analyzer - Relevant to the rust-analyzer + team, which will review and decide on the PR/issue.","T-rustdoc-internals + - Relevant to the rustdoc internals team, which will review and decide + on the PR/issue."],"title":"Rust Versions | Rust Changelogs","url":"https://releases.rs/"},{"description":"Empowering + everyone to build reliable and efficient software. - Releases Β· rust-lang/rust","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/965c2fcedff6dbee97c6e81ff9e86ca7d93ba06b36c08dd84d449d801ca3195b/rust-lang/rust","snippets":["Empowering + everyone to build reliable and efficient software. - rust-lang/rust","Allow + a dependency to specify both a git repository and an alternate registry. + Just like with crates.io, the git repository will be used locally, but + the registry version will be used when published. Added target.''cfg(..)''.rustdocflags + support in configuration.","Refer to Rust''s platform support page for + more information on Rust''s tiered platform support. Relax T: Ord bound + for some BinaryHeap methods. ... Stabilize the config include key. The + top-level include config key allows loading additional config files, enabling + better organization, sharing, and management of Cargo configurations across + projects and environments. docs #16284 Β· Stabilize the pubtime field in + registry index. This records when a crate version was published and enables + time-based dependency resolution in the future.","Putting the #[test] + attribute in these places is no longer ignored, and will now result in + an error; this may also result in errors when generating rustdoc. Error + when test attribute is applied to structs Β· Cargo now sets the CARGO_CFG_DEBUG_ASSERTIONS + environment variable in more situations. This will cause crates depending + on static-init versions 1.0.1 to 1.0.3 to fail compilation with \"failed + to resolve: use of unresolved module or unlinked crate parking_lot\"."],"thumbnail_url":"https://opengraph.githubassets.com/965c2fcedff6dbee97c6e81ff9e86ca7d93ba06b36c08dd84d449d801ca3195b/rust-lang/rust","title":"Releases + Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/releases"},{"description":"Check + end-of-life, release policy and support schedule for Rust.","favicon_url":"https://you.com/favicon?domain=endoflife.date&size=128","original_thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","page_age":"2026-07-11T05:20:01","snippets":["Rust + uses editions to make backwards-incompatible changes. Editions are a mechanism + for opt-in changes that may otherwise pose a backwards compatibility risk. + Since editions are opt-in, existing crates won’t use the changes unless + they explicitly migrate to the new edition. Each crate chooses its edition + within its Cargo.toml file. Editions are skin-deep, and as a result - + you can always use an older edition alongside a supported version of the + compiler.","More information is available on the Rust website. You should + be running one of the supported release numbers listed above in the rightmost + column. ... You can submit an improvement to this page on GitHub . This + page has also a corresponding Talk Page πŸ’¬. A JSON version of this page + is available at /api/v1/products/rust/ πŸ“‘. See the API Documentation πŸ“– + for more information.","Rust is an open source multi-paradigm, general-purpose + programming language that emphasizes performance, type safety, and concurrency. + It enforces memory safety, meaning that all references point to valid + memory, without requiring the use of automated memory management techniques + such as garbage collection Β· A new stable release is produced every six + weeks. Only the latest stable release receives patches."],"thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","title":"Rust + | endoflife.date","url":"https://endoflife.date/rust"},{"description":"The + only aim in Rust is to survive. Everything wants you to die - the island’s + wildlife, other inhabitants, the environment, and other survivors. Do + whatever it takes to last another night. Rust Steam charts, data, update + history.","favicon_url":"https://you.com/favicon?domain=steamdb.info&size=128","original_thumbnail_url":"https://steamdb.info/app/252490.png?_=57609173","snippets":[],"thumbnail_url":"https://steamdb.info/app/252490.png?_=57609173","title":"Rust + Price history Β· SteamDB","url":"https://steamdb.info/app/252490/patchnotes/"},{"description":"Redirecting + to /","favicon_url":"https://you.com/favicon?domain=rust-lang.org&size=128","snippets":[],"title":"Rust + Programming Language","url":"https://rust-lang.org/en-US/"},{"description":"Allow + a dependency to specify both a git repository and an alternate registry. + Just like with crates.io, the git repository will be used locally, but + the registry version will be used when published. Added target.''cfg(..)''.rustdocflags + support in configuration.","favicon_url":"https://you.com/favicon?domain=doc.rust-lang.org&size=128","page_age":"2026-05-28T00:00:00","snippets":["Refer + to Rust’s platform support page for more information on Rust’s tiered + platform support. Relax T: Ord bound for some BinaryHeap methods. ... + Stabilize the config include key. The top-level include config key allows + loading additional config files, enabling better organization, sharing, + and management of Cargo configurations across projects and environments. + docs #16284 Β· Stabilize the pubtime field in registry index. This records + when a crate version was published and enables time-based dependency resolution + in the future.","Putting the #[test] attribute in these places is no longer + ignored, and will now result in an error; this may also result in errors + when generating rustdoc. Error when test attribute is applied to structs + Β· Cargo now sets the CARGO_CFG_DEBUG_ASSERTIONS environment variable in + more situations. This will cause crates depending on static-init versions + 1.0.1 to 1.0.3 to fail compilation with β€œfailed to resolve: use of unresolved + module or unlinked crate parking_lot”. See the linked issue for details.","Add + future incompatibility lint for temporary lifetime shortening in Rust + 1.92 ... cargo publish no longer keeps .crate tarballs as final build + artifacts when build.build-dir is set. These tarballs were previously + included due to an oversight and are now treated as intermediate artifacts. + To get .crate tarballs as final artifacts, use cargo package. In a future + version, this change will apply regardless of build.build-dir.","Usage + of #[bench] without #![feature(custom_test_frameworks)] already triggered + a deny-by-default future-incompatibility lint since Rust 1.77, but will + now become a hard error. Add warn-by-default dangerous_implicit_autorefs + lint against implicit autoref of raw pointer dereference. The lint will + be bumped to deny-by-default in the next version of Rust."],"title":"Rust + Release Notes","url":"https://doc.rust-lang.org/beta/releases.html"},{"description":"975 + votes, 67 comments. Empowering everyone to build reliable and efficient + software.","favicon_url":"https://you.com/favicon?domain=www.reddit.com&size=128","original_thumbnail_url":"https://external-preview.redd.it/rust-1-96-0-is-out-v0-CI5itE2KlGAV4vsTeO9oZsPhsFkQe2fnOR17HHynxAs.jpeg?width=1080&crop=smart&auto=webp&s=28b32b990e90e234feecb5ad39639b766655a30e","page_age":"2026-05-28T19:12:02","snippets":["How + to find out the latest stable Rust version supporting a certain target? + r/rust β€’ Β· upvote Β· Β· comments Β· What''s the best way to track when a + Rust feature has been released? r/rust β€’ Β· upvotes Β· Β· comments Β· Rust + Release Video: Rust 1.89.0 Β· r/rust β€’ Β·","If a user had use third_party_crate::*; + and the only item they use from that crate is now available in the prelude, + that use statement is dead code. Might be counterintuitive, but Rust considers + the first declaration of a symbol to be the canonical one, and anything + after it to either an error (if explicitly used) or dead code (if implicitly + through a glob pattern).","Ahh, I see what you mean now in the reference: + \"Items and named imports are allowed to shadow names from glob imports + in the same namespace. That is, if there is a name already defined by + another item in the same namespace, the glob import will be shadowed.\" + - https://doc.rust-lang.org/reference/items/use-declarations.html#r-items.use.glob.shadowing","Macros + have always been a bit of a problem in Rust; there are a lot of things + that work differently about macros than other things in the import system."],"thumbnail_url":"https://external-preview.redd.it/rust-1-96-0-is-out-v0-CI5itE2KlGAV4vsTeO9oZsPhsFkQe2fnOR17HHynxAs.jpeg?width=1080&crop=smart&auto=webp&s=28b32b990e90e234feecb5ad39639b766655a30e","title":"r/rust + on Reddit: Rust 1.96.0 is out","url":"https://www.reddit.com/r/rust/comments/1tqd97o/rust_1960_is_out/"},{"description":"The + only aim in Rust is to survive. Everything wants you to die - the island’s + wildlife and other inhabitants, the environment, other survivors. Do whatever + it takes to last another night.","favicon_url":"https://you.com/favicon?domain=rust.facepunch.com&size=128","original_thumbnail_url":"https://files.facepunch.com/lewis/1b2411b1/og-image.jpg","page_age":"2026-05-07T00:30:03","snippets":["What’s + changed on Rust in each patch.","Removed all obsolete and generic versions + of BaseEntity.ClientRPC, ClientRPCEx, ClientRPCPlayer, ServerRPC and ServerRPCList + calls - switch to existing methods or write your own via *RPCStart->*RPCWrite->*RPCSend"],"thumbnail_url":"https://files.facepunch.com/lewis/1b2411b1/og-image.jpg","title":"Changes + - Rust - facepunch","url":"https://rust.facepunch.com/changes"},{"description":"Empowering + everyone to build reliable and efficient software. - rust-lang/rust","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/48c59e4644f44114fd3ccc60b5e6d6f6d6f94f2f994d8d230822965280321e16/rust-lang/rust","snippets":["Empowering + everyone to build reliable and efficient software. - rust/RELEASES.md + at main Β· rust-lang/rust","rust-lang / rust Public Β· There was an error + while loading. Please reload this page. Notifications Β· You must be signed + in to change notification settings Β· Fork 15.2k Β· Star 114k Β· main Β· / + Copy path Β· Blame Β· More file actions Β· Blame Β· More file actions Β·"],"thumbnail_url":"https://opengraph.githubassets.com/48c59e4644f44114fd3ccc60b5e6d6f6d6f94f2f994d8d230822965280321e16/rust-lang/rust","title":"rust/RELEASES.md + at main Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/blob/main/RELEASES.md"}]}}' + tool_use_id: chatcmpl-tool-b6ddcc2891299003 + type: tool_result + - content: '{"metadata":{"latency":0.6569726467132568,"query":"latest Python + version","search_uuid":"b10d1cae-8b00-4670-9a54-dc111057bc42"},"query":"latest + Python version","results":{"news":[{"description":"MicroPython, for the + uninitiated, is a pared-down version of python meant to run on today’s + powerful microcontollers. As impressive as it was for its day, the SNES + is not quite in their leagu…","page_age":"2026-07-11T20:00:04","thumbnail_url":"https://hackaday.com/wp-content/uploads/2026/07/micropython-snes-featured.jpg","title":"MicroPython + Is This Summer’s Hottest Title For The SNES, Thanks To Claude Fable","url":"https://hackaday.com/2026/07/11/micropython-is-this-summers-hottest-title-for-the-snes-thanks-to-claude-fable/"},{"description":"According + to the Bible, the world’s first con man was a snake. It deceived Adam + and Eve, leading them to commit the first sin, which is how we wound up + living in this messed-up world we have now. I me...","page_age":"2026-06-18T04:05:09","thumbnail_url":"https://s.yimg.com/os/en/florida_phoenix_articles_551/e96f29c562fad941f13b2581d88cb610","title":"Python + captures by Florida scientists overshadowed by hoopla over annual amateur + contest","url":"https://www.yahoo.com/news/science/articles/python-captures-florida-scientists-overshadowed-040509842.html"},{"description":"OpenAI + made a number of clarification changes to its crawler help documentation + to say that version number of the user-agent string may be updated and + it may use a user-agent string with an additional robots.txt marker.","page_age":"2026-07-14T00:00:00","thumbnail_url":"https://images.seroundtable.com/openai-crawl-hero-y3OTfea5.jpg","title":"OpenAI + Clarifies Its Crawlers Can Update Its User Agent String","url":"https://www.seroundtable.com/openai-crawlers-user-agent-string-41649.html"},{"description":"Animals + can be pushed into smaller spaces or into new overlaps with other species.","page_age":"2026-07-13T04:04:00","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/6a13994e2d586ae458adaf2da3629e14bb4e8908b4f75aba4fbb5d86e12b6d50/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fthe_cool_down_737%2Fb3edbcfc04087444b34eab1547098e52","title":"Video + shows crocodile attacking python in dramatic reptile-versus-reptile clash","url":"https://www.yahoo.com/news/science/articles/video-shows-crocodile-attacking-python-040400952.html"},{"description":"The + state''s annual Florida Python Challenge began Friday with hunters seeking + to nab snakes and big prize money. The 10-day challenge, held from July + 10-19, 2026,","page_age":"2026-07-10T00:00:00","thumbnail_url":"https://ewscripps.brightspotcdn.com/dims4/default/d6f73ca/2147483647/strip/true/crop/1280x672+0+64/resize/1200x630!/quality/90/?url=http%3A%2F%2Fewscripps-brightspot.s3.amazonaws.com%2Fab%2F98%2Ffdd9499942eb81fce8d9b735506f%2Fpythonchallenge.jpg","title":"Florida + Python Challenge kicks off: 10-day hunt offers $10,000 prize to remove + invasive snakes","url":"https://www.wptv.com/news/protecting-paradise/florida-python-challenge-kicks-off-10-day-hunt-offers-10-000-prize-to-remove-invasive-snakes"},{"description":"A + SLITHERING nightmare has been unleashed on a Chinese village after nearly + 900 snakes escaped a flooded breeding farm. Extreme weather brought by + a devastating typhoon caused the farm to collapse …","page_age":"2026-07-09T08:45:51","thumbnail_url":"https://www.the-sun.com/wp-content/uploads/sites/6/2026/07/5613d154-5bb7-4893-a274-c2ea2656718a.jpg?quality=90&strip=all&w=735&h=876&crop=1","title":"Horror + vid show snakes including venomous cobras plague flooded village","url":"https://www.the-sun.com/news/16660533/hundreds-of-escaped-snakes-plague-flooded-village/"},{"description":"Spread + the loveβ€œ`html Updating Python is a crucial task for both novice and seasoned + programmers. Whether you’re maintaining compatibility with the latest + packages or enhancing the performance and security of your projects, knowing + how to update Python effectively can significantly impact your work. This + article will guide you through the essential steps and considerations + involved in the update process, ensuring you have a smooth experience + while keeping your Python environment up-to-date. 1. Understanding the + Importance of Updating Python Before diving into the how-to, it’s vital + to grasp why updating Python is essential. Each new version of Python + introduces bug fixes, […]","page_age":"2026-06-13T06:31:32","thumbnail_url":"https://www.thetechedvocate.org/wp-content/uploads/2026/06/featured-image-1781332277831.png","title":"How + to update Python","url":"https://www.thetechedvocate.org/how-to-update-python/"},{"description":"The + snake did not attack the chickens, apparently.","page_age":"2026-06-19T16:58:00","thumbnail_url":"https://s.yimg.com/ny/api/res/1.2/HieO9CdA3rapbowQ7nZ7.A--/YXBwaWQ9aGlnaGxhbmRlcjt3PTEyMDA7aD02NzU7Y2Y9d2VicA--/https://media.zenfs.com/en/the_cool_down_737/bd3f72c398872124de06684c519649da","title":"Resident + locks cat in chicken coop to save it from stray dogs, then finds python + too full to move","url":"https://www.yahoo.com/news/world/articles/resident-locks-cat-chicken-coop-165800255.html"},{"description":"Wildman’s + Pizza, Pasta, and Python is doing its part to combat the invasive species.","page_age":"2026-07-15T20:30:00","thumbnail_url":"https://www.popsci.com/wp-content/uploads/2026/07/Burmese-Python-Coiled.jpg?quality=85&w=1200","title":"Florida + man will trade pizza for pythons","url":"https://www.popsci.com/environment/python-for-pizza-restaurant-florida/"},{"description":"Audiences + attending the national tour of \"Monty Python''s Spamalot\" at the Denver + Center for the Performing Arts next month will see plenty of outrageous + comedy on stage, but much of the magic begins behind the curtain.","page_age":"2026-07-15T18:14:55","thumbnail_url":"https://assets1.cbsnewsstatic.com/hub/i/r/2026/07/15/40768a7f-ae7e-4d04-bac0-6df6e29012bb/thumbnail/1200x630/df50cea8c6151314b2e730acf6ba7a84/killer-rabbit.jpg","title":"Go + backstage for prop tour of \"Spamalot,\" coming to Denver Center for the + Performing Arts","url":"https://www.cbsnews.com/colorado/news/spamalot-denver-monty-python-props-buell-theatre/"}],"web":[{"description":"Historically, + most, but not all, Python releases have also been GPL-compatible. The + Licenses page details GPL-compatibility and Terms and Conditions. ... + For most Unix systems, you must download and compile the source code. + The same source code archive can also be used to build the Windows and + Mac versions, and is the starting point for ports to all other platforms. + Download the latest ...","favicon_url":"https://you.com/favicon?domain=www.python.org&size=128","original_thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","snippets":["Historically, + most, but not all, Python releases have also been GPL-compatible. The + Licenses page details GPL-compatibility and Terms and Conditions. ... + For most Unix systems, you must download and compile the source code. + The same source code archive can also be used to build the Windows and + Mac versions, and is the starting point for ports to all other platforms. + Download the latest Python 3 source.","The official home of the Python + Programming Language","See Status of Python versions for all an overview + of all versions, including unsupported.","Python versions before 3.14 + are also signed using OpenPGP private keys of the respective release manager. + In this case, verification through the release manager''s public key is + also possible."],"thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","title":"Download + Python | Python.org","url":"https://www.python.org/downloads/"},{"description":"The + main branch is currently the future Python 3.16, and is the only branch + that accepts new features. The latest release for each Python version + can be found on the download page.(See below for a ...","favicon_url":"https://you.com/favicon?domain=devguide.python.org&size=128","original_thumbnail_url":"https://devguide.python.org/_static/og-image-200x200.png","page_age":"2026-05-27T13:42:06","snippets":["Python + releases go through five phases, as described in PEP 602. Release managers + can adjust specific dates as needed. ... Before the first beta, the next + full release can accept new features, bug fixes, and security fixes. ... + After the first beta, no new features can go in, but feature fixes (including + significant changes to new features), bug fixes, and security fixes are + accepted for the upcoming feature release. ... Once a version has been + fully released, bug fixes and security fixes are accepted.","(See below + for a chart with older versions. Another useful visualization is endoflife.date/python.)","After + two years (18 months for versions before 3.13), only security fixes are + accepted and no more binaries are released.","New source-only versions + can be released as needed."],"thumbnail_url":"https://devguide.python.org/_static/og-image-200x200.png","title":"Status + of Python versions","url":"https://devguide.python.org/versions/"},{"description":"Learn + what the latest Python version is, why upgrading matters, and how to check + and update Python on Linux, macOS, and Windows.","favicon_url":"https://you.com/favicon?domain=phoenixnap.com&size=128","original_thumbnail_url":"https://phoenixnap.com/kb/wp-content/uploads/2026/03/the-latest-version-of-python.png","page_age":"2026-03-12T05:59:17","snippets":["Python + is one of the most widely used programming languages for web development, + automation, data science, and system administration. Because the language + evolves quickly, new versions regularly introduce performance improvements, + security fixes, and new features. The latest stable Python version ensures + better performance, improved compatibility with modern libraries, and + ongoing security support.","This guide will explain the current Python + release, why updating matters, and how to check and upgrade your installed + version. As of February 2026, the latest stable version of Python is Python + 3.14.3, released on February 3, 2026.","Python versions typically receive + about 2 years of regular bugfix releases, followed by about 3 additional + years of security-only updates, for a total of roughly 5 years of support. + Running the latest stable Python version improves reliability, performance, + and security.","To check your Python version, run the following command + in your terminal: ... The command prints the installed version. If the + installed version is older than the current stable release, consider upgrading + to access the latest features and security updates."],"thumbnail_url":"https://phoenixnap.com/kb/wp-content/uploads/2026/03/the-latest-version-of-python.png","title":"The + Latest Version of Python | phoenixNAP KB","url":"https://phoenixnap.com/kb/latest-python-version"},{"description":"Python + 3.14 is the latest release, packed with features like free-threaded mode, + JIT compiler, lazy annotations, and a modern colorized REPL. Learn more + about what’s new and why upgrading matters.","favicon_url":"https://you.com/favicon?domain=www.liquidweb.com&size=128","original_thumbnail_url":"https://res.cloudinary.com/lwcom/images/w_1024,h_1024,c_scale/f_auto,q_auto/v1776926892/prod/python-logo-3/python-logo-3.png?_i=AA","page_age":"2026-01-14T21:46:37","snippets":["Python + continues to evolve, bringing powerful new features, enhanced security, + and performance improvements with every release. The latest major version, + Python 3.14 was officially released on October 7, 2025.","Better compatibility + to support the latest libraries and frameworks. If you’re boxed into an + older Python version, there are multiple workarounds and best practices + to consider.","Software companies like Python release upgrades to ensure + that users can work with a more efficient, secure, and feature-rich version + while keeping up with industry advancements. ... Protect your applications + from exploits. Optimize processing speed. Modernize the language. Phase + out outdated functionalities. Maintain a clean and efficient codebase. + Align with the latest standards and user needs.","To check your current + Python version, use the following commands: ... You can download the latest + version of Python from the official Python website."],"thumbnail_url":"https://res.cloudinary.com/lwcom/images/w_1024,h_1024,c_scale/f_auto,q_auto/v1776926892/prod/python-logo-3/python-logo-3.png?_i=AA","title":"Latest + Python Version (2025) – What''s New in Python 3.14?","url":"https://www.liquidweb.com/blog/latest-python-version/"},{"description":"The + official home of the Python Programming Language","favicon_url":"https://you.com/favicon?domain=www.python.org&size=128","original_thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","snippets":["Python + releases are now listed on the downloads page.","The Python Software Foundation + is the organization behind Python."],"thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","title":"Releases + | Python.org","url":"https://www.python.org/download/releases/"},{"description":"The + official home of the Python Programming Language","favicon_url":"https://you.com/favicon?domain=www.python.org&size=128","original_thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","snippets":["Note: + Python 3.12.0 has been superseded by Python 3.12.13.","Python 3.12.0 is + the newest major release of the Python programming language, and it contains + many new features and optimizations.","Support for the buffer protocol + in Python code (PEP 688).","Support for the Linux perf profiler to report + Python function names in traces."],"thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","title":"Python + Release Python 3.12.0 | Python.org","url":"https://www.python.org/downloads/release/python-3120/"},{"description":"The + official home of the Python Programming Language","favicon_url":"https://you.com/favicon?domain=www.python.org&size=128","original_thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","snippets":["Python>>> + Downloads>>> Windows Β· Latest Python install manager - Python install + manager 26.3 Β· Latest Python 3 Release - Python 3.14.6 Β· Python install + manager 26.3 - June 30, 2026 Β· Download Installer (MSIX) Download MSI + package Β· Python 3.13.14 - June 10, 2026 Β·","Note that Python 3.13.14 + cannot be used on Windows 7 or earlier.","Download using the Python install + manager.","Note that Python 3.13.13 cannot be used on Windows 7 or earlier."],"thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","title":"Python + Releases for Windows | Python.org","url":"https://www.python.org/downloads/windows/"},{"description":"Check + end-of-life, release policy and support schedule for Python.","favicon_url":"https://you.com/favicon?domain=endoflife.date&size=128","original_thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","page_age":"2026-07-14T11:10:29","snippets":["A + Python release only supports a Windows platform while Microsoft considers + the platform under extended support. Python 3.8 was the last version to + support Windows 7.","A JSON version of this page is available at /api/v1/products/python/ + πŸ“‘. See the API Documentation πŸ“– for more information.","python --version + # or alternatively python3 --version","Python is an interpreted, high-level, + general-purpose programming language."],"thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","title":"Python + | endoflife.date","url":"https://endoflife.date/python"},{"description":"The + official home of the Python Programming Language","favicon_url":"https://you.com/favicon?domain=www.python.org&size=128","original_thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","snippets":["Python + 1.4, released on 25 October 1996 Β· The latest, and unreleased, documentation + for versions of Python still under development: Development version Β· + Python 3.x Β· The Python Software Foundation is the organization behind + Python.","For unreleased (in development) documentation, see In development + versions. ... The Python Software Foundation is the organization behind + Python.","Some previous versions of the documentation remain available + online.","Use the list below to select a version to view."],"thumbnail_url":"https://www.python.org/static/opengraph-icon-200x200.png","title":"Python + documentation by version | Python.org","url":"https://www.python.org/doc/versions/"},{"description":"Before + removing or installing anything, ... local versions of Python: I write + a bunch of small scripts that don''t use any third-party libraries, and + don''t need a dedicated virtual environment. I want to have a python command + on my system that always points to the latest point release ...","favicon_url":"https://you.com/favicon?domain=www.mostlypython.com&size=128","original_thumbnail_url":"https://storage.ghost.io/c/b0/88/b088fa59-ac78-44d9-9662-4c7568d7cb9a/content/images/2025/08/status_python_versions_bordered-1.png","page_age":"2025-08-07T16:30:08","snippets":["Before + removing or installing anything, I want to re-evaluate my needs for local + versions of Python: I write a bunch of small scripts that don''t use any + third-party libraries, and don''t need a dedicated virtual environment. + I want to have a python command on my system that always points to the + latest point release of the most recent stable version of Python.","My + work has become more varied and complex, but the tools for managing different + Python versions have gotten much better. So, I should end up with a simpler + setup even though my needs have gotten more complicated. It''s also a + good to check out Python''s status page, and see what the latest point + releases are for each major release:","$ uv python install Installed Python + 3.13.5 in 930ms + cpython-3.13.5-macos-aarch64-none (python3.13) If you + don''t specify a version, uv will install the latest stable release.","$ + uv python upgrade All versions already on latest supported patch release"],"thumbnail_url":"https://storage.ghost.io/c/b0/88/b088fa59-ac78-44d9-9662-4c7568d7cb9a/content/images/2025/08/status_python_versions_bordered-1.png","title":"Updating + Python","url":"https://www.mostlypython.com/updating-python/"}]}}' + tool_use_id: chatcmpl-tool-a5945cf518d40532 + type: tool_result + role: user + model: qwen3 + stream: false + tools: + - description: Search the web for current information. + input_schema: + properties: + query: + type: string + required: + - query + type: object + name: web_search + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + method: POST + path: /v1/messages + query_params: {} + response: + body: + content: + - signature: fd138160af0449b3905a43ed4d0ec899 + thinking: ' + + Okay, let''s see. The user asked to find the latest Rust and Python versions + using web searches. First, I need to check the tool responses. + + + For Rust, the search results show a blog post titled "Announcing Rust 1.97.0" + and another mention of Rust 1.95.0 and 1.96.0. The latest stable version + seems to be 1.97.0. The web results also mention Rust 1.97.0 as the latest + release. + + + For Python, the results mention Python 3.14.6 and Python 3.13.14. However, + there''s a note about Python 3.12.0 being superseded by 3.12.13. The latest + stable version appears to be 3.14.6. Another source says Python 3.14 is + the latest, released in 2025. But the most recent version in the search + results is 3.14.6. + + + Wait, but some sources might have different update times. The user wants + the latest, so I should confirm which is more recent. The Python.org download + page lists 3.14.6 as the latest. So, the answer should be Rust 1.97.0 and + Python 3.14.6. + + ' + type: thinking + - text: "\n\nThe latest versions are:\n\n- **Rust**: 1.97.0 \n- **Python**:\ + \ 3.14.6 \n\nThese are the most recent stable releases as of the latest\ + \ search results." + type: text + id: chatcmpl-a486c6f7b0bbcb55 + model: qwen3 + role: assistant + stop_reason: end_turn + type: message + usage: + input_tokens: 10609 + output_tokens: 331 + headers: + content-type: application/json + status_code: 200 diff --git a/crates/agentic-server-core/tests/cassettes/messages_multiround/sequential-web-search-qwen3-nonstreaming.yaml b/crates/agentic-server-core/tests/cassettes/messages_multiround/sequential-web-search-qwen3-nonstreaming.yaml new file mode 100644 index 0000000..60f7b0a --- /dev/null +++ b/crates/agentic-server-core/tests/cassettes/messages_multiround/sequential-web-search-qwen3-nonstreaming.yaml @@ -0,0 +1,832 @@ +turns: +- filename: t1 + request: + body: + max_tokens: 1024 + messages: + - content: First call web_search to find the latest stable Rust version number. + Then, in a SEPARATE second web_search call, search for the release notes + of exactly that version. Only then answer. Make the searches one at a time, + not together. + role: user + model: qwen3 + stream: false + tools: + - description: Search the web for current information. + input_schema: + properties: + query: + type: string + required: + - query + type: object + name: web_search + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + method: POST + path: /v1/messages + query_params: {} + response: + body: + content: + - signature: 4759f7c704c54e71bf34892b8ac51fd0 + thinking: "\nOkay, let's tackle this step by step. The user wants me to first\ + \ find the latest stable Rust version number using a web search. Then, in\ + \ a separate search, look for the release notes of exactly that version.\ + \ I need to make sure I do each search one at a time.\n\nFirst, I'll start\ + \ with the first query: \"latest stable Rust version number\". I need to\ + \ call the web_search function with that query. Let me format the tool call\ + \ correctly. The function name is web_search, and the argument is the query.\ + \ So the JSON should be {\"name\": \"web_search\", \"arguments\": {\"query\"\ + : \"latest stable Rust version number\"}}. \n\nOnce I get the result from\ + \ that search, I'll have the version number. Then, the next step is to search\ + \ for the release notes of that specific version. The second query would\ + \ be something like \"Rust [version] release notes\", replacing [version]\ + \ with the actual number found in the first step. But since I can't execute\ + \ the first search yet, I'll have to wait for the response. However, according\ + \ to the user's instructions, I need to make the searches one at a time.\ + \ So the first tool call is just the initial search for the version. After\ + \ that, the user (or the system) would process the result and then proceed\ + \ to the second search. \n\nI need to ensure that the first tool call is\ + \ correctly structured and that the second call is separate. The user emphasized\ + \ that the searches should be done one after the other, not together. So\ + \ I'll proceed with the first search first.\n" + type: thinking + - id: chatcmpl-tool-86f4e1d8180872c1 + input: + query: latest stable Rust version number + name: web_search + type: tool_use + id: chatcmpl-85b38914ade59756 + model: qwen3 + role: assistant + stop_reason: tool_use + type: message + usage: + input_tokens: 196 + output_tokens: 352 + headers: + content-type: application/json + status_code: 200 +- filename: t2 + request: + body: + max_tokens: 1024 + messages: + - content: First call web_search to find the latest stable Rust version number. + Then, in a SEPARATE second web_search call, search for the release notes + of exactly that version. Only then answer. Make the searches one at a time, + not together. + role: user + - content: + - id: chatcmpl-tool-86f4e1d8180872c1 + input: + query: latest stable Rust version number + name: web_search + type: tool_use + role: assistant + - content: + - content: '{"metadata":{"latency":0.633882999420166,"query":"latest stable + Rust version number","search_uuid":"1dc90bce-1380-424b-86a3-9767d2325102"},"query":"latest + stable Rust version number","results":{"news":[{"description":"C won''t + be disappearing tomorrow, says the stable kernel maintainer, but the future + of Linux belongs to Rust.","page_age":"2026-07-15T16:13:42","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/2bdc8a3e1869eab0452a0fac6e599caba4c8496223475dd9e0a193f7b3fd8d98/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fzdnet_articles_730%2F5ea4c915b08f4417f23ec9143f381180","title":"''Rust + makes coding fun again'': Why Linux is moving away from C, according to + Greg Kroah-Hartman","url":"https://tech.yahoo.com/computing/articles/rust-makes-coding-fun-again-161342409.html"},{"description":"Xiaomi + just rolled out the stable Android 17 update to the Xiaomi 17 Ultra and + Xiaomi 17. The only issue is that it''s still based on HyperOS 3.0, so + you won''t see any new features.","page_age":"2026-07-16T07:45:51","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/ad2d7ffb2de57f87b99027ef35c0ab6878da2a854f5421ea2d052f32ea2bb61b/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fandroid_central_347%2F339a43e3965ba3504f0886c44b62e9e0","title":"Xiaomi + somehow rolls out stable Android 17 update ahead of its rivals","url":"https://tech.yahoo.com/phones/articles/xiaomi-somehow-rolls-stable-android-074551979.html"},{"description":"A + man is in critical but stable condition but stable condition after being + stabbed in Pittsburgh on Wednesday.","page_age":"2026-07-16T03:45:23","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/0bb0be4cf36a2674d69f0bdcf2444762fa66c60cbafae5bcfa867d24395a2daa/lightyear_networkapi/resizefill_w300_h169;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fwpxi_cox_articles_540%2F6faa5b7a77d8ef1d57ab56e66606ad77","title":"Man + in critical, but stable condition after stabbing in Pittsburgh","url":"https://www.yahoo.com/news/articles/man-critical-stable-condition-stabbing-034523965.html"},{"description":"Redis + CVE-2026-23479 enables authenticated RCE; affecting versions since 7.2.0, + patched May 5 to reduce exploitation risk.","page_age":"2026-06-03T13:58:10","thumbnail_url":"https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjTV6zPqD9KC3Rc5Mz9c8XENLiJntboDT6LIoD3L2FXlTUVC3rsWZ_3YLfe_jmhhyphenhyphenjb5RCwkTsdoyypD9VXxYgj_2GYaAupNMlOyZkycm94qr7XiarmBCpYQdZDN_Gwq6KrOmomzx1dmbz4hpUkw4OZparqEbydJneNKaWQI0fcP5tctpKxsZ7kFP5FCv0/s1700-e365/redis-db.png","title":"Autonomous + AI Tool Finds 2-Year-Old RCE Flaw in Redis (CVE-2026-23479)","url":"https://thehackernews.com/2026/06/autonomous-ai-tool-finds-2-year-old-rce.html"},{"description":"The + first public beta release of iOS 27 has just hit Apple devices, and here + are five cool new features you can take for a spin after installing the + update.","page_age":"2026-07-13T21:13:28","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/2f32549c6a571c5c328364097e74a1e957c1d51d2ddaffa38952f2c2be39d5be/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fbgr_ecomm_183%2F8da9cbfa864cc31686c78e6ba081daae","title":"5 + Cool New iOS 27 Features To Try In The First Public Beta","url":"https://tech.yahoo.com/ai/apple-intelligence/articles/5-cool-ios-27-features-211328715.html"},{"description":"WASHINGTON, + July 16 (Reuters) - The number of Americans filing claims for unemployment + benefits fell last week, pointing to continued labor market stability. + Initial claims for state unemployment benefits dropped 8,000 to a seasonally + adjusted 208,0...","page_age":"2026-07-16T07:50:00","thumbnail_url":"https://storage.googleapis.com/media.mwcradio.com/mimesis/2026-07/16/2026-07-16T125029Z_1_LYNXMPEM6F0U7_RTROPTP_3_USA-ECONOMY.JPG","title":"US + weekly jobless claims fall; labor market remains stable","url":"https://kelo.com/2026/07/16/us-weekly-jobless-claims-fall-labor-market-remains-stable/"},{"description":"American + workers'' share of the nation''s income is at its lowest point in almost + 80 years, as more of the economy''s gains flow to corporations and investors.","page_age":"2026-06-30T09:00:04","thumbnail_url":"https://assets3.cbsnewsstatic.com/hub/i/r/2026/06/26/03953273-3a9e-4da1-ab74-21b0b58858ea/thumbnail/1200x630/dacc2f73d9272aaa59c7545f89296ae9/gettyimages-2272349966.jpg","title":"This + number helps explain why many Americans are down on the economy","url":"https://www.cbsnews.com/news/labor-share-income-lowest-since-world-war-ii/"},{"description":"Mighty + Morphin Power Rangers #2 Reveals Its Heart, But Is Held Back From Its + Potential (Review)","page_age":"2026-07-16T01:01:33","thumbnail_url":"https://comicbook.com/wp-content/uploads/sites/4/2026/07/Mighty-Morphin-Power-Rangers-2.jpg?resize=2000,1125","title":"Mighty + Morphin Power Rangers #2 Reveals Its Heart, But Is Held Back From Its + Potential (Review)","url":"https://comicbook.com/comics/review/mighty-morphin-power-rangers-2-reveals-its-heart-but-is-held-back-from-its-potential-review/"},{"description":"Netflix + rolls out a ''VHS'' special edition of Stranger Things season 1 and says + more seasons could follow if enough people watch it.","page_age":"2026-07-16T09:19:06","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/0e792cc17e526c0c69d3fc7b967467181e541068d40429d69cccad13e0aeaeda/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fengadget_100%2F7d5d7d5f5c045134cc3f8adee2347188","title":"Netflix + Releases A VHS-Quality Version Of Stranger Things'' First Season","url":"https://www.yahoo.com/entertainment/tv/articles/netflix-releases-vhs-quality-version-091906497.html"},{"description":"Jaylen + Waddle is expected to be an immediate success for the Denver Broncos in + 2026.","page_age":"2026-07-03T20:28:06","thumbnail_url":"https://s.yimg.com/os/en/the_sporting_news_articles_584/3e3d140840b708f27813c2e1ddacf889","title":"Broncos'' + newest offensive weapon predicted to hit massive numbers in 2026","url":"https://sports.yahoo.com/articles/broncos-newest-offensive-weapon-predicted-202806292.html"}],"web":[{"description":"Rust + Versions # Stable: 1.95.0 Beta: 1.96.0 (28 May, 2026, 8 days left) Nightly: + 1.97.0 (9 July, 2026, 50 days left) Ongoing Stabilization PRs # Stabilize + core::range::{legacy, RangeFull, RangeTo} (4 days old) T-compiler - Relevant + to the compiler team, which will review and decide on the PR/issue.","favicon_url":"https://you.com/favicon?domain=releases.rs&size=128","page_age":"2026-06-02T03:45:04","snippets":["T-rustdoc + - Relevant to the rustdoc team, which will review and decide on the PR/issue. + ... T-compiler - Relevant to the compiler team, which will review and + decide on the PR/issue. S-blocked - Status: Blocked on something else + such as an RFC or other implementation work. needs-fcp - This change is + insta-stable, or significant enough to need a team FCP to proceed.","T-rustdoc + - Relevant to the rustdoc team, which will review and decide on the PR/issue. + S-waiting-on-review - Status: Awaiting review from the assignee but also + interested parties. proposed-final-comment-period - Proposed to merge/close + by relevant subteam, see T- ... T-libs-api - Relevant to the library API + team, which will review and decide on the PR/issue. needs-fcp - This change + is insta-stable, or significant enough to need a team FCP to proceed.","T-rustdoc + - Relevant to the rustdoc team, which will review and decide on the PR/issue. + T-compiler - Relevant to the compiler team, which will review and decide + on the PR/issue. S-waiting-on-review - Status: Awaiting review from the + assignee but also interested parties. S-blocked - Status: Blocked on something + else such as an RFC or other implementation work. needs-fcp - This change + is insta-stable, or significant enough to need a team FCP to proceed.","needs-fcp + - This change is insta-stable, or significant enough to need a team FCP + to proceed. A-rust-for-linux - Relevant for the Rust-for-Linux project"],"title":"Rust + Versions | Rust Changelogs","url":"https://releases.rs/"},{"description":"Click + here to be redirected to the latest Rust release announcement","favicon_url":"https://you.com/favicon?domain=blog.rust-lang.org&size=128","page_age":"2026-07-09T05:20:01","snippets":["Click + here to be redirected to the latest Rust release announcement."],"title":"Announcing + Rust 1.97.0","url":"https://blog.rust-lang.org/releases/latest/"},{"description":"Empowering + everyone to build reliable and efficient software. - Releases Β· rust-lang/rust","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/965c2fcedff6dbee97c6e81ff9e86ca7d93ba06b36c08dd84d449d801ca3195b/rust-lang/rust","snippets":["Empowering + everyone to build reliable and efficient software. - rust-lang/rust","Allow + a dependency to specify both a git repository and an alternate registry. + Just like with crates.io, the git repository will be used locally, but + the registry version will be used when published. Added target.''cfg(..)''.rustdocflags + support in configuration.","Refer to Rust''s platform support page for + more information on Rust''s tiered platform support. Relax T: Ord bound + for some BinaryHeap methods. ... Stabilize the config include key. The + top-level include config key allows loading additional config files, enabling + better organization, sharing, and management of Cargo configurations across + projects and environments. docs #16284 Β· Stabilize the pubtime field in + registry index. This records when a crate version was published and enables + time-based dependency resolution in the future.","Putting the #[test] + attribute in these places is no longer ignored, and will now result in + an error; this may also result in errors when generating rustdoc. Error + when test attribute is applied to structs Β· Cargo now sets the CARGO_CFG_DEBUG_ASSERTIONS + environment variable in more situations. This will cause crates depending + on static-init versions 1.0.1 to 1.0.3 to fail compilation with \"failed + to resolve: use of unresolved module or unlinked crate parking_lot\"."],"thumbnail_url":"https://opengraph.githubassets.com/965c2fcedff6dbee97c6e81ff9e86ca7d93ba06b36c08dd84d449d801ca3195b/rust-lang/rust","title":"Releases + Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/releases"},{"description":"Fix + errors in std::fs::rename on Windows 10, version 1607. Downgrade bootstrap + cc to fix custom targets. Skip submodule updates when building Rust from + a source tarball. The 2024 Edition is now stable.","favicon_url":"https://you.com/favicon?domain=doc.rust-lang.org&size=128","page_age":"2026-05-28T00:00:00","snippets":["powerpc64: + Use the ELF ABI version set in target spec instead of guessing (fixes + the ELF ABI used by the OpenBSD target) Matching on a #[non_exhaustive] + enum now reads the discriminant, even if the enum has only one variant. + This can cause closures to capture values that they previously wouldn’t. + mut ref and mut ref mut patterns, part of the unstable Match Ergonomics + 2024 RFC, were accidentally allowed on stable within struct pattern field + shorthand.","Previously, they could not be used without the standard library, + which has no stable build mechanism. In preparation for the build-std + project adding that support, JSON target specs are being proactively gated + to ensure they remain unstable even if build-std is stabilized.","The + unstable -Zprofile flag (β€œgcov-style” coverage instrumentation) has been + removed. This does not affect the stable flags for coverage instrumentation + (-Cinstrument-coverage) and profile-guided optimization (-Cprofile-generate, + -Cprofile-use), which are unrelated and remain available.","Use lld by + default on x86_64-unknown-linux-gnu. See also https://blog.rust-lang.org/2025/09/01/rust-lld-on-1.90.0-stable/."],"title":"Rust + Release Notes","url":"https://doc.rust-lang.org/beta/releases.html"},{"description":"Check + end-of-life, release policy and support schedule for Rust.","favicon_url":"https://you.com/favicon?domain=endoflife.date&size=128","original_thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","page_age":"2026-07-11T05:20:01","snippets":["Rust + is an open source multi-paradigm, general-purpose programming language + that emphasizes performance, type safety, and concurrency. It enforces + memory safety, meaning that all references point to valid memory, without + requiring the use of automated memory management techniques such as garbage + collection Β· A new stable release is produced every six weeks. Only the + latest stable release receives patches.","Editions are skin-deep, and + as a result - you can always use an older edition alongside a supported + version of the compiler. Editions are expected to be released every three + years. ... Rust 2015, the default edition from Rust 1.0. Rust 2018, which + added path and module system changes and was released alongside 1.31 in + December 2018. Rust 2021, released alongside 1.56 in October 2021. Rust + 2024, released alongside 1.85 in February 2025. More information is available + on the Rust website. You should be running one of the supported release + numbers listed above in the rightmost column."],"thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","title":"Rust + | endoflife.date","url":"https://endoflife.date/rust"},{"description":"I + note that the rust forge displays the current stable, beta and nightly + versions. That seems to be set by an mdbook preprocessor called blackjack + and I was not able to see, quickly, where blackjack gets its data from. + Also, the stable version given there contains only the major and minor + parts of the semantic version, excluding the patch number. The GitHub + API provides the latest ...","favicon_url":"https://you.com/favicon?domain=stackoverflow.com&size=128","original_thumbnail_url":"https://stackoverflow.com/Content/Sites/stackoverflow/Img/apple-touch-icon@2.png?v=0f0cab681579","snippets":["I + note that the rust forge displays the current stable, beta and nightly + versions. That seems to be set by an mdbook preprocessor called blackjack + and I was not able to see, quickly, where blackjack gets its data from. + Also, the stable version given there contains only the major and minor + parts of the semantic version, excluding the patch number. The GitHub + API provides the latest Rust release, however that does not correspond + to the current stable version.","Is it possible to find out the latest + stable Rust version from inside a web page? E.g. by getting some JSON + or a text file from somewhere? I note that the rust forge displays the + current stable, b...","It is not even the newest Rust. At the time of + writing, the stable Rust is 1.72 but the latest GitHub release is 1.71.1. + ... Another avenue of enquiry might be to look at how rustup determines + the latest versions.","Funny that there doesn''t seem to be a rustup command + that just emits the latest stable version. rustup check | grep stable + is the nearest I can see. But as long as that matches the url parsing + method I can check that the output is correct."],"thumbnail_url":"https://stackoverflow.com/Content/Sites/stackoverflow/Img/apple-touch-icon@2.png?v=0f0cab681579","title":"Determine + current stable rust version - Stack Overflow","url":"https://stackoverflow.com/questions/77156044/determine-current-stable-rust-version"},{"description":"It + builds your Rust crate and checks ... Rust version. For this, it needs + to have an idea about the toolchains which have been released, and can + be installed. Originally we simply parsed the latest channel manifest, + and then decreased the minor semver version, but this was obviously not + great for many reasons, including: Except for the latest released version, + we are left guessing the decreased version numbers actually exist Β· Only + stable versions were ...","favicon_url":"https://you.com/favicon?domain=crates.io&size=128","original_thumbnail_url":"https://static.crates.io/og-images/rust-releases.png","page_age":"2026-05-07T00:00:00","snippets":["It + builds your Rust crate and checks whether the build succeeds or fails, + as this gives the most complete idea whether a version will work with + your (external) dependencies. cargo-msrv uses bisection, or a reverse-linear + search, to find the lowest appropriate Rust version. For this, it needs + to have an idea about the toolchains which have been released, and can + be installed. Originally we simply parsed the latest channel manifest, + and then decreased the minor semver version, but this was obviously not + great for many reasons, including: Except for the latest released version, + we are left guessing the decreased version numbers actually exist Β· Only + stable versions were supported, not nightly, beta, or other channels","Find, + index and obtain all officially available Rust releases","crates.io serves + as a central registry for sharing crates, which are packages or libraries + written in Rust that you can use to enhance your projects","Each data + source implements the Source trait. Source provides a build_index method, + which can be used to build a catalog of released Rust versions. 1: Currently + most of the rust-releases public API supports only stable. Support for + the beta and nightly channel is work-in-progress, and the table currently + lists whether there is theoretical support for these channels."],"thumbnail_url":"https://static.crates.io/og-images/rust-releases.png","title":"rust-releases + - crates.io: Rust Package Registry","url":"https://crates.io/crates/rust-releases"},{"description":"If + you''re using stable, you can look at the Rust release history to get + a release version number to use.","favicon_url":"https://you.com/favicon?domain=crates.io&size=128","original_thumbnail_url":"https://static.crates.io/og-images/rust-latest.png","snippets":["A + CLI tool to determine the latest version of the Rust toolchain. For nightly, + it finds the latest version to include builds of all the builtin components + (clippy, rustfmt, rls, etc.). $ rust-latest 1.89.0 $ rust-latest --force-date + stable-2025-08-07 $ rust-latest -c nightly nightly-2025-09-07","[default: + all] [possible values: all, current] -d, --force-date Whether date-stamped + toolchains like stable-2019-04-25 should be used instead of version numbers + for stable releases. -h, --help Print help Β· When you run the tool, it + will output a toolchain name that can be used in your rust-toolchain file:","The + first step to deciding what you should put in your rust-toolchain file + is to decide what release channel you want to use. This decision is outside + the scope of this tool, but you can read up on the differences between + stable, beta, and nightly here and here. The next step, which this tool + solves for you, is to decide which release in that channel to use. When + you don''t have a particular preference for a version, or just want the + latest version, how do determine the actual label?","crates.io serves + as a central registry for sharing crates, which are packages or libraries + written in Rust that you can use to enhance your projects"],"thumbnail_url":"https://static.crates.io/og-images/rust-latest.png","title":"rust-latest + - crates.io: Rust Package Registry","url":"https://crates.io/crates/rust-latest"},{"description":"Empowering + everyone to build reliable and efficient software. - rust-lang/rust","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/48c59e4644f44114fd3ccc60b5e6d6f6d6f94f2f994d8d230822965280321e16/rust-lang/rust","snippets":["Empowering + everyone to build reliable and efficient software. - rust/RELEASES.md + at main Β· rust-lang/rust"],"thumbnail_url":"https://opengraph.githubassets.com/48c59e4644f44114fd3ccc60b5e6d6f6d6f94f2f994d8d230822965280321e16/rust-lang/rust","title":"rust/RELEASES.md + at main Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/blob/main/RELEASES.md"},{"description":"Adding + support for the Rust language to the Linux kernel","favicon_url":"https://you.com/favicon?domain=rust-for-linux.com&size=128","original_thumbnail_url":"https://rust-for-linux.com/Rust-for-Linux.svg","snippets":["This + means that, apart from intentional changes on their side (that we work + around conditionally on our side as needed), the upcoming Rust and bindgen + versions should generally work. This applies to beta and nightly versions + of Rust as well. In addition, getting Linux to build on stable Rust was + a \"flagship goal\" of the Rust project for 2024H2 and 2025H1.","To clarify, + the Rust language is stable, i.e. it promises backwards compatibility, + except for those unstable features. The Rust versions currently supported + should already be enough for kernel developers in recent distributions, + such as:","In addition, 24.04 LTS using versioned packages. In addition, + we support the toolchains distributed by Rust, installed via rustup or + the standalone installers. Finally, slim and fast LLVM+Rust toolchains + are provided at kernel.org. Please see the Quick Start guide for details. + Since early 2026, our current policy is to follow Debian Stable''s Rust + version as the minimum supported version.","The initial minimum was Rust + 1.78.0, and the next one will be Rust 1.85.0 (expected in Linux v7.1)1, + following Debian 13 (Trixie)''s Rust toolchain version β€” please see \"Minimum + upgrade policy\" below for details."],"thumbnail_url":"https://rust-for-linux.com/Rust-for-Linux.svg","title":"Rust + version policy - Rust for Linux","url":"https://rust-for-linux.com/rust-version-policy"}]}}' + tool_use_id: chatcmpl-tool-86f4e1d8180872c1 + type: tool_result + role: user + model: qwen3 + stream: false + tools: + - description: Search the web for current information. + input_schema: + properties: + query: + type: string + required: + - query + type: object + name: web_search + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + method: POST + path: /v1/messages + query_params: {} + response: + body: + content: + - signature: 522c39688b914d2e9245e471279bf038 + thinking: "\nOkay, let's see. The user wants me to first find the latest stable\ + \ Rust version number using a web search. The initial search for \"latest\ + \ stable Rust version number\" returned some results. Looking through the\ + \ web results, there's a link to \"Rust Versions | Rust Changelogs\" which\ + \ mentions \"Stable: 1.95.0 Beta: 1.96.0 (28 May, 2026, 8 days left) Nightly:\ + \ 1.97.0 (9 July, 2026, 50 days left)\". So the stable version here is 1.95.0.\n\ + \nBut wait, another result is \"Announcing Rust 1.97.0\" from the blog.rust-lang.org.\ + \ That might be a newer version. However, the date on that result is from\ + \ July 9, 2026, but the current date in the response is July 16, 2026. If\ + \ the blog post is from July 9, maybe 1.97.0 is the latest. But the first\ + \ result mentions 1.95.0 as stable. There's a conflict here. \n\nI need\ + \ to check which one is correct. The \"Rust Versions\" page might be more\ + \ reliable as it's a dedicated page for versions. However, the blog post\ + \ announcing 1.97.0 could indicate that it's the latest. But the date on\ + \ the blog post is July 9, 2026, and the current date in the tool response\ + \ is July 16. If the blog post is recent, maybe 1.97.0 is the stable version.\ + \ However, the \"Rust Versions\" page shows 1.95.0 as stable, with 1.96.0\ + \ as beta. \n\nWait, the \"Rust Versions\" page's entry for stable is 1.95.0,\ + \ and the beta is 1.96.0, which was released on May 28, 2026. The blog post\ + \ for 1.97.0 is from July 9, 2026. That suggests that 1.97.0 might be the\ + \ next version, but if it's a beta or nightly, not stable. The user is asking\ + \ for the latest stable version. So maybe 1.95.0 is still the latest stable,\ + \ and 1.97.0 is a future version. \n\nAlternatively, maybe the blog post\ + \ is announcing the release of 1.97.0 as stable. But the way it's written,\ + \ \"Announcing Rust 1.97.0\" might be a future release. The dates on the\ + \ results might be from 2026, but the actual current date in the system\ + \ is July 16, 2026. If the blog post is from July 9, then 1.97.0 could be\ + \ the latest stable. However, the \"Rust Versions\" page might be more up-to-date.\ + \ \n\nI think the correct approach is to take the stable version from the\ + \ \"Rust Versions\" page, which is 1.95.0. But I need to confirm. Another\ + \ result is from the GitHub releases page, which mentions \"The 2024 Edition\ + \ is now stable.\" But that's about the edition, not the version number.\ + \ \n\nLooking at the \"Rust Release Notes\" link, it's from May 28, 2026,\ + \ and mentions \"Use lld by default on x86_64-unknown-linux-gnu.\" But that's\ + \ probably part of an older version. \n\nGiven the conflicting information,\ + \ I'll go with the \"Rust Versions\" page which states Stable: 1.95.0. So\ + \ the latest stable version is 1.95.0. Then, the next step is to perform\ + \ a second web search for the release notes of exactly that version, 1.95.0.\n" + type: thinking + - id: chatcmpl-tool-9b9c030fa0156afb + input: + query: Rust 1.95.0 release notes + name: web_search + type: tool_use + id: chatcmpl-bbe657db5b8d25d2 + model: qwen3 + role: assistant + stop_reason: tool_use + type: message + usage: + input_tokens: 6479 + output_tokens: 880 + headers: + content-type: application/json + status_code: 200 +- filename: t3 + request: + body: + max_tokens: 1024 + messages: + - content: First call web_search to find the latest stable Rust version number. + Then, in a SEPARATE second web_search call, search for the release notes + of exactly that version. Only then answer. Make the searches one at a time, + not together. + role: user + - content: + - id: chatcmpl-tool-86f4e1d8180872c1 + input: + query: latest stable Rust version number + name: web_search + type: tool_use + role: assistant + - content: + - content: '{"metadata":{"latency":0.633882999420166,"query":"latest stable + Rust version number","search_uuid":"1dc90bce-1380-424b-86a3-9767d2325102"},"query":"latest + stable Rust version number","results":{"news":[{"description":"C won''t + be disappearing tomorrow, says the stable kernel maintainer, but the future + of Linux belongs to Rust.","page_age":"2026-07-15T16:13:42","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/2bdc8a3e1869eab0452a0fac6e599caba4c8496223475dd9e0a193f7b3fd8d98/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fzdnet_articles_730%2F5ea4c915b08f4417f23ec9143f381180","title":"''Rust + makes coding fun again'': Why Linux is moving away from C, according to + Greg Kroah-Hartman","url":"https://tech.yahoo.com/computing/articles/rust-makes-coding-fun-again-161342409.html"},{"description":"Xiaomi + just rolled out the stable Android 17 update to the Xiaomi 17 Ultra and + Xiaomi 17. The only issue is that it''s still based on HyperOS 3.0, so + you won''t see any new features.","page_age":"2026-07-16T07:45:51","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/ad2d7ffb2de57f87b99027ef35c0ab6878da2a854f5421ea2d052f32ea2bb61b/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fandroid_central_347%2F339a43e3965ba3504f0886c44b62e9e0","title":"Xiaomi + somehow rolls out stable Android 17 update ahead of its rivals","url":"https://tech.yahoo.com/phones/articles/xiaomi-somehow-rolls-stable-android-074551979.html"},{"description":"A + man is in critical but stable condition but stable condition after being + stabbed in Pittsburgh on Wednesday.","page_age":"2026-07-16T03:45:23","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/0bb0be4cf36a2674d69f0bdcf2444762fa66c60cbafae5bcfa867d24395a2daa/lightyear_networkapi/resizefill_w300_h169;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fwpxi_cox_articles_540%2F6faa5b7a77d8ef1d57ab56e66606ad77","title":"Man + in critical, but stable condition after stabbing in Pittsburgh","url":"https://www.yahoo.com/news/articles/man-critical-stable-condition-stabbing-034523965.html"},{"description":"Redis + CVE-2026-23479 enables authenticated RCE; affecting versions since 7.2.0, + patched May 5 to reduce exploitation risk.","page_age":"2026-06-03T13:58:10","thumbnail_url":"https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjTV6zPqD9KC3Rc5Mz9c8XENLiJntboDT6LIoD3L2FXlTUVC3rsWZ_3YLfe_jmhhyphenhyphenjb5RCwkTsdoyypD9VXxYgj_2GYaAupNMlOyZkycm94qr7XiarmBCpYQdZDN_Gwq6KrOmomzx1dmbz4hpUkw4OZparqEbydJneNKaWQI0fcP5tctpKxsZ7kFP5FCv0/s1700-e365/redis-db.png","title":"Autonomous + AI Tool Finds 2-Year-Old RCE Flaw in Redis (CVE-2026-23479)","url":"https://thehackernews.com/2026/06/autonomous-ai-tool-finds-2-year-old-rce.html"},{"description":"The + first public beta release of iOS 27 has just hit Apple devices, and here + are five cool new features you can take for a spin after installing the + update.","page_age":"2026-07-13T21:13:28","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/2f32549c6a571c5c328364097e74a1e957c1d51d2ddaffa38952f2c2be39d5be/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fbgr_ecomm_183%2F8da9cbfa864cc31686c78e6ba081daae","title":"5 + Cool New iOS 27 Features To Try In The First Public Beta","url":"https://tech.yahoo.com/ai/apple-intelligence/articles/5-cool-ios-27-features-211328715.html"},{"description":"WASHINGTON, + July 16 (Reuters) - The number of Americans filing claims for unemployment + benefits fell last week, pointing to continued labor market stability. + Initial claims for state unemployment benefits dropped 8,000 to a seasonally + adjusted 208,0...","page_age":"2026-07-16T07:50:00","thumbnail_url":"https://storage.googleapis.com/media.mwcradio.com/mimesis/2026-07/16/2026-07-16T125029Z_1_LYNXMPEM6F0U7_RTROPTP_3_USA-ECONOMY.JPG","title":"US + weekly jobless claims fall; labor market remains stable","url":"https://kelo.com/2026/07/16/us-weekly-jobless-claims-fall-labor-market-remains-stable/"},{"description":"American + workers'' share of the nation''s income is at its lowest point in almost + 80 years, as more of the economy''s gains flow to corporations and investors.","page_age":"2026-06-30T09:00:04","thumbnail_url":"https://assets3.cbsnewsstatic.com/hub/i/r/2026/06/26/03953273-3a9e-4da1-ab74-21b0b58858ea/thumbnail/1200x630/dacc2f73d9272aaa59c7545f89296ae9/gettyimages-2272349966.jpg","title":"This + number helps explain why many Americans are down on the economy","url":"https://www.cbsnews.com/news/labor-share-income-lowest-since-world-war-ii/"},{"description":"Mighty + Morphin Power Rangers #2 Reveals Its Heart, But Is Held Back From Its + Potential (Review)","page_age":"2026-07-16T01:01:33","thumbnail_url":"https://comicbook.com/wp-content/uploads/sites/4/2026/07/Mighty-Morphin-Power-Rangers-2.jpg?resize=2000,1125","title":"Mighty + Morphin Power Rangers #2 Reveals Its Heart, But Is Held Back From Its + Potential (Review)","url":"https://comicbook.com/comics/review/mighty-morphin-power-rangers-2-reveals-its-heart-but-is-held-back-from-its-potential-review/"},{"description":"Netflix + rolls out a ''VHS'' special edition of Stranger Things season 1 and says + more seasons could follow if enough people watch it.","page_age":"2026-07-16T09:19:06","thumbnail_url":"https://s.yimg.com/lo/mysterio/api/0e792cc17e526c0c69d3fc7b967467181e541068d40429d69cccad13e0aeaeda/lightyear_networkapi/resizefill_w1200;quality_80;format_webp/https:%2F%2Fmedia.zenfs.com%2Fen%2Fengadget_100%2F7d5d7d5f5c045134cc3f8adee2347188","title":"Netflix + Releases A VHS-Quality Version Of Stranger Things'' First Season","url":"https://www.yahoo.com/entertainment/tv/articles/netflix-releases-vhs-quality-version-091906497.html"},{"description":"Jaylen + Waddle is expected to be an immediate success for the Denver Broncos in + 2026.","page_age":"2026-07-03T20:28:06","thumbnail_url":"https://s.yimg.com/os/en/the_sporting_news_articles_584/3e3d140840b708f27813c2e1ddacf889","title":"Broncos'' + newest offensive weapon predicted to hit massive numbers in 2026","url":"https://sports.yahoo.com/articles/broncos-newest-offensive-weapon-predicted-202806292.html"}],"web":[{"description":"Rust + Versions # Stable: 1.95.0 Beta: 1.96.0 (28 May, 2026, 8 days left) Nightly: + 1.97.0 (9 July, 2026, 50 days left) Ongoing Stabilization PRs # Stabilize + core::range::{legacy, RangeFull, RangeTo} (4 days old) T-compiler - Relevant + to the compiler team, which will review and decide on the PR/issue.","favicon_url":"https://you.com/favicon?domain=releases.rs&size=128","page_age":"2026-06-02T03:45:04","snippets":["T-rustdoc + - Relevant to the rustdoc team, which will review and decide on the PR/issue. + ... T-compiler - Relevant to the compiler team, which will review and + decide on the PR/issue. S-blocked - Status: Blocked on something else + such as an RFC or other implementation work. needs-fcp - This change is + insta-stable, or significant enough to need a team FCP to proceed.","T-rustdoc + - Relevant to the rustdoc team, which will review and decide on the PR/issue. + S-waiting-on-review - Status: Awaiting review from the assignee but also + interested parties. proposed-final-comment-period - Proposed to merge/close + by relevant subteam, see T- ... T-libs-api - Relevant to the library API + team, which will review and decide on the PR/issue. needs-fcp - This change + is insta-stable, or significant enough to need a team FCP to proceed.","T-rustdoc + - Relevant to the rustdoc team, which will review and decide on the PR/issue. + T-compiler - Relevant to the compiler team, which will review and decide + on the PR/issue. S-waiting-on-review - Status: Awaiting review from the + assignee but also interested parties. S-blocked - Status: Blocked on something + else such as an RFC or other implementation work. needs-fcp - This change + is insta-stable, or significant enough to need a team FCP to proceed.","needs-fcp + - This change is insta-stable, or significant enough to need a team FCP + to proceed. A-rust-for-linux - Relevant for the Rust-for-Linux project"],"title":"Rust + Versions | Rust Changelogs","url":"https://releases.rs/"},{"description":"Click + here to be redirected to the latest Rust release announcement","favicon_url":"https://you.com/favicon?domain=blog.rust-lang.org&size=128","page_age":"2026-07-09T05:20:01","snippets":["Click + here to be redirected to the latest Rust release announcement."],"title":"Announcing + Rust 1.97.0","url":"https://blog.rust-lang.org/releases/latest/"},{"description":"Empowering + everyone to build reliable and efficient software. - Releases Β· rust-lang/rust","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/965c2fcedff6dbee97c6e81ff9e86ca7d93ba06b36c08dd84d449d801ca3195b/rust-lang/rust","snippets":["Empowering + everyone to build reliable and efficient software. - rust-lang/rust","Allow + a dependency to specify both a git repository and an alternate registry. + Just like with crates.io, the git repository will be used locally, but + the registry version will be used when published. Added target.''cfg(..)''.rustdocflags + support in configuration.","Refer to Rust''s platform support page for + more information on Rust''s tiered platform support. Relax T: Ord bound + for some BinaryHeap methods. ... Stabilize the config include key. The + top-level include config key allows loading additional config files, enabling + better organization, sharing, and management of Cargo configurations across + projects and environments. docs #16284 Β· Stabilize the pubtime field in + registry index. This records when a crate version was published and enables + time-based dependency resolution in the future.","Putting the #[test] + attribute in these places is no longer ignored, and will now result in + an error; this may also result in errors when generating rustdoc. Error + when test attribute is applied to structs Β· Cargo now sets the CARGO_CFG_DEBUG_ASSERTIONS + environment variable in more situations. This will cause crates depending + on static-init versions 1.0.1 to 1.0.3 to fail compilation with \"failed + to resolve: use of unresolved module or unlinked crate parking_lot\"."],"thumbnail_url":"https://opengraph.githubassets.com/965c2fcedff6dbee97c6e81ff9e86ca7d93ba06b36c08dd84d449d801ca3195b/rust-lang/rust","title":"Releases + Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/releases"},{"description":"Fix + errors in std::fs::rename on Windows 10, version 1607. Downgrade bootstrap + cc to fix custom targets. Skip submodule updates when building Rust from + a source tarball. The 2024 Edition is now stable.","favicon_url":"https://you.com/favicon?domain=doc.rust-lang.org&size=128","page_age":"2026-05-28T00:00:00","snippets":["powerpc64: + Use the ELF ABI version set in target spec instead of guessing (fixes + the ELF ABI used by the OpenBSD target) Matching on a #[non_exhaustive] + enum now reads the discriminant, even if the enum has only one variant. + This can cause closures to capture values that they previously wouldn’t. + mut ref and mut ref mut patterns, part of the unstable Match Ergonomics + 2024 RFC, were accidentally allowed on stable within struct pattern field + shorthand.","Previously, they could not be used without the standard library, + which has no stable build mechanism. In preparation for the build-std + project adding that support, JSON target specs are being proactively gated + to ensure they remain unstable even if build-std is stabilized.","The + unstable -Zprofile flag (β€œgcov-style” coverage instrumentation) has been + removed. This does not affect the stable flags for coverage instrumentation + (-Cinstrument-coverage) and profile-guided optimization (-Cprofile-generate, + -Cprofile-use), which are unrelated and remain available.","Use lld by + default on x86_64-unknown-linux-gnu. See also https://blog.rust-lang.org/2025/09/01/rust-lld-on-1.90.0-stable/."],"title":"Rust + Release Notes","url":"https://doc.rust-lang.org/beta/releases.html"},{"description":"Check + end-of-life, release policy and support schedule for Rust.","favicon_url":"https://you.com/favicon?domain=endoflife.date&size=128","original_thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","page_age":"2026-07-11T05:20:01","snippets":["Rust + is an open source multi-paradigm, general-purpose programming language + that emphasizes performance, type safety, and concurrency. It enforces + memory safety, meaning that all references point to valid memory, without + requiring the use of automated memory management techniques such as garbage + collection Β· A new stable release is produced every six weeks. Only the + latest stable release receives patches.","Editions are skin-deep, and + as a result - you can always use an older edition alongside a supported + version of the compiler. Editions are expected to be released every three + years. ... Rust 2015, the default edition from Rust 1.0. Rust 2018, which + added path and module system changes and was released alongside 1.31 in + December 2018. Rust 2021, released alongside 1.56 in October 2021. Rust + 2024, released alongside 1.85 in February 2025. More information is available + on the Rust website. You should be running one of the supported release + numbers listed above in the rightmost column."],"thumbnail_url":"https://endoflife.date/assets/logo-512x512.png","title":"Rust + | endoflife.date","url":"https://endoflife.date/rust"},{"description":"I + note that the rust forge displays the current stable, beta and nightly + versions. That seems to be set by an mdbook preprocessor called blackjack + and I was not able to see, quickly, where blackjack gets its data from. + Also, the stable version given there contains only the major and minor + parts of the semantic version, excluding the patch number. The GitHub + API provides the latest ...","favicon_url":"https://you.com/favicon?domain=stackoverflow.com&size=128","original_thumbnail_url":"https://stackoverflow.com/Content/Sites/stackoverflow/Img/apple-touch-icon@2.png?v=0f0cab681579","snippets":["I + note that the rust forge displays the current stable, beta and nightly + versions. That seems to be set by an mdbook preprocessor called blackjack + and I was not able to see, quickly, where blackjack gets its data from. + Also, the stable version given there contains only the major and minor + parts of the semantic version, excluding the patch number. The GitHub + API provides the latest Rust release, however that does not correspond + to the current stable version.","Is it possible to find out the latest + stable Rust version from inside a web page? E.g. by getting some JSON + or a text file from somewhere? I note that the rust forge displays the + current stable, b...","It is not even the newest Rust. At the time of + writing, the stable Rust is 1.72 but the latest GitHub release is 1.71.1. + ... Another avenue of enquiry might be to look at how rustup determines + the latest versions.","Funny that there doesn''t seem to be a rustup command + that just emits the latest stable version. rustup check | grep stable + is the nearest I can see. But as long as that matches the url parsing + method I can check that the output is correct."],"thumbnail_url":"https://stackoverflow.com/Content/Sites/stackoverflow/Img/apple-touch-icon@2.png?v=0f0cab681579","title":"Determine + current stable rust version - Stack Overflow","url":"https://stackoverflow.com/questions/77156044/determine-current-stable-rust-version"},{"description":"It + builds your Rust crate and checks ... Rust version. For this, it needs + to have an idea about the toolchains which have been released, and can + be installed. Originally we simply parsed the latest channel manifest, + and then decreased the minor semver version, but this was obviously not + great for many reasons, including: Except for the latest released version, + we are left guessing the decreased version numbers actually exist Β· Only + stable versions were ...","favicon_url":"https://you.com/favicon?domain=crates.io&size=128","original_thumbnail_url":"https://static.crates.io/og-images/rust-releases.png","page_age":"2026-05-07T00:00:00","snippets":["It + builds your Rust crate and checks whether the build succeeds or fails, + as this gives the most complete idea whether a version will work with + your (external) dependencies. cargo-msrv uses bisection, or a reverse-linear + search, to find the lowest appropriate Rust version. For this, it needs + to have an idea about the toolchains which have been released, and can + be installed. Originally we simply parsed the latest channel manifest, + and then decreased the minor semver version, but this was obviously not + great for many reasons, including: Except for the latest released version, + we are left guessing the decreased version numbers actually exist Β· Only + stable versions were supported, not nightly, beta, or other channels","Find, + index and obtain all officially available Rust releases","crates.io serves + as a central registry for sharing crates, which are packages or libraries + written in Rust that you can use to enhance your projects","Each data + source implements the Source trait. Source provides a build_index method, + which can be used to build a catalog of released Rust versions. 1: Currently + most of the rust-releases public API supports only stable. Support for + the beta and nightly channel is work-in-progress, and the table currently + lists whether there is theoretical support for these channels."],"thumbnail_url":"https://static.crates.io/og-images/rust-releases.png","title":"rust-releases + - crates.io: Rust Package Registry","url":"https://crates.io/crates/rust-releases"},{"description":"If + you''re using stable, you can look at the Rust release history to get + a release version number to use.","favicon_url":"https://you.com/favicon?domain=crates.io&size=128","original_thumbnail_url":"https://static.crates.io/og-images/rust-latest.png","snippets":["A + CLI tool to determine the latest version of the Rust toolchain. For nightly, + it finds the latest version to include builds of all the builtin components + (clippy, rustfmt, rls, etc.). $ rust-latest 1.89.0 $ rust-latest --force-date + stable-2025-08-07 $ rust-latest -c nightly nightly-2025-09-07","[default: + all] [possible values: all, current] -d, --force-date Whether date-stamped + toolchains like stable-2019-04-25 should be used instead of version numbers + for stable releases. -h, --help Print help Β· When you run the tool, it + will output a toolchain name that can be used in your rust-toolchain file:","The + first step to deciding what you should put in your rust-toolchain file + is to decide what release channel you want to use. This decision is outside + the scope of this tool, but you can read up on the differences between + stable, beta, and nightly here and here. The next step, which this tool + solves for you, is to decide which release in that channel to use. When + you don''t have a particular preference for a version, or just want the + latest version, how do determine the actual label?","crates.io serves + as a central registry for sharing crates, which are packages or libraries + written in Rust that you can use to enhance your projects"],"thumbnail_url":"https://static.crates.io/og-images/rust-latest.png","title":"rust-latest + - crates.io: Rust Package Registry","url":"https://crates.io/crates/rust-latest"},{"description":"Empowering + everyone to build reliable and efficient software. - rust-lang/rust","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/48c59e4644f44114fd3ccc60b5e6d6f6d6f94f2f994d8d230822965280321e16/rust-lang/rust","snippets":["Empowering + everyone to build reliable and efficient software. - rust/RELEASES.md + at main Β· rust-lang/rust"],"thumbnail_url":"https://opengraph.githubassets.com/48c59e4644f44114fd3ccc60b5e6d6f6d6f94f2f994d8d230822965280321e16/rust-lang/rust","title":"rust/RELEASES.md + at main Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/blob/main/RELEASES.md"},{"description":"Adding + support for the Rust language to the Linux kernel","favicon_url":"https://you.com/favicon?domain=rust-for-linux.com&size=128","original_thumbnail_url":"https://rust-for-linux.com/Rust-for-Linux.svg","snippets":["This + means that, apart from intentional changes on their side (that we work + around conditionally on our side as needed), the upcoming Rust and bindgen + versions should generally work. This applies to beta and nightly versions + of Rust as well. In addition, getting Linux to build on stable Rust was + a \"flagship goal\" of the Rust project for 2024H2 and 2025H1.","To clarify, + the Rust language is stable, i.e. it promises backwards compatibility, + except for those unstable features. The Rust versions currently supported + should already be enough for kernel developers in recent distributions, + such as:","In addition, 24.04 LTS using versioned packages. In addition, + we support the toolchains distributed by Rust, installed via rustup or + the standalone installers. Finally, slim and fast LLVM+Rust toolchains + are provided at kernel.org. Please see the Quick Start guide for details. + Since early 2026, our current policy is to follow Debian Stable''s Rust + version as the minimum supported version.","The initial minimum was Rust + 1.78.0, and the next one will be Rust 1.85.0 (expected in Linux v7.1)1, + following Debian 13 (Trixie)''s Rust toolchain version β€” please see \"Minimum + upgrade policy\" below for details."],"thumbnail_url":"https://rust-for-linux.com/Rust-for-Linux.svg","title":"Rust + version policy - Rust for Linux","url":"https://rust-for-linux.com/rust-version-policy"}]}}' + tool_use_id: chatcmpl-tool-86f4e1d8180872c1 + type: tool_result + role: user + - content: + - id: chatcmpl-tool-9b9c030fa0156afb + input: + query: Rust 1.95.0 release notes + name: web_search + type: tool_use + role: assistant + - content: + - content: '{"metadata":{"latency":0.5060904026031494,"query":"Rust 1.95.0 + release notes","search_uuid":"0cf0ef2b-1fa0-4b01-b76c-d3d8ceb9a958"},"query":"Rust + 1.95.0 release notes","results":{"web":[{"description":"1.95.0 # Released + on: 16 April, 2026 Branched from master on: 27 February, 2026 Language + # Stabilize if let guards on match arms irrefutable_let_patterns lint + no longer lints on let chains Support importing path-segment keywords + with renaming Stabilize inline assembly for PowerPC and PowerPC64 ...","favicon_url":"https://you.com/favicon?domain=releases.rs&size=128","snippets":["1.95.0 + # Released on: 16 April, 2026 Branched from master on: 27 February, 2026 + Language # Stabilize if let guards on match arms irrefutable_let_patterns + lint no longer lints on let chains Support importing path-segment keywords + with renaming Stabilize inline assembly for PowerPC and PowerPC64 const-eval: + be more consistent in the behavior of padding during typed copies Const + blocks are no longer evaluated to determine if expressions involving fallible + operations can implicitly be constant-promoted..","Refer to Rust’s platform + support page for more information on Rust’s tiered platform support.","In + preparation for the build-std project adding that support, JSON target + specs are being proactively gated to ensure they remain unstable even + if build-std is stabilized. Cargo now includes the -Z json-target-spec + CLI flag to automatically pass -Z unstable-options to the compiler when + needed. See #150151, #151534, and rust-lang/cargo#16557.","These changes + do not affect any public interfaces of Rust, but they represent significant + improvements to the performance or internals of rustc and related tools."],"title":"1.95.0 + | Rust Changelogs","url":"https://releases.rs/docs/1.95.0/"},{"description":"Version + 1.95.0 of the Rust language has been released. Changes include the addition + of a cfg_select! macro, the capability to use if let guards to allow conditionals + based on pattern matching, and many newly stabilized APIs. See the release + notes for a full list of changes.","favicon_url":"https://you.com/favicon?domain=lwn.net&size=128","page_age":"2026-04-16T00:00:00","snippets":["Version + 1.95.0 of the Rust language has been released. Changes include the addition + of a cfg_se [...]"],"title":"Rust 1.95.0 released [LWN.net]","url":"https://lwn.net/Articles/1068011/"},{"description":"Empowering + everyone to build reliable and efficient software. - Release Rust 1.95.0 + Β· rust-lang/rust","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/0421ae520f72cbf27d0e35b96a87deeb7e397cd591784e8f645cf504b233a81f/rust-lang/rust/releases/tag/1.95.0","snippets":["Language + Stabilize if let guards on match arms irrefutable_let_patterns lint no + longer lints on let chains Support importing path-segment keywords with + renaming Stabilize inline assembly for Power...","rustbot released this + Β· 16 Apr 18:19 Β· Β· 7845 commits to main since this release Β· 1.95.0 Β· + 5980761 Β· Stabilize if let guards on match arms Β· irrefutable_let_patterns + lint no longer lints on let chains Β· Support importing path-segment keywords + with renaming Β·","Refer to Rust''s platform support page for more information + on Rust''s tiered platform support.","In preparation for the build-std + project adding that support, JSON target specs are being proactively gated + to ensure they remain unstable even if build-std is stabilized. Cargo + now includes the -Z json-target-spec CLI flag to automatically pass -Z + unstable-options to the compiler when needed. See #150151, #151534, and + rust-lang/cargo#16557."],"thumbnail_url":"https://opengraph.githubassets.com/0421ae520f72cbf27d0e35b96a87deeb7e397cd591784e8f645cf504b233a81f/rust-lang/rust/releases/tag/1.95.0","title":"Release + Rust 1.95.0 Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/releases/tag/1.95.0"},{"description":"If + you have a previous version of Rust installed via rustup, you can get + 1.95.0 with: ... If you don''t have it already, you can get rustup from + the appropriate page on our website, and check out the detailed release + notes for 1.95.0.","favicon_url":"https://you.com/favicon?domain=blog.rust-lang.org&size=128","original_thumbnail_url":"https://www.rust-lang.org/static/images/rust-social-wide.jpg","page_age":"2026-04-16T00:00:00","snippets":["Empowering + everyone to build reliable and efficient software.","Apr. 16, 2026 Β· The + Rust Release Team Β· The Rust team is happy to announce a new version of + Rust, 1.95.0.","Many people came together to create Rust 1.95.0. We couldn''t + have done it without all of you.","Rust 1.95 brings that capability into + match expressions, allowing for conditionals based on pattern matching. + match value { Some(x) if let Ok(y) = compute(x) => { // Both `x` and `y` + are available here println!(\"{}, {}\", x, y); } _ => {} } Note that the + compiler will not currently consider the patterns matched in if let guards + as part of the exhaustiveness evaluation of the overall match, just like + if guards."],"thumbnail_url":"https://www.rust-lang.org/static/images/rust-social-wide.jpg","title":"Announcing + Rust 1.95.0 | Rust Blog","url":"https://blog.rust-lang.org/2026/04/16/Rust-1.95.0/"},{"description":"The + 1.95.0 pre-release is ready for testing. The release is scheduled for + April 16. Release notes can be found here. You can try it out locally + by running: RUSTUP_DIST_SERVER=https://dev-static.rust-lang.org rustup + update stable The index is https://dev-static.rust-lang.org/dist/2026-04-13...","favicon_url":"https://you.com/favicon?domain=internals.rust-lang.org&size=128","original_thumbnail_url":"https://us1.discourse-cdn.com/flex002/uploads/rustlang/original/2X/b/b71959a2097ce0b5c3a193ce0a6466a42d4c4952.png","page_age":"2026-04-13T13:18:49","snippets":["The + 1.95.0 pre-release is ready for testing. The release is scheduled for + April 16. Release notes can be found here. You can try it out locally + by running: RUSTUP_DIST_SERVER=https://dev-static.rust-lang.org rustup + update stable The index is https://dev-static.rust-lang.org/dist/2026-04-13/index.html.","The + 1.95.0 pre-release is ready for testing. The release is scheduled for + April 16. Release notes can be found here. You can try it out locally + by running: RUSTUP_DIST_SERVER=https://dev-static.rust-lang.org rustup + upd…"],"thumbnail_url":"https://us1.discourse-cdn.com/flex002/uploads/rustlang/original/2X/b/b71959a2097ce0b5c3a193ce0a6466a42d4c4952.png","title":"Rust + 1.95.0 pre-release testing - announcements - Rust Internals","url":"https://internals.rust-lang.org/t/rust-1-95-0-pre-release-testing/24171"},{"description":"Rust + Versions # Stable: 1.95.0 Beta: 1.96.0 (28 May, 2026, 8 days left) Nightly: + 1.97.0 (9 July, 2026, 50 days left) Ongoing Stabilization PRs # Stabilize + core::range::{legacy, RangeFull, RangeTo} (4 days old) T-compiler - Relevant + to the compiler team, which will review and decide on the PR/issue.","favicon_url":"https://you.com/favicon?domain=releases.rs&size=128","page_age":"2026-06-02T03:45:04","snippets":["T-rustdoc-frontend + - Relevant to the rustdoc-frontend team, which will review and decide + on the web UI/UX output. ... T-compiler - Relevant to the compiler team, + which will review and decide on the PR/issue. relnotes - Marks issues + that should be documented in the release notes of the next release.","T-rust-analyzer + - Relevant to the rust-analyzer team, which will review and decide on + the PR/issue. P-lang-drag-1 - Lang team prioritization drag level 1.","P-lang-drag-1 + - Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang","T-rustdoc-internals + - Relevant to the rustdoc internals team, which will review and decide + on the PR/issue."],"title":"Rust Versions | Rust Changelogs","url":"https://releases.rs/"},{"description":"Click + here to be redirected to the latest Rust release announcement","favicon_url":"https://you.com/favicon?domain=blog.rust-lang.org&size=128","page_age":"2026-07-09T05:20:01","snippets":["Click + here to be redirected to the latest Rust release announcement."],"title":"Announcing + Rust 1.97.0","url":"https://blog.rust-lang.org/releases/latest/"},{"description":"Cargo + now sets the CARGO_CFG_DEBUG_ASSERTIONS environment variable in more situations. + This will cause crates depending on static-init versions 1.0.1 to 1.0.3 + to fail compilation with β€œfailed to resolve: use of unresolved module + or unlinked crate parking_lot”. See the linked issue for details.","favicon_url":"https://you.com/favicon?domain=doc.rust-lang.org&size=128","page_age":"2026-05-28T00:00:00","snippets":["Use + lld by default on x86_64-unknown-linux-gnu. See also https://blog.rust-lang.org/2025/09/01/rust-lld-on-1.90.0-stable/.","This + records when a crate version was published and enables time-based dependency + resolution in the future. Note that crates.io will gradually backfill + existing packages when a new version is published. Not all crates have + pubtime yet. #16369 #16372 Β· Cargo now parses TOML v1.1 for manifests + and configuration files.","Cargo: update tar to 0.4.45 This resolves CVE-2026-33055 + and CVE-2026-33056. Users of crates.io are not affected.","The wasm_c_abi + future compatibility warning is now a hard error. Users of wasm-bindgen + should upgrade to at least version 0.2.89, otherwise compilation will + fail."],"title":"Rust Release Notes","url":"https://doc.rust-lang.org/beta/releases.html"},{"description":"1.94.0 + # Released on: 5 March, 2026 Branched from master on: 16 January, 2026 + Language # Impls and impl items inherit dead_code lint level of the corresponding + traits and trait items Stabilize additional 29 RISC-V target features + including large portions of the RVA22U64 / RVA23U64 profiles ...","favicon_url":"https://you.com/favicon?domain=releases.rs&size=128","snippets":["1.94.0 + # Released on: 5 March, 2026 Branched from master on: 16 January, 2026 + Language # Impls and impl items inherit dead_code lint level of the corresponding + traits and trait items Stabilize additional 29 RISC-V target features + including large portions of the RVA22U64 / RVA23U64 profiles Add warn-by-default + unused_visibilities lint for visibility on const _ declarations Update + to Unicode 17 Avoid incorrect lifetime errors for closures Platform Support + # Add riscv64im-unknown-none-elf as a tier 3 target Refer to Rust’s platform + support page for more information on Rust’s tiered platform support.","Stabilize + the pubtime field in registry index. This records when a crate version + was published and enables time-based dependency resolution in the future. + Note that crates.io will gradually backfill existing packages when a new + version is published. Not all crates have pubtime yet.","Cargo now parses + TOML v1.1 for manifests and configuration files. Note that using these + features in Cargo.toml will raise your development MSRV, but the published + manifest remains compatible with older parsers.","These changes do not + affect any public interfaces of Rust, but they represent significant improvements + to the performance or internals of rustc and related tools."],"title":"1.94.0 + | Rust Changelogs","url":"https://releases.rs/docs/1.94.0/"},{"description":"NOTE: + Use the πŸ“ links to edit those that have a relnotes-tracking-issue, and + they will be updated when we regenerate the notes periodically (manually). + Version 1.95 (2026-04-16) Language Stabilize ...","favicon_url":"https://you.com/favicon?domain=github.com&size=128","original_thumbnail_url":"https://opengraph.githubassets.com/8a94087f9ea333488ea8394d4d48cbdd34bc8ba5f2786a3db3f31aadbea7eea6/rust-lang/rust/issues/154711","page_age":"2026-04-02T10:22:28","snippets":["NOTE: + Use the πŸ“ links to edit those that have a relnotes-tracking-issue, and + they will be updated when we regenerate the notes periodically (manually). + Version 1.95 (2026-04-16) Language Stabilize if let guards on match arms) + πŸ“ irrefutab...","rust-lang / rust Public Β· There was an error while loading. + Please reload this page. Notifications Β· You must be signed in to change + notification settings Β· Fork 15.2k Β· Star 114k Β· New issueCopy link Β· + New issueCopy link Β· Closed Β· Closed Β· Draft release notes for 1.95#154711 + Β·","These changes do not affect any public interfaces of Rust, but they + represent significant improvements to the performance or internals of + rustc and related tools. ... T-releaseRelevant to the release subteam, + which will review and decide on the PR/issue.Relevant to the release subteam, + which will review and decide on the PR/issue.relnotes-tracking-issueMarks + issues tracking what text to put in release notes.Marks issues tracking + what text to put in release notes.","Refer to Rust''s platform support + page for more information on Rust''s tiered platform support."],"thumbnail_url":"https://opengraph.githubassets.com/8a94087f9ea333488ea8394d4d48cbdd34bc8ba5f2786a3db3f31aadbea7eea6/rust-lang/rust/issues/154711","title":"Draft + release notes for 1.95 Β· Issue #154711 Β· rust-lang/rust","url":"https://github.com/rust-lang/rust/issues/154711"}]}}' + tool_use_id: chatcmpl-tool-9b9c030fa0156afb + type: tool_result + role: user + model: qwen3 + stream: false + tools: + - description: Search the web for current information. + input_schema: + properties: + query: + type: string + required: + - query + type: object + name: web_search + headers: + accept: '*/*' + authorization: Bearer *** + content-type: application/json + method: POST + path: /v1/messages + query_params: {} + response: + body: + content: + - signature: 951c01f2757f4f64a25255909581724a + thinking: "\nOkay, let me process what the user needs. They wanted me to first\ + \ find the latest stable Rust version using a web search. The initial search\ + \ for \"latest stable Rust version number\" returned results mentioning\ + \ Rust 1.95.0. Then, the second search for \"Rust 1.95.0 release notes\"\ + \ confirmed that 1.95.0 is indeed the latest stable version. \n\nNow, the\ + \ user's next step is to answer based on these findings. I need to make\ + \ sure that the information is accurate. The first search result from releases.rs\ + \ mentions \"Stable: 1.95.0\" and the blog post from Rust's official site\ + \ announces version 1.95.0. The release notes also confirm the details of\ + \ this version.\n\nSince the user specified to answer only after both searches\ + \ are done, I should compile the information. The latest stable Rust version\ + \ is 1.95.0, and the release notes are available at the provided links.\ + \ I should present this information clearly, mentioning the version number\ + \ and the source where the release notes can be found. I need to avoid any\ + \ markdown and keep the answer straightforward.\n" + type: thinking + - text: "\n\nThe latest stable version of Rust is **1.95.0**, as confirmed by\ + \ the release notes and official announcements. \n\n### Key Details:\n-\ + \ **Release Date**: April 16, 2026 \n- **Release Notes**: [Rust 1.95.0\ + \ Release Notes](https://blog.rust-lang.org/2026/04/16/Rust-1.95.0/) \n\ + - **Highlights**: \n - Stabilization of `if let` guards in match arms.\ + \ \n - Support for importing path-segment keywords with renaming. \n\ + \ - Inline assembly stabilization for PowerPC and PowerPC64. \n\nFor full\ + \ details, refer to the [official release announcement](https://blog.rust-lang.org/2026/04/16/Rust-1.95.0/)." + type: text + id: chatcmpl-911ae1a172c80118 + model: qwen3 + role: assistant + stop_reason: end_turn + type: message + usage: + input_tokens: 10291 + output_tokens: 427 + headers: + content-type: application/json + status_code: 200 diff --git a/crates/agentic-server-core/tests/messages_loop_test.rs b/crates/agentic-server-core/tests/messages_loop_test.rs new file mode 100644 index 0000000..8cbf1fa --- /dev/null +++ b/crates/agentic-server-core/tests/messages_loop_test.rs @@ -0,0 +1,545 @@ +//! Acceptance test for the Messages-native gateway tool loop (#115, non-streaming). +//! +//! Drives `run_messages_loop` against a mock vLLM `/v1/messages` upstream that +//! replays the recorded #123 cassette (turn 0: model emits a `web_search` +//! `tool_use`; turn 1: final text after the fed-back `tool_result`) and a mock +//! You.com search backend. Asserts the gateway tool is executed server-side, +//! hidden from the client, and only the final assistant message surfaces. +//! +//! The #123 cassette records real vLLM `/v1/messages` upstream turns β€” exactly +//! what this loop consumes β€” so replaying it is a faithful acceptance test, not +//! a hand-authored mock. + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use agentic_core::executor::{ConversationHandler, ExecutionContext, ResponseHandler, run_messages_loop}; +use agentic_core::storage::{ConversationStore, ResponseStore}; +use agentic_core::tool::{ToolRegistry, WebSearchHandler}; +use agentic_core::types::messages::{GatewayToolMap, ToolParam, registry_tools}; +use axum::extract::State; +use axum::routing::post; +use axum::{Json, Router}; +use serde_json::Value; +use tokio::net::TcpListener; +use tokio::sync::mpsc; + +mod support; + +const CASSETTE: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/cassettes/messages/messages-web-search-Qwen-Qwen3-30B-A3B-FP8-nonstreaming.yaml" +); + +/// Load the recorded assistant response bodies (one per turn) from a cassette. +fn cassette_turn_bodies() -> Vec { + cassette_bodies_at(CASSETTE) +} + +fn cassette_bodies_at(path: &str) -> Vec { + let text = std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {path}: {e}")); + let doc: Value = serde_yaml::from_str(&text).unwrap_or_else(|e| panic!("parse {path}: {e}")); + doc["turns"] + .as_array() + .expect("turns array") + .iter() + .map(|t| t["response"]["body"].clone()) + .collect() +} + +const MULTIROUND_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/cassettes/messages_multiround"); + +/// Mock vLLM `/v1/messages` β€” serves the recorded response bodies in order and +/// records each request body it received (to assert the loop fed the +/// `tool_result` back on round 2). +#[derive(Clone)] +struct UpstreamState { + bodies: Arc>, + calls: Arc, + requests: Arc>>, +} + +async fn spawn_mock_vllm_messages(bodies: Vec) -> (String, UpstreamState, tokio::task::JoinHandle<()>) { + let state = UpstreamState { + bodies: Arc::new(bodies), + calls: Arc::new(AtomicUsize::new(0)), + requests: Arc::new(tokio::sync::Mutex::new(Vec::new())), + }; + let app = Router::new() + .route( + "/v1/messages", + post(|State(st): State, Json(req): Json| async move { + let n = st.calls.fetch_add(1, Ordering::SeqCst); + st.requests.lock().await.push(req); + let body = st.bodies.get(n).cloned().unwrap_or_else(|| { + serde_json::json!({"type": "error", "error": {"type": "api_error", "message": "mock exhausted"}}) + }); + Json(body) + }), + ) + .with_state(state.clone()); + 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.ok(); + }); + (format!("http://{addr}"), state, handle) +} + +struct CapturedSearch { + body: Value, +} + +/// Mock You.com search backend the `web_search` executor calls. Uses an +/// unbounded channel so a test that doesn't drain captures (e.g. the max-rounds +/// cap, which fires ~10 searches) never blocks the handler. +async fn spawn_mock_search() -> ( + String, + mpsc::UnboundedReceiver, + tokio::task::JoinHandle<()>, +) { + let (tx, rx) = mpsc::unbounded_channel(); + let app = Router::new() + .route( + "/v1/search", + post(|State(tx): State>, Json(body): Json| async move { + let _ = tx.send(CapturedSearch { body }); + Json(serde_json::json!({ + "results": {"web": [{"url": "https://www.rust-lang.org/", "title": "Rust", + "description": "d", "snippets": ["Rust 1.89.0 is the latest stable release."]}], "news": []}, + "metadata": {"query": "q", "search_uuid": "s1", "latency": 0.1} + })) + }), + ) + .with_state(tx); + 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.ok(); + }); + (format!("http://{addr}"), rx, handle) +} + +/// Search backend that always 500s β€” drives a gateway tool dispatch failure (E5). +async fn spawn_failing_search() -> (String, mpsc::Receiver<()>, tokio::task::JoinHandle<()>) { + let (tx, rx) = mpsc::channel(8); + let app = Router::new() + .route( + "/v1/search", + post( + |State(tx): State>, _body: axum::body::Bytes| async move { + let _ = tx.send(()).await; + (http::StatusCode::INTERNAL_SERVER_ERROR, "search backend down") + }, + ), + ) + .with_state(tx); + 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.ok(); + }); + (format!("http://{addr}"), rx, handle) +} + +async fn build_exec_ctx(vllm_url: &str, search_url: &str) -> ExecutionContext { + let pool = support::setup_pool().await; + let conv = ConversationHandler::new(ConversationStore::new(Arc::clone(&pool))); + let resp = ResponseHandler::new(ResponseStore::new(pool)); + let client = Arc::new(reqwest::Client::new()); + ExecutionContext::new(conv, resp, Arc::clone(&client), vllm_url.to_owned()).with_gateway_executor(Arc::new( + WebSearchHandler::with_api_key(client, "test-key".to_owned(), search_url), + )) +} + +fn web_search_request() -> Value { + serde_json::json!({ + "model": "qwen3", + "max_tokens": 1024, + "stream": false, + "messages": [{"role": "user", "content": "What is the latest stable Rust release? Use web_search."}], + "tools": [{"name": "web_search", "description": "Search the web.", + "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}] + }) +} + +#[tokio::test] +async fn messages_loop_hides_gateway_tool_and_surfaces_final_text() { + let (vllm_url, upstream, _v) = spawn_mock_vllm_messages(cassette_turn_bodies()).await; + let (search_url, mut captured, _s) = spawn_mock_search().await; + let exec_ctx = build_exec_ctx(&vllm_url, &search_url).await; + + let request = web_search_request(); + let tools: Vec = serde_json::from_value(request["tools"].clone()).unwrap(); + let registry = ToolRegistry::build_with_handlers( + ®istry_tools(Some(&tools), &GatewayToolMap::default()), + &exec_ctx.gateway_executors, + ) + .await + .unwrap(); + + let result = run_messages_loop(request, ®istry, &exec_ctx, None) + .await + .expect("loop runs"); + + // The gateway executed the web_search server-side. + let search = captured.recv().await.expect("search backend hit"); + assert!(search.body.get("query").is_some(), "web_search dispatched with a query"); + + // Two upstream rounds, and round 2 carried the fed-back tool_result. + assert_eq!( + upstream.calls.load(Ordering::SeqCst), + 2, + "one tool round + one final round" + ); + let reqs = upstream.requests.lock().await; + let round2_msgs = reqs[1]["messages"].as_array().expect("round-2 messages"); + let has_tool_result = round2_msgs.iter().any(|m| { + m["content"] + .as_array() + .is_some_and(|blocks| blocks.iter().any(|b| b["type"] == "tool_result")) + }); + assert!(has_tool_result, "round 2 fed the tool_result back to the model"); + + // Hide-the-call: the returned message has NO tool_use block β€” only the final + // thinking/text the client should see. + let content = result["content"].as_array().expect("final content"); + assert!( + !content.iter().any(|b| b["type"] == "tool_use"), + "gateway tool_use must be hidden: {content:?}" + ); + let text: String = content + .iter() + .filter(|b| b["type"] == "text") + .map(|b| b["text"].as_str().unwrap_or_default()) + .collect(); + assert!(text.contains("1.89.0"), "final answer surfaces: {text}"); + assert_eq!(result["stop_reason"], "end_turn"); +} + +// ── Repro tests for Maral's #131 review (currently FAILING β€” proves each bug) ── + +// F3: the assistant turn fed back on the next round must preserve preceding +// thinking/text/signature blocks, not just the gateway tool_use. Dropping them +// loses conversation state and breaks extended-thinking round-tripping. +#[tokio::test] +async fn repro_f3_next_round_preserves_thinking_and_text_blocks() { + // Round 0: assistant emits thinking + text + a gateway tool_use. + let round0 = serde_json::json!({ + "id": "m", "type": "message", "role": "assistant", "model": "qwen3", + "content": [ + {"type": "thinking", "thinking": "let me search", "signature": "sig_abc"}, + {"type": "text", "text": "I'll look that up."}, + {"type": "tool_use", "id": "t1", "name": "web_search", "input": {"query": "rust"}} + ], + "stop_reason": "tool_use", "usage": {"input_tokens": 5, "output_tokens": 3} + }); + let round1 = serde_json::json!({ + "id": "m2", "type": "message", "role": "assistant", "model": "qwen3", + "content": [{"type": "text", "text": "Rust 1.89.0."}], + "stop_reason": "end_turn", "usage": {"input_tokens": 5, "output_tokens": 3} + }); + let (vllm_url, upstream, _v) = spawn_mock_vllm_messages(vec![round0, round1]).await; + let (search_url, _rx, _s) = spawn_mock_search().await; + let exec_ctx = build_exec_ctx(&vllm_url, &search_url).await; + let request = web_search_request(); + let tools: Vec = serde_json::from_value(request["tools"].clone()).unwrap(); + let registry = ToolRegistry::build_with_handlers( + ®istry_tools(Some(&tools), &GatewayToolMap::default()), + &exec_ctx.gateway_executors, + ) + .await + .unwrap(); + run_messages_loop(request, ®istry, &exec_ctx, None).await.unwrap(); + + // Inspect the assistant turn the loop appended for round 2. + let reqs = upstream.requests.lock().await; + let round2_msgs = reqs[1]["messages"].as_array().expect("round-2 messages"); + let assistant = round2_msgs + .iter() + .rev() + .find(|m| m["role"] == "assistant") + .expect("round-2 has a reconstructed assistant turn"); + let block_types: Vec<&str> = assistant["content"] + .as_array() + .unwrap() + .iter() + .filter_map(|b| b["type"].as_str()) + .collect(); + // The model's thinking + text must be carried forward alongside the tool_use. + assert!( + block_types.contains(&"thinking"), + "thinking preserved in history: {block_types:?}" + ); + assert!( + block_types.contains(&"text"), + "text preserved in history: {block_types:?}" + ); + assert!(block_types.contains(&"tool_use"), "tool_use present: {block_types:?}"); +} + +// F5: mixed gateway + client tool_use β€” the non-streaming path must NOT expose +// the gateway tool_use (hide-the-call), matching streaming. One consistent +// policy: surface the client tool_use, suppress the gateway one, stop the loop. +#[tokio::test] +async fn repro_f5_mixed_call_hides_gateway_tool_use() { + let body = serde_json::json!({ + "id": "m", "type": "message", "role": "assistant", "model": "qwen3", + "content": [ + {"type": "tool_use", "id": "g1", "name": "web_search", "input": {"query": "x"}}, + {"type": "tool_use", "id": "c1", "name": "get_weather", "input": {"city": "SF"}} + ], + "stop_reason": "tool_use", "usage": {"input_tokens": 5, "output_tokens": 3} + }); + let mut request = web_search_request(); + request["tools"] = serde_json::json!([ + {"name": "web_search", "description": "s", "input_schema": {"type": "object"}}, + {"name": "get_weather", "description": "w", "input_schema": {"type": "object"}} + ]); + let (result, _calls) = run_against(vec![body], request).await; + let names: Vec<&str> = result["content"] + .as_array() + .unwrap() + .iter() + .filter(|b| b["type"] == "tool_use") + .filter_map(|b| b["name"].as_str()) + .collect(); + assert!(names.contains(&"get_weather"), "client tool_use surfaces: {names:?}"); + assert!( + !names.contains(&"web_search"), + "gateway tool_use must be hidden: {names:?}" + ); +} + +/// Build a registry + `exec_ctx` for a canned single-turn upstream (no search +/// hit expected). +async fn run_against(bodies: Vec, request: Value) -> (Value, usize) { + let (vllm_url, upstream, _v) = spawn_mock_vllm_messages(bodies).await; + let (search_url, _rx, _s) = spawn_mock_search().await; + let exec_ctx = build_exec_ctx(&vllm_url, &search_url).await; + let tools: Vec = serde_json::from_value(request["tools"].clone()).unwrap_or_default(); + let registry = ToolRegistry::build_with_handlers( + ®istry_tools(Some(&tools), &GatewayToolMap::default()), + &exec_ctx.gateway_executors, + ) + .await + .unwrap(); + let result = run_messages_loop(request, ®istry, &exec_ctx, None).await.unwrap(); + (result, upstream.calls.load(Ordering::SeqCst)) +} + +// E2: gateway tool declared but the model answers directly (end_turn) β€” one +// round, returned as-is, no loop. +#[tokio::test] +async fn messages_loop_returns_immediately_when_model_does_not_call_tool() { + let body = serde_json::json!({ + "id": "m", "type": "message", "role": "assistant", "model": "qwen3", + "content": [{"type": "text", "text": "Rust 1.89.0."}], + "stop_reason": "end_turn", "usage": {"input_tokens": 5, "output_tokens": 3} + }); + let (result, calls) = run_against(vec![body], web_search_request()).await; + assert_eq!(calls, 1, "one round only"); + assert_eq!(result["stop_reason"], "end_turn"); + assert_eq!(result["content"][0]["text"], "Rust 1.89.0."); +} + +// E7: a client-owned tool_use in the turn must be returned to the client (the +// loop can't execute it server-side), even if a gateway tool is also declared. +#[tokio::test] +async fn messages_loop_returns_client_owned_tool_use_to_client() { + let body = serde_json::json!({ + "id": "m", "type": "message", "role": "assistant", "model": "qwen3", + "content": [{"type": "tool_use", "id": "t1", "name": "get_weather", "input": {"city": "SF"}}], + "stop_reason": "tool_use", "usage": {"input_tokens": 5, "output_tokens": 3} + }); + // Declare BOTH a gateway tool and the client tool. + let mut request = web_search_request(); + request["tools"] = serde_json::json!([ + {"name": "web_search", "description": "s", "input_schema": {"type": "object"}}, + {"name": "get_weather", "description": "w", "input_schema": {"type": "object"}} + ]); + let (result, calls) = run_against(vec![body], request).await; + assert_eq!(calls, 1, "client tool_use ends the loop in one round"); + assert_eq!(result["stop_reason"], "tool_use"); + assert_eq!( + result["content"][0]["name"], "get_weather", + "client tool_use surfaces to the client" + ); +} + +// Multi-round (3 rounds): replay the live-recorded sequential cassette +// (tool_use -> tool_use -> text). The loop must run three upstream rounds, +// hit the search backend twice, and surface only the final text. +#[tokio::test] +async fn messages_loop_multi_round_sequential() { + let bodies = cassette_bodies_at(&format!( + "{MULTIROUND_DIR}/sequential-web-search-qwen3-nonstreaming.yaml" + )); + assert_eq!(bodies.len(), 3, "cassette has 3 upstream rounds"); + let (vllm_url, upstream, _v) = spawn_mock_vllm_messages(bodies).await; + let (search_url, mut captured, _s) = spawn_mock_search().await; + let exec_ctx = build_exec_ctx(&vllm_url, &search_url).await; + let request = web_search_request(); + let tools: Vec = serde_json::from_value(request["tools"].clone()).unwrap(); + let registry = ToolRegistry::build_with_handlers( + ®istry_tools(Some(&tools), &GatewayToolMap::default()), + &exec_ctx.gateway_executors, + ) + .await + .unwrap(); + + let result = run_messages_loop(request, ®istry, &exec_ctx, None).await.unwrap(); + + assert_eq!(upstream.calls.load(Ordering::SeqCst), 3, "three upstream rounds"); + // Two gateway searches executed (rounds 0 and 1). + captured.recv().await.expect("first search"); + captured.recv().await.expect("second search"); + let content = result["content"].as_array().unwrap(); + assert!(!content.iter().any(|b| b["type"] == "tool_use"), "gateway tools hidden"); + assert_eq!(result["stop_reason"], "end_turn"); +} + +// Parallel: replay the live-recorded parallel cassette (two tool_use blocks in +// one turn). Both gateway calls execute; neither surfaces. +#[tokio::test] +async fn messages_loop_parallel_tool_use() { + let bodies = cassette_bodies_at(&format!("{MULTIROUND_DIR}/parallel-web-search-qwen3-nonstreaming.yaml")); + let (vllm_url, upstream, _v) = spawn_mock_vllm_messages(bodies).await; + let (search_url, mut captured, _s) = spawn_mock_search().await; + let exec_ctx = build_exec_ctx(&vllm_url, &search_url).await; + let request = web_search_request(); + let tools: Vec = serde_json::from_value(request["tools"].clone()).unwrap(); + let registry = ToolRegistry::build_with_handlers( + ®istry_tools(Some(&tools), &GatewayToolMap::default()), + &exec_ctx.gateway_executors, + ) + .await + .unwrap(); + + let result = run_messages_loop(request, ®istry, &exec_ctx, None).await.unwrap(); + + // Two parallel gateway calls both executed against the backend. + captured.recv().await.expect("first parallel search"); + captured.recv().await.expect("second parallel search"); + assert_eq!(upstream.calls.load(Ordering::SeqCst), 2, "tool round + final round"); + let content = result["content"].as_array().unwrap(); + assert!(!content.iter().any(|b| b["type"] == "tool_use"), "gateway tools hidden"); + // Round 2 fed BOTH tool_results back. + let reqs = upstream.requests.lock().await; + let round2 = reqs[1]["messages"].as_array().unwrap(); + let tool_results: usize = round2 + .iter() + .filter_map(|m| m["content"].as_array()) + .flatten() + .filter(|b| b["type"] == "tool_result") + .count(); + assert_eq!(tool_results, 2, "both parallel tool_results fed back"); +} + +// E5: a gateway tool that fails to dispatch (search backend returns 500) becomes +// an error tool_result fed back to the model β€” never a whole-request failure. +#[tokio::test] +async fn messages_loop_tool_failure_becomes_error_tool_result() { + // Round 0: web_search call; round 1: final text (model recovers from the error). + let round0 = serde_json::json!({ + "id": "m", "type": "message", "role": "assistant", "model": "qwen3", + "content": [{"type": "tool_use", "id": "t1", "name": "web_search", "input": {"query": "x"}}], + "stop_reason": "tool_use", "usage": {"input_tokens": 5, "output_tokens": 3} + }); + let round1 = serde_json::json!({ + "id": "m2", "type": "message", "role": "assistant", "model": "qwen3", + "content": [{"type": "text", "text": "Search failed, here's what I know."}], + "stop_reason": "end_turn", "usage": {"input_tokens": 5, "output_tokens": 3} + }); + let (vllm_url, upstream, _v) = spawn_mock_vllm_messages(vec![round0, round1]).await; + // Search backend that returns 500 β†’ dispatch error. + let (search_url, _rx, _s) = spawn_failing_search().await; + let exec_ctx = build_exec_ctx(&vllm_url, &search_url).await; + let request = web_search_request(); + let tools: Vec = serde_json::from_value(request["tools"].clone()).unwrap(); + let registry = ToolRegistry::build_with_handlers( + ®istry_tools(Some(&tools), &GatewayToolMap::default()), + &exec_ctx.gateway_executors, + ) + .await + .unwrap(); + + let result = run_messages_loop(request, ®istry, &exec_ctx, None).await.unwrap(); + + // The request did NOT fail β€” it looped to a final answer. + assert_eq!(result["stop_reason"], "end_turn"); + assert_eq!(upstream.calls.load(Ordering::SeqCst), 2); + // Round 2 fed back a tool_result (carrying the error text), not a hard failure. + let reqs = upstream.requests.lock().await; + let round2 = reqs[1]["messages"].as_array().unwrap(); + let has_tool_result = round2 + .iter() + .filter_map(|m| m["content"].as_array()) + .flatten() + .any(|b| b["type"] == "tool_result"); + assert!(has_tool_result, "tool failure fed back as an (error) tool_result"); +} + +// E14: an upstream error body mid-loop is surfaced (not swallowed or looped on). +#[tokio::test] +async fn messages_loop_surfaces_upstream_error_body() { + let err = serde_json::json!({"type": "error", "error": {"type": "overloaded_error", "message": "busy"}}); + let (result, calls) = run_against(vec![err], web_search_request()).await; + assert_eq!(calls, 1, "error surfaced on the first round, no loop"); + assert_eq!(result["type"], "error"); + assert_eq!(result["error"]["type"], "overloaded_error"); +} + +// E4: the loop caps at MAX rounds. Feed an unbounded run of tool_use rounds and +// assert it terminates with an error rather than looping forever. +#[tokio::test] +async fn messages_loop_caps_at_max_rounds() { + let tool_round = serde_json::json!({ + "id": "m", "type": "message", "role": "assistant", "model": "qwen3", + "content": [{"type": "tool_use", "id": "t", "name": "web_search", "input": {"query": "x"}}], + "stop_reason": "tool_use", "usage": {"input_tokens": 1, "output_tokens": 1} + }); + // 20 tool rounds available, but the loop must stop at its cap (10). + let bodies = vec![tool_round; 20]; + let (vllm_url, upstream, _v) = spawn_mock_vllm_messages(bodies).await; + let (search_url, _rx, _s) = spawn_mock_search().await; + let exec_ctx = build_exec_ctx(&vllm_url, &search_url).await; + let request = web_search_request(); + let tools: Vec = serde_json::from_value(request["tools"].clone()).unwrap(); + let registry = ToolRegistry::build_with_handlers( + ®istry_tools(Some(&tools), &GatewayToolMap::default()), + &exec_ctx.gateway_executors, + ) + .await + .unwrap(); + + let result = run_messages_loop(request, ®istry, &exec_ctx, None).await.unwrap(); + + let calls = upstream.calls.load(Ordering::SeqCst); + assert!(calls <= 10, "loop must cap at MAX_GATEWAY_TOOL_ROUNDS (got {calls})"); + assert_eq!(result["type"], "error", "round-budget exhaustion surfaces an error"); + assert!( + result["error"]["message"].as_str().unwrap().contains("rounds"), + "error mentions the round cap" + ); +} + +// E3: a malformed tool_use.input (not an object) must not panic β€” the arg +// stringification falls back and the call still dispatches. +#[tokio::test] +async fn messages_loop_handles_malformed_tool_input() { + let round0 = serde_json::json!({ + "id": "m", "type": "message", "role": "assistant", "model": "qwen3", + // input as a bare string instead of an object. + "content": [{"type": "tool_use", "id": "t1", "name": "web_search", "input": "not-an-object"}], + "stop_reason": "tool_use", "usage": {"input_tokens": 1, "output_tokens": 1} + }); + let round1 = serde_json::json!({ + "id": "m2", "type": "message", "role": "assistant", "model": "qwen3", + "content": [{"type": "text", "text": "done"}], + "stop_reason": "end_turn", "usage": {"input_tokens": 1, "output_tokens": 1} + }); + let (result, calls) = run_against(vec![round0, round1], web_search_request()).await; + assert_eq!(calls, 2, "loop still ran the tool round + final"); + assert_eq!(result["stop_reason"], "end_turn"); +} diff --git a/crates/agentic-server-core/tests/messages_stream_test.rs b/crates/agentic-server-core/tests/messages_stream_test.rs new file mode 100644 index 0000000..3291d74 --- /dev/null +++ b/crates/agentic-server-core/tests/messages_stream_test.rs @@ -0,0 +1,269 @@ +//! Acceptance test for the streaming Messages-native gateway tool loop (#115). +//! +//! Drives `run_messages_stream` against a mock vLLM `/v1/messages` that replays +//! the recorded #123 **streaming** cassette (turn 0 streams a `web_search` +//! `tool_use`; turn 1 streams the final text) as `text/event-stream`, plus a +//! mock You.com backend. Asserts the client sees ONE logical message +//! (`message_start`/`message_stop` once), the gateway `tool_use` is suppressed, +//! block indices stay contiguous across rounds, and no raw per-round terminal +//! leaks. + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use agentic_core::executor::{ConversationHandler, ExecutionContext, ResponseHandler, run_messages_stream}; +use agentic_core::storage::{ConversationStore, ResponseStore}; +use agentic_core::tool::{ToolRegistry, WebSearchHandler}; +use agentic_core::types::messages::{GatewayToolMap, ToolParam, registry_tools}; +use axum::extract::State; +use axum::response::{IntoResponse, Response}; +use axum::routing::post; +use axum::{Json, Router}; +use futures::StreamExt; +use http::StatusCode; +use serde_json::Value; +use tokio::net::TcpListener; + +mod support; + +const CASSETTE: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/cassettes/messages/messages-web-search-Qwen-Qwen3-30B-A3B-FP8-streaming.yaml" +); + +const MULTIROUND: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/cassettes/messages_multiround/multiround-web-search-qwen3-streaming.yaml" +); + +/// Load each streaming turn's SSE body (the raw event-stream text) from the cassette. +fn cassette_turn_streams() -> Vec { + streams_at(CASSETTE) +} + +fn streams_at(path: &str) -> Vec { + let text = std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {path}: {e}")); + let doc: Value = serde_yaml::from_str(&text).unwrap_or_else(|e| panic!("parse {path}: {e}")); + doc["turns"] + .as_array() + .expect("turns") + .iter() + .map(|t| { + let mut body = t["response"]["sse"] + .as_array() + .expect("sse array") + .iter() + .map(|l| l.as_str().unwrap_or_default()) + .collect::(); + if !body.contains("data: [DONE]") { + body.push_str("data: [DONE]\n\n"); + } + body + }) + .collect() +} + +#[derive(Clone)] +struct UpstreamState { + streams: Arc>, + calls: Arc, +} + +async fn spawn_mock_vllm_stream(streams: Vec) -> (String, Arc, tokio::task::JoinHandle<()>) { + let calls = Arc::new(AtomicUsize::new(0)); + let state = UpstreamState { + streams: Arc::new(streams), + calls: Arc::clone(&calls), + }; + let app = Router::new() + .route( + "/v1/messages", + post(|State(st): State, _body: axum::body::Bytes| async move { + let n = st.calls.fetch_add(1, Ordering::SeqCst); + let body = st.streams.get(n).cloned().unwrap_or_default(); + Response::builder() + .status(StatusCode::OK) + .header("content-type", "text/event-stream") + .body(axum::body::Body::from(body)) + .unwrap() + .into_response() + }), + ) + .with_state(state); + 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.ok(); + }); + (format!("http://{addr}"), calls, handle) +} + +async fn spawn_mock_search() -> (String, tokio::task::JoinHandle<()>) { + let app = Router::new().route( + "/v1/search", + post(|Json(_body): Json| async move { + Json(serde_json::json!({ + "results": {"web": [{"url": "https://www.rust-lang.org/", "title": "Rust", + "description": "d", "snippets": ["Rust 1.89.0 is the latest stable release."]}], "news": []}, + "metadata": {"query": "q", "search_uuid": "s1", "latency": 0.1} + })) + }), + ); + 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.ok(); + }); + (format!("http://{addr}"), handle) +} + +async fn build_exec_ctx(vllm_url: &str, search_url: &str) -> Arc { + let pool = support::setup_pool().await; + let conv = ConversationHandler::new(ConversationStore::new(Arc::clone(&pool))); + let resp = ResponseHandler::new(ResponseStore::new(pool)); + let client = Arc::new(reqwest::Client::new()); + Arc::new( + ExecutionContext::new(conv, resp, Arc::clone(&client), vllm_url.to_owned()).with_gateway_executor(Arc::new( + WebSearchHandler::with_api_key(client, "test-key".to_owned(), search_url), + )), + ) +} + +#[tokio::test] +async fn messages_stream_presents_one_message_and_hides_gateway_tool() { + let (vllm_url, calls, _v) = spawn_mock_vllm_stream(cassette_turn_streams()).await; + let (search_url, _s) = spawn_mock_search().await; + let exec_ctx = build_exec_ctx(&vllm_url, &search_url).await; + + let request = serde_json::json!({ + "model": "qwen3", "max_tokens": 1024, "stream": true, + "messages": [{"role": "user", "content": "What is the latest stable Rust release? Use web_search."}], + "tools": [{"name": "web_search", "description": "s", + "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}] + }); + let tools: Vec = serde_json::from_value(request["tools"].clone()).unwrap(); + let registry = Arc::new( + ToolRegistry::build_with_handlers( + ®istry_tools(Some(&tools), &GatewayToolMap::default()), + &exec_ctx.gateway_executors, + ) + .await + .unwrap(), + ); + + let stream = run_messages_stream(request, registry, Arc::clone(&exec_ctx), None); + let chunks: Vec = stream.collect().await; + let sse = chunks.join(""); + + // Two upstream rounds ran (tool round + final). + assert_eq!(calls.load(Ordering::SeqCst), 2, "one tool round + one final round"); + + // Exactly one logical message lifecycle. + assert_eq!( + sse.matches("event: message_start").count(), + 1, + "one message_start: {sse:?}" + ); + assert_eq!(sse.matches("event: message_stop").count(), 1, "one message_stop"); + assert_eq!( + sse.matches("event: message_delta").count(), + 1, + "one terminal message_delta (intermediate suppressed)" + ); + + // Gateway tool_use suppressed β€” no tool_use content block surfaces. + assert!( + !sse.contains(r#""type":"tool_use""#), + "gateway tool_use must be hidden from the client stream" + ); + + // Final terminal is end_turn (not the intermediate tool_use). + assert!(sse.contains(r#""stop_reason":"end_turn""#), "terminal is end_turn"); + + // Block indices contiguous across rounds (no reset/collision): parse every + // content_block_start index and assert 0..N with no dup. + let mut indices: Vec = Vec::new(); + for line in sse.lines() { + if let Some(d) = line.strip_prefix("data: ") { + if let Ok(ev) = serde_json::from_str::(d) { + if ev["type"] == "content_block_start" { + indices.push(ev["index"].as_u64().expect("index")); + } + } + } + } + assert!(!indices.is_empty(), "some blocks surfaced"); + assert_eq!( + indices, + (0..indices.len() as u64).collect::>(), + "surfaced block indices contiguous across rounds: {indices:?}" + ); +} + +// Multi-round streaming: replay the live-recorded multi-round streaming cassette +// and assert the same single-lifecycle / contiguous-index / hidden-tool +// invariants hold across a tool round + a final round. +#[tokio::test] +async fn messages_stream_multiround_single_lifecycle() { + let (vllm_url, calls, _v) = spawn_mock_vllm_stream(streams_at(MULTIROUND)).await; + let (search_url, _s) = spawn_mock_search().await; + let exec_ctx = build_exec_ctx(&vllm_url, &search_url).await; + + let request = serde_json::json!({ + "model": "qwen3", "max_tokens": 1024, "stream": true, + "messages": [{"role": "user", "content": "Use web_search for the latest rust version, then its date."}], + "tools": [{"name": "web_search", "description": "s", + "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}] + }); + let tools: Vec = serde_json::from_value(request["tools"].clone()).unwrap(); + let registry = Arc::new( + ToolRegistry::build_with_handlers( + ®istry_tools(Some(&tools), &GatewayToolMap::default()), + &exec_ctx.gateway_executors, + ) + .await + .unwrap(), + ); + + let stream = run_messages_stream(request, registry, Arc::clone(&exec_ctx), None); + let sse = stream.collect::>().await.join(""); + + assert!( + calls.load(Ordering::SeqCst) >= 2, + "at least a tool round + a final round" + ); + assert_eq!( + sse.matches("event: message_start").count(), + 1, + "one message_start across rounds" + ); + assert_eq!( + sse.matches("event: message_stop").count(), + 1, + "one message_stop across rounds" + ); + assert!( + !sse.contains(r#""type":"tool_use""#), + "gateway tool_use suppressed in the client stream" + ); + assert!( + !sse.contains("response.output_text.delta"), + "no raw Responses SSE leaks" + ); + // Contiguous surfaced indices across rounds. + let mut idx = Vec::new(); + for line in sse.lines() { + if let Some(d) = line.strip_prefix("data: ") { + if let Ok(ev) = serde_json::from_str::(d) { + if ev["type"] == "content_block_start" { + idx.push(ev["index"].as_u64().expect("index")); + } + } + } + } + assert_eq!( + idx, + (0..idx.len() as u64).collect::>(), + "contiguous indices: {idx:?}" + ); +} diff --git a/crates/agentic-server/src/handler/http/messages.rs b/crates/agentic-server/src/handler/http/messages.rs index 6660d2b..147c5e8 100644 --- a/crates/agentic-server/src/handler/http/messages.rs +++ b/crates/agentic-server/src/handler/http/messages.rs @@ -1,19 +1,25 @@ +use std::sync::Arc; + use axum::extract::{Request, State}; -use axum::response::Response; +use axum::response::{IntoResponse, Response}; use bytes::Bytes; +use http::HeaderMap; +use tracing::debug; -use agentic_core::proxy::{ProxyAuth, ProxyRequest, proxy_request_with_path}; +use agentic_core::executor::{ExecutorError, run_messages_loop, run_messages_stream}; +use agentic_core::proxy::{ProxyAuth, ProxyRequest, error_response_for_auth, proxy_request_with_path}; +use agentic_core::tool::ToolRegistry; +use agentic_core::types::messages::{MessagesRequest, has_gateway_tool, registry_tools}; -use super::super::common::{convert_response, read_bytes_with_auth}; +use super::super::common::{convert_response, read_bytes_with_auth, sse_response}; use crate::app::AppState; -async fn proxy_messages(state: &AppState, request: Request, path: &'static str) -> Response { - let (parts, body) = request.into_parts(); - let body: Bytes = match read_bytes_with_auth(body, ProxyAuth::Anthropic).await { - Ok(body) => body, - Err(response) => return response, - }; - +async fn proxy_messages( + state: &AppState, + parts: axum::http::request::Parts, + body: Bytes, + path: &'static str, +) -> Response { convert_response( proxy_request_with_path( ProxyRequest { @@ -29,10 +35,105 @@ async fn proxy_messages(state: &AppState, request: Request, path: &'static str) ) } +/// Extract the client's Anthropic credential β€” `x-api-key` (Anthropic-native) or +/// an `Authorization: Bearer` β€” falling back to the server's configured key. +/// Consistent with the proxy path forwarding the client's `x-api-key` (E15). +fn extract_client_key(headers: &HeaderMap, config_key: Option<&str>) -> Option { + headers + .get("x-api-key") + .and_then(|v| v.to_str().ok()) + .filter(|s| !s.is_empty()) + .map(str::to_owned) + .or_else(|| { + headers + .get("authorization") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.strip_prefix("Bearer ")) + .filter(|s| !s.is_empty()) + .map(str::to_owned) + }) + .or_else(|| config_key.filter(|s| !s.is_empty()).map(str::to_owned)) +} + +/// Render an executor error as the Anthropic error envelope +/// (`{"type":"error","error":{"type","message"}}`), consistent with the proxy +/// path (E14). +fn messages_error_response(err: &ExecutorError) -> Response { + convert_response(error_response_for_auth( + err.http_status(), + err.error_code(), + &err.to_string(), + ProxyAuth::Anthropic, + )) +} + +/// Drive the Messages-native gateway tool loop (non-streaming or streaming) for +/// a request that declares a gateway-owned tool. +async fn execute_messages(state: &AppState, headers: &HeaderMap, req: &MessagesRequest, body: &Bytes) -> Response { + let auth = extract_client_key(headers, state.openai_api_key.as_deref()); + + // Build the request-scoped registry from the declared tools (M6). Gateway + // ownership (incl. configured aliases like Claude Code's `WebSearch`) is + // resolved against the operator-configured map. + let gateway_map = &state.exec_ctx.messages_gateway_tools; + let registry = match ToolRegistry::build_with_handlers( + ®istry_tools(req.tools.as_ref(), gateway_map), + &state.exec_ctx.gateway_executors, + ) + .await + { + Ok(r) => r, + Err(e) => return messages_error_response(&ExecutorError::from(e)), + }; + + // Parse the raw body to a JSON Value the loop forwards upstream untouched β€” + // preserving every Anthropic field (tool_choice, stop_sequences, …). + let request_json: serde_json::Value = match serde_json::from_slice(body) { + Ok(v) => v, + Err(e) => return messages_error_response(&ExecutorError::from(e)), + }; + + if req.stream { + let stream = run_messages_stream(request_json, Arc::new(registry), Arc::clone(&state.exec_ctx), auth); + sse_response(stream) + } else { + match run_messages_loop(request_json, ®istry, &state.exec_ctx, auth.as_deref()).await { + Ok(message) => axum::Json(message).into_response(), + Err(e) => messages_error_response(&e), + } + } +} + pub async fn messages(State(state): State, request: Request) -> Response { - proxy_messages(&state, request, "/v1/messages").await + let (parts, body) = request.into_parts(); + let bytes: Bytes = match read_bytes_with_auth(body, ProxyAuth::Anthropic).await { + Ok(bytes) => bytes, + Err(response) => return response, + }; + + // Route to the loop only when a gateway-owned tool is declared; everything + // else keeps the transparent proxy path. + if let Ok(req) = serde_json::from_slice::(&bytes) { + let route_to_loop = has_gateway_tool(req.tools.as_ref(), &state.exec_ctx.messages_gateway_tools); + debug!( + route = if route_to_loop { "messages_loop" } else { "proxy" }, + stream = req.stream, + tools = req.tools.as_ref().map_or(0, Vec::len), + "routing HTTP messages request" + ); + if route_to_loop { + return execute_messages(&state, &parts.headers, &req, &bytes).await; + } + } + + proxy_messages(&state, parts, bytes, "/v1/messages").await } pub async fn count_tokens(State(state): State, request: Request) -> Response { - proxy_messages(&state, request, "/v1/messages/count_tokens").await + let (parts, body) = request.into_parts(); + let bytes: Bytes = match read_bytes_with_auth(body, ProxyAuth::Anthropic).await { + Ok(bytes) => bytes, + Err(response) => return response, + }; + proxy_messages(&state, parts, bytes, "/v1/messages/count_tokens").await } diff --git a/crates/agentic-server/tests/messages_test.rs b/crates/agentic-server/tests/messages_test.rs index d0b96bd..f39f344 100644 --- a/crates/agentic-server/tests/messages_test.rs +++ b/crates/agentic-server/tests/messages_test.rs @@ -223,3 +223,80 @@ async fn messages_returns_anthropic_error_for_unreachable_upstream() { }) ); } + +/// Mock vLLM `/v1/messages` that returns a canned Anthropic message and records +/// how many times it was called (to prove routing). +async fn spawn_mock_vllm_messages(body: &'static str) -> (String, Arc>, tokio::task::JoinHandle<()>) { + let calls = Arc::new(Mutex::new(0usize)); + let route_calls = Arc::clone(&calls); + let app = Router::new().route( + "/v1/messages", + post(move |_body: Bytes| { + let route_calls = Arc::clone(&route_calls); + async move { + *route_calls.lock().await += 1; + Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .body(axum::body::Body::from(body)) + .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}"), calls, handle) +} + +// A request declaring a gateway-owned `web_search` tool routes to the native +// loop (hits vLLM /v1/messages) and returns an Anthropic message. The model +// answers directly here (end_turn) so no search backend is needed. +#[tokio::test] +async fn messages_with_web_search_tool_routes_to_native_loop() { + let final_msg = r#"{"id":"m","type":"message","role":"assistant","model":"qwen3","content":[{"type":"text","text":"Rust 1.89.0."}],"stop_reason":"end_turn","usage":{"input_tokens":5,"output_tokens":3}}"#; + let (llm_url, calls, _upstream) = spawn_mock_vllm_messages(final_msg).await; + let (gateway_url, _gateway) = spawn_gateway(test_state(&test_config(&llm_url))).await; + let body = br#"{"model":"qwen3","max_tokens":256,"stream":false,"messages":[{"role":"user","content":"latest rust?"}],"tools":[{"name":"web_search","description":"s","input_schema":{"type":"object","properties":{"query":{"type":"string"}}}}]}"#; + + let response = reqwest::Client::new() + .post(format!("{gateway_url}/v1/messages")) + .body(body.to_vec()) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + // The loop called vLLM /v1/messages upstream (native, not the Responses path). + assert_eq!(*calls.lock().await, 1, "native loop should call /v1/messages"); + let json: serde_json::Value = response.json().await.unwrap(); + assert_eq!(json["type"], "message"); + assert_eq!(json["role"], "assistant"); + assert_eq!(json["content"][0]["text"], "Rust 1.89.0."); + assert_eq!(json["stop_reason"], "end_turn"); +} + +// A request with NO gateway-owned tool stays on the transparent proxy β€” the +// native loop is never engaged. +#[tokio::test] +async fn messages_without_gateway_tool_uses_proxy() { + let (llm_url, requests, _upstream) = + spawn_recording_upstream(StatusCode::OK, "application/json", r#"{"id":"proxied"}"#).await; + let (gateway_url, _gateway) = spawn_gateway(test_state(&test_config(&llm_url))).await; + // A custom (client-owned) tool only β€” not gateway-owned. + let body = br#"{"model":"qwen3","max_tokens":64,"stream":false,"messages":[{"role":"user","content":"hi"}],"tools":[{"name":"get_weather","input_schema":{"type":"object"}}]}"#; + + let response = reqwest::Client::new() + .post(format!("{gateway_url}/v1/messages")) + .body(body.to_vec()) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + // Proxied verbatim (the recording upstream echoes the raw body path). + let requests = requests.lock().await; + assert_eq!(requests.len(), 1); + assert_eq!(requests[0].body.as_ref(), body); +} diff --git a/docs/design/messages-gateway-tool-classification.md b/docs/design/messages-gateway-tool-classification.md new file mode 100644 index 0000000..2094b52 --- /dev/null +++ b/docs/design/messages-gateway-tool-classification.md @@ -0,0 +1,160 @@ +# Design: gateway-owned tool classification for `/v1/messages` (Claude Code end-to-end) + +> **Status:** proposal / follow-up to PR #131 (the Messages-native gateway tool loop, #115). +> **Depends on:** #131 merged. +> **Relates to:** #113 (Claude Code integration), the Responses-side ownership model in +> [`codex-integration.md`](./codex-integration.md), and the loop-consolidation note. + +--- + +## Problem + +PR #131 makes `POST /v1/messages` run the gateway tool loop when a request declares a +gateway-owned tool. It classifies gateway-owned **by name**: + +```rust +fn is_gateway_owned_tool_name(name: &str) -> bool { name == "web_search" } +``` + +That is enough for a client that declares the lowercase `web_search` tool (curl, SDKs, our +own harness), but it does **not** make Claude Code's web search run through the gateway, and +the reason is instructive. + +### What Claude Code actually sends (live-captured) + +A real Claude Code request to `/v1/messages` declares ~27 **client-owned function tools**. +Its web search is one of them: + +```json +{ "name": "WebSearch", + "description": "Search the web. Returns result blocks with titles and URLs. US-only. ...", + "input_schema": { "type": "object", "properties": { "query": {"type":"string"}, + "allowed_domains": {...}, "blocked_domains": {...} } } } +``` + +Two facts follow: + +1. **The name is `WebSearch` (PascalCase), a plain function** β€” not `web_search`, and not a + typed Anthropic server tool. Our `name == "web_search"` predicate does not match it, so the + request is proxied and the loop never engages. (Verified live: every default Claude Code turn + routes `route="proxy"`.) +2. **It is semantically Claude Code's own tool.** Claude Code declares `WebSearch` expecting + *it* executes the search (its own `allowed_domains`/`blocked_domains` handling, its + "Sources:" formatting) and receives the `tool_use` back to run client-side. + +### Why a *hardcoded* name alias is wrong β€” and why a *configured* one is right + +There are two versions of "alias `WebSearch` to `web_search`", and only one is acceptable. + +**Rejected β€” a hardcoded in-code alias** (`is_gateway_owned_tool_name` returning true for +`WebSearch` unconditionally). This: + +- **Hijacks a client-owned tool by default.** Silently converting Claude Code's `WebSearch` to + gateway-owned for *every* deployment means Claude Code never gets the `tool_use` it expects to + run. That contradicts the ownership doctrine: + > "`function` β†’ client-owned by default ... unless configured as gateway-owned" + > β€” [`codex-integration.md`](./codex-integration.md) +- **Is a silent heuristic.** The same doc bans detecting compatibility "by user agent, route, or + heuristics." A hardcoded PascalCase match is exactly that. + +**Shipped β€” an operator-configured alias** (`MESSAGES_GATEWAY_TOOL_ALIASES=WebSearch=web_search`, +empty by default). This is the *"unless **configured** as gateway-owned"* clause itself: nothing +is captured unless the operator opts in, so it is a deliberate configuration, not a silent +heuristic. It does not violate the doctrine β€” it *implements* the doctrine's escape hatch. + +**Why not structural typed-tool detection for Claude Code?** Because Claude Code never sends a +typed server tool. Its web search on the wire is `{"name":"WebSearch", "input_schema": …}` β€” a +plain `function`, no `type` field (verified live). Structural `{"type":"web_search_20250305"}` +detection (below) is the right path for Anthropic-SDK clients that *do* send a typed tool, but it +is inert for the real Claude Code CLI/SDK. For those, a **nameβ†’executor mapping is the only +mechanism that can work** β€” the question is only whether it's hardcoded (no) or operator-configured +(yes). The two mechanisms are complementary, not alternatives. + +## How Codex already gets this right (live-verified) + +Codex talks the Responses API (`/v1/responses`), and it declares web search **structurally**: + +```json +{ "type": "web_search", "name": null } +``` + +alongside its client tools (`{"type":"function","name":"exec_command"}`, …) and a +`{"type":"namespace"}` group. The Responses side classifies gateway-owned **by type**, and the +enum already aliases the known server-tool type tags: + +```rust +#[serde(rename = "web_search_preview", + alias = "web_search", alias = "web_search_preview_2025_03_11", + alias = "web_search_2025_08_26")] +WebSearch(WebSearchToolParam), +``` + +Replaying a real Codex request to the gateway returns output items +`reasoning β†’ web_search_call β†’ reasoning β†’ message` β€” i.e. the gateway executed the search +server-side and continued the loop, while Codex's `function`/`namespace` tools stayed +client-owned. **No name matching, no hijack, works today.** + +The asymmetry is the root issue: Messages classifies by **name** (fragile), Responses by +**type** (robust). This is the DIR-1 item in the loop-consolidation note. + +## Shipped (this change) + +### Operator-configured client-tool β†’ executor aliases + +A `GatewayToolMap` on `ExecutionContext`, loaded from `MESSAGES_GATEWAY_TOOL_ALIASES` +(e.g. `WebSearch=web_search`), **empty by default**. When configured, the aliased client tool is +classified gateway-owned at every site (routing, registry build, both loops), dispatch +canonicalises the call name to the executor key, and `adapt_web_search_input` rewrites the +client's argument schema (`allowed_domains β†’ include_domains`, `blocked_domains β†’ +exclude_domains`) so Claude Code's `WebSearch` reaches the `web_search` executor. The raw request +still forwards to vLLM verbatim, so the model keeps the client's own tool contract; only the +executed call is adapted and hidden. + +This is the doctrine's "unless configured" clause, and β€” per the section above β€” the only +mechanism that works for Claude Code, whose search is a client `function`, not a typed tool. + +## Future (additive, not shipped here) + +These extend the *same* `GatewayToolMap` classification seam; they do not replace the config +alias, they complement it for clients that speak a different shape. + +### 1. Structural typed-tool detection + +Recognise a **typed Anthropic server tool** (`{"type":"web_search_20250305"}` and versioned tags, +carried in `ToolParam.type_`) as gateway-owned automatically β€” for Anthropic-SDK clients that +send a typed tool. Inert for the current Claude Code CLI/SDK (they send `{"name":"WebSearch"}`), +which is why the config alias ships first. + +### 2. Per-request opt-in override + +For a per-request (rather than process-global) signal β€” `metadata.gateway_tools: ["WebSearch"]` +on the body (a header like `x-gateway-tools` is unreachable from the Claude Code SDK, which +exposes no custom-header knob β€” verified). Lets two clients with different tool casing coexist, +which the env-global map cannot. + +### 3. MCP generalisation + +MCP-over-Messages lands on the **same** seam: an `mcp_servers` request param is gateway-owned by +construction (the server connects out and runs the tool). A new *shape* to recognise, not a new +name to special-case. + +## Non-goals + +- Detecting "is this Claude Code?" by user agent or headers β€” banned by the documented doctrine. + (The config alias keys on the declared tool name the operator opted in, not on client identity.) +- Reshaping `RequestPayload` / the Responses loop β€” that is the ADR-gated consolidation, not this. +- A gateway-side Claude Code runtime. +- Working around the executor's `include_domains` + `exclude_domains` mutual exclusion β€” Claude + Code's `WebSearch` allows both `allowed_domains` and `blocked_domains`; a call setting both + yields a graceful error `tool_result`. The constraint stays in one place (the executor). + +## Validation done + +- **Claude Code, live (G6e):** captured its real request; confirmed `WebSearch` is a client + function (PascalCase), default sessions route `proxy`. A typed/lowercase `web_search` + declaration *does* route `messages_loop` and execute against You.com with the call hidden β€” + proving the loop is correct; only classification of Claude Code's own tool is the gap. +- **Codex, live (G6e):** captured its real request; confirmed `{"type":"web_search"}` is + recognised structurally, routes through the loop, returns a `web_search_call` item. + +Evidence: `contrib-track/agentic-api/stress/pr131_evidence/` (retained internally).