Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ access happen — those live in `tool/`, `executor/`, and `storage/` respectivel
(the normalized `FunctionTool` and `ToolChoice`, distinct from tool *declarations*),
`usage.rs` (token accounting structs).
- **`types/tools/params.rs`** — the tool **declaration** shapes a client sends:
`ResponsesTool` (tagged enum: `Function`, `Mcp`, `WebSearch`, `FileSearch`,
`ResponsesTool` (tagged enum: `Function`, `ToolSearch`, `Mcp`, `WebSearch`, `FileSearch`,
`CodeInterpreter`, `Namespace`, `Custom`, `Unknown`) and each variant's param struct.
This is a good concrete example of the module boundary: `ResponsesTool` is *defined*
here as a pure shape, but its behavior — `validate()` and `to_function_tools()` — is
Expand Down Expand Up @@ -329,16 +329,19 @@ via `process_event`/`synthetic_event`/`emit_sse_frame`.

#### `function_sse.rs` — `FunctionSseTranslator`

vLLM only ever emits `function_call` SSE events, regardless of which tool type the
call is routed to. This translator looks up each call's name in the tool registry and
reshapes the raw stream accordingly:
Upstreams without native support for a declared tool type emit `function_call` SSE
events instead. This translator borrows the request-scoped tool registry for
classification and reshapes those raw calls accordingly:
- **Custom tools** — rewritten into the public `custom_tool_call` event shape
(`output_item.added` / `custom_tool_call_input.delta` / `.done` / `output_item.done`),
reconstructing the `input` JSON incrementally from the streamed `arguments`.
- **Gateway-owned tools** (`Mcp`, `WebSearch`, `FileSearch`, `CodeInterpreter`) — raw
frames are suppressed entirely. Their real client-visible events are synthesized
later, once the call has actually executed, by `gateway.rs`.
- **Client-owned tools** (`Function`, `CodexNamespace`) — pass through unchanged.
- **Tool search** — native `tool_search_call` events pass through as typed items;
synthetic `function_call` events named `tool_search` are projected into that same
public lifecycle after validation.

It also buffers function-call events that arrive before the call's name is known
(bounded at 256 KiB) and replays them once the name resolves.
Expand Down
3 changes: 3 additions & 0 deletions crates/agentic-server-core/src/events/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::types::io::ResponseUsage;
pub enum SSEItemType {
Reasoning,
FunctionCall,
ToolSearchCall,
CustomToolCall,
WebSearchCall,
McpCall,
Expand All @@ -22,6 +23,7 @@ impl SSEItemType {
match self {
Self::Reasoning => "reasoning",
Self::FunctionCall => "function_call",
Self::ToolSearchCall => "tool_search_call",
Self::CustomToolCall => "custom_tool_call",
Self::WebSearchCall => "web_search_call",
Self::McpCall => "mcp_call",
Expand All @@ -37,6 +39,7 @@ impl From<&str> for SSEItemType {
match s {
"reasoning" => Self::Reasoning,
"function_call" => Self::FunctionCall,
"tool_search_call" => Self::ToolSearchCall,
"custom_tool_call" => Self::CustomToolCall,
"web_search_call" => Self::WebSearchCall,
"mcp_call" => Self::McpCall,
Expand Down
51 changes: 51 additions & 0 deletions crates/agentic-server-core/src/executor/accumulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum InFlight {
Message { item: OutputMessage, text: String },
Reasoning { item: ReasoningOutput, text: String },
FunctionCall { item: FunctionToolCall, arguments: String },
ToolSearchCall { item: crate::types::io::ToolSearchCall },
CustomToolCall { item: CustomToolCall, input: String },
WebSearchCall { item: Option<WebSearchCall> },
McpCall { item: McpCall },
Expand All @@ -47,6 +48,7 @@ impl std::fmt::Debug for InFlight {
Self::Message { .. } => write!(f, "InFlight::Message {{ .. }}"),
Self::Reasoning { .. } => write!(f, "InFlight::Reasoning {{ .. }}"),
Self::FunctionCall { .. } => write!(f, "InFlight::FunctionCall {{ .. }}"),
Self::ToolSearchCall { .. } => write!(f, "InFlight::ToolSearchCall {{ .. }}"),
Self::CustomToolCall { .. } => write!(f, "InFlight::CustomToolCall {{ .. }}"),
Self::WebSearchCall { .. } => write!(f, "InFlight::WebSearchCall {{ .. }}"),
Self::McpCall { .. } => write!(f, "InFlight::McpCall {{ .. }}"),
Expand All @@ -72,6 +74,7 @@ impl InFlight {
item.status = MessageStatus::Completed;
Some(OutputItem::FunctionCall(item))
}
Self::ToolSearchCall { item } => Some(OutputItem::ToolSearchCall(item)),
Self::Message { mut item, text } => {
if !text.is_empty() {
item.content.push(OutputTextContent::new(text));
Expand Down Expand Up @@ -469,6 +472,9 @@ impl ResponseAccumulator {
item,
arguments: String::with_capacity(128),
}),
SSEItemType::ToolSearchCall => crate::types::io::ToolSearchCall::try_from(payload)
.ok()
.map(|item| InFlight::ToolSearchCall { item }),
SSEItemType::CustomToolCall => {
CustomToolCall::try_from(payload)
.ok()
Expand Down Expand Up @@ -535,6 +541,7 @@ impl ResponseAccumulator {
if let Some(entry) = in_flight_key.as_deref().and_then(|key| self.in_flight.get_mut(key)) {
match (&mut entry.item, done_item) {
(InFlight::FunctionCall { item, arguments }, _) => item.apply_done(payload, arguments),
(InFlight::ToolSearchCall { item }, _) => item.apply_done(payload, &mut String::new()),
(InFlight::CustomToolCall { item, input }, _) => item.apply_done(payload, input),
(InFlight::McpCall { item }, _) => item.apply_done(payload, &mut String::new()),
(InFlight::McpListTools { item }, _) => item.apply_done(payload, &mut String::new()),
Expand All @@ -555,6 +562,7 @@ impl ResponseAccumulator {

if let Some(
mut output_item @ (OutputItem::FunctionCall(_)
| OutputItem::ToolSearchCall(_)
| OutputItem::CustomToolCall(_)
| OutputItem::WebSearchCall(_)
| OutputItem::McpCall(_)
Expand Down Expand Up @@ -618,6 +626,8 @@ impl ResponseAccumulator {
previous_response_id: previous_response_id.map(str::to_string),
conversation_id: self.conversation_id,
instructions: instructions.map(str::to_string),
tools: None,
tool_choice: None,
}
}
}
Expand All @@ -626,6 +636,7 @@ fn in_flight_matches_call_type(item: &InFlight, item_type: SSEItemType) -> bool
matches!(
(item, item_type),
(InFlight::FunctionCall { .. }, SSEItemType::FunctionCall)
| (InFlight::ToolSearchCall { .. }, SSEItemType::ToolSearchCall)
| (InFlight::CustomToolCall { .. }, SSEItemType::CustomToolCall)
| (InFlight::WebSearchCall { .. }, SSEItemType::WebSearchCall)
| (InFlight::McpCall { .. }, SSEItemType::McpCall)
Expand Down Expand Up @@ -1843,4 +1854,44 @@ mod tests {
assert_eq!(call.name, "raw_echo");
assert_eq!(call.input, "hello");
}

#[test]
fn native_tool_search_call_accumulates_from_added_and_done() {
let acc = ResponseAccumulator::from_sse_lines(
[
r#"data: {"type":"response.output_item.added","output_index":0,"item":{"type":"tool_search_call","id":"tsc_native","call_id":"call_search","execution":"client","arguments":{},"status":"in_progress"}}"#.to_owned(),
r#"data: {"type":"response.output_item.done","output_index":0,"item":{"type":"tool_search_call","id":"tsc_native","call_id":"call_search","execution":"client","arguments":{"query":"weather"},"status":"completed"}}"#.to_owned(),
r#"data: {"type":"response.completed","response":{"id":"resp_1","status":"completed"}}"#.to_owned(),
],
None,
);

let [OutputItem::ToolSearchCall(call)] = acc.output.as_slice() else {
panic!("expected native tool_search_call");
};
assert_eq!(call.id, "tsc_native");
assert_eq!(call.call_id, "call_search");
assert_eq!(call.arguments["query"], "weather");
assert_eq!(call.status, crate::types::tools::ToolSearchStatus::Completed);
}

#[test]
fn blocking_native_tool_search_call_remains_typed() {
let body = serde_json::json!({
"id": "resp_1",
"status": "completed",
"output": [{
"type": "tool_search_call",
"id": "tsc_native",
"call_id": "call_search",
"execution": "client",
"arguments": {"query": "weather"},
"status": "completed"
}]
})
.to_string();

let acc = ResponseAccumulator::from_json(&body, None).expect("valid blocking response");
assert!(matches!(acc.output.as_slice(), [OutputItem::ToolSearchCall(_)]));
}
}
52 changes: 49 additions & 3 deletions crates/agentic-server-core/src/executor/compaction.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::executor::error::{ExecutorError, ExecutorResult};
use crate::executor::persist::persist_prepared_turn;
use crate::executor::prepare::prepare_request_tools;
use crate::executor::rehydrate::rehydrate_conversation;
use crate::executor::request::{ExecutionContext, RequestContext};
use crate::executor::upstream::fetch_blocking_payload;
Expand Down Expand Up @@ -87,6 +89,8 @@ fn item_has_meaningful_context(item: &InputItem) -> bool {
},
InputItem::FunctionCall(call) => !call.name.trim().is_empty() || !call.arguments.trim().is_empty(),
InputItem::FunctionCallOutput(output) => output.output.has_content(),
InputItem::ToolSearchCall(call) => !call.call_id.trim().is_empty() || !call.arguments.is_empty(),
InputItem::ToolSearchOutput(output) => !output.call_id.trim().is_empty() || !output.tools.is_empty(),
InputItem::CustomToolCall(call) => !call.name.trim().is_empty() || !call.input.trim().is_empty(),
InputItem::CustomToolCallOutput(output) => output.output.has_content(),
InputItem::Reasoning(reasoning) => {
Expand Down Expand Up @@ -203,7 +207,7 @@ pub(crate) async fn compact_items(
conversation_id: None,
conversation_version: None,
};
let response = fetch_blocking_payload(&ctx, exec_ctx, auth).await?;
let response = fetch_blocking_payload(&ctx, exec_ctx, auth, &crate::tool::ToolRegistry::default()).await?;
let summary = completed_summary_text(&response)?;

Ok((
Expand Down Expand Up @@ -278,15 +282,24 @@ pub async fn compact_response(
request.instructions,
);
payload.previous_response_id = request.previous_response_id;
let mut ctx = rehydrate_conversation(payload, exec_ctx).await?;
let ctx = rehydrate_conversation(payload, exec_ctx).await?;
let (mut ctx, registry) = prepare_request_tools(ctx, &exec_ctx.conv_handler, &exec_ctx.resp_handler).await?;
let model = ctx.enriched_request.model.clone();
let instructions = ctx.enriched_request.instructions.clone();
let input = std::mem::replace(&mut ctx.enriched_request.input, ResponsesInput::Items(Vec::new()));
let (output, usage) = compact_items(&model, input, instructions.as_deref(), exec_ctx, auth).await?;

let response_id = ctx.response_id.clone();
ctx.new_input_items.clone_from(&output);
match exec_ctx.resp_handler.execute_turn(ctx, Vec::new()).await {
match persist_prepared_turn(
ctx,
registry,
Vec::new(),
&exec_ctx.conv_handler,
&exec_ctx.resp_handler,
)
.await
{
Ok(()) | Err(ExecutorError::Storage(crate::StorageError::NotConfigured)) => {}
Err(error) => return Err(error),
}
Expand Down Expand Up @@ -548,6 +561,38 @@ mod tests {
server.abort();
}

#[tokio::test]
async fn compaction_prepares_tool_search_before_summarization() {
let (exec_ctx, server) = mock_execution_context(ResponseStore::disabled()).await;
let request = serde_json::from_value(serde_json::json!({
"model": "test-model",
"input": [{
"type": "tool_search_call",
"id": "tsc_1",
"call_id": "call_search_1",
"arguments": {"query": "weather"}
}, {
"type": "tool_search_output",
"call_id": "call_search_1",
"tools": []
}]
}))
.expect("valid compact request");

let compacted = compact_response(request, &exec_ctx, None)
.await
.expect("compaction prepares and summarizes public search history");

assert!(matches!(compacted.output.last(), Some(InputItem::Compaction(_))));
assert!(
compacted
.output
.iter()
.all(|item| !matches!(item, InputItem::ToolSearchCall(_) | InputItem::ToolSearchOutput(_)))
);
server.abort();
}

#[tokio::test]
async fn compaction_persists_a_reusable_response_checkpoint() {
let pool = create_pool_with_schema(Some("sqlite::memory:"))
Expand Down Expand Up @@ -585,6 +630,7 @@ mod tests {
model: "test-model".to_owned(),
previous_response_id: None,
effective_tools: None,
tool_search_loaded_tools: None,
effective_tool_choice: crate::ToolChoice::Auto,
effective_instructions: None,
},
Expand Down
Loading