use std::sync::Arc;
use serde_json::Value;
use crate::extensions::manager::ExtensionManager;
use crate::runtime::trace::extension as trace_ext;
use crate::tools::TurnToolContext;
type RouteResult = Result<Value, Box<dyn std::error::Error + Send + Sync>>;
fn local_gate_session() -> crate::tools::activation::SessionId {
crate::tools::activation::SessionId::parse(&format!(
"extension-route-{}-{}",
std::process::id(),
uuid::Uuid::new_v4()
))
.expect("generated local gate session id is always valid")
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn route_extension_provider(
manager: Arc<tokio::sync::RwLock<ExtensionManager>>,
plugin_id: &str,
provider_id: &str,
model_id: &str,
model: &str,
tools_schema: &Arc<Vec<Value>>,
system_prompt: &Option<String>,
messages: &[crate::SharedMessage],
tx: &tokio::sync::mpsc::UnboundedSender<crate::runtime::types::StreamEvent>,
temperature: Option<f32>,
max_tokens: Option<u32>,
thinking_budget: u32,
cancel: &tokio_util::sync::CancellationToken,
tool_session_id: Option<&crate::tools::activation::SessionId>,
session_tool_set: Option<&crate::tools::activation::SharedSessionToolSet>,
tool_context: Option<&TurnToolContext>,
trace: &crate::runtime::trace::TraceContext,
) -> RouteResult {
let provider_runtime_id = format!("{}:{}", plugin_id, provider_id);
let Some((handler, hook_bus, tools_shared, streaming, model_tool_use)) = ({
let manager = manager.read().await;
manager.provider(&provider_runtime_id).and_then(|provider| {
provider.handler.as_ref().map(|handler| {
let model_spec = provider.spec.models.iter().find(|m| m.id == model_id);
let streaming = model_spec
.and_then(|m| m.capabilities.get("streaming"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let model_tool_use = model_spec
.and_then(|m| m.capabilities.get("tool_use"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
(
handler.clone(),
manager.hook_bus().clone(),
manager.tools_shared(),
streaming,
model_tool_use,
)
})
})
}) else {
return Err(format!("Extension provider model '{}' is not available", model).into());
};
let trust = match crate::extensions::trust::load_trust_state() {
Ok(t) => t,
Err(e) => {
tracing::warn!("trust.json corrupt or unreadable, failing closed: {e}");
return Err(format!("Cannot route to provider: trust state unreadable: {e}").into());
}
};
if !crate::extensions::trust::is_provider_enabled(&trust, &provider_runtime_id) {
let _ = crate::extensions::audit::append_audit_entry(
&crate::extensions::audit::new_audit_entry(
plugin_id,
provider_id,
model_id,
false,
0,
false,
"blocked",
Some("trust_disabled".to_string()),
),
);
return Err(format!(
"Provider '{}' is disabled by user trust settings",
provider_runtime_id
)
.into());
}
let audit_plugin = plugin_id.to_string();
let audit_provider = provider_id.to_string();
let audit_model = model_id.to_string();
let tools_exposed = !tools_schema.is_empty();
let emit_audit =
|streamed: bool, outcome: &str, error_class: Option<&str>, tools_requested: u32| {
let _ = crate::extensions::audit::append_audit_entry(
&crate::extensions::audit::new_audit_entry(
audit_plugin.clone(),
audit_provider.clone(),
audit_model.clone(),
tools_exposed,
tools_requested,
streamed,
outcome,
error_class.map(|s| s.to_string()),
),
);
};
if cancel.is_cancelled() {
emit_audit(false, "error", Some("canceled"), 0);
return Err("operation canceled".into());
}
let params = crate::extensions::runtime::process::ProviderCompleteParams {
provider_id: provider_id.to_string(),
model_id: model_id.to_string(),
model: model.to_string(),
messages: messages.to_vec(),
system_prompt: system_prompt.clone(),
tools: tools_schema.as_ref().clone(),
temperature,
max_tokens,
thinking_budget,
};
let has_active_tools = model_tool_use && !tools_schema.is_empty();
if streaming && !has_active_tools {
let mut attempt = trace_ext::ExtensionAttempt::new(
trace_ext::begin_extension_tracer(
trace,
plugin_id,
provider_id,
model_id,
true,
messages,
system_prompt.as_deref(),
tools_schema,
trace_ext::extension_capability_losses(tools_schema, model_tool_use, false),
)
.await,
);
let first_event = attempt.first_event_mark();
let (sink_tx, mut sink_rx) =
tokio::sync::mpsc::channel::<crate::extensions::runtime::process::ProviderStreamEvent>(
crate::extensions::runtime::process::PROVIDER_EVENT_QUEUE_CAPACITY,
);
let tx_clone = tx.clone();
let stream_tool_uses = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let stream_tool_uses_fwd = std::sync::Arc::clone(&stream_tool_uses);
let forwarder = tokio::spawn(async move {
use crate::extensions::runtime::process::ProviderStreamEvent;
while let Some(event) = sink_rx.recv().await {
match event {
ProviderStreamEvent::TextDelta { text } => {
first_event.mark();
let _ = tx_clone.send(crate::runtime::types::StreamEvent::Llm(
crate::runtime::types::LlmEvent::Text(text),
));
}
ProviderStreamEvent::ToolUse { .. } => {
first_event.mark();
stream_tool_uses_fwd.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
tracing::warn!("provider.stream tool_use event ignored (streaming tool-use not yet wired)");
}
ProviderStreamEvent::ThinkingDelta { .. }
| ProviderStreamEvent::Usage { .. } => {
first_event.mark();
}
_ => {}
}
}
});
let stream_fut = handler.provider_stream(params, sink_tx);
let result = tokio::select! {
biased;
_ = cancel.cancelled() => {
forwarder.abort();
attempt.finish_canceled();
emit_audit(
true,
"error",
Some("canceled"),
stream_tool_uses.load(std::sync::atomic::Ordering::Relaxed),
);
return Err("operation canceled".into());
}
res = stream_fut => res,
};
let _ = forwarder.await;
let stream_tool_uses = stream_tool_uses.load(std::sync::atomic::Ordering::Relaxed);
if cancel.is_cancelled() {
attempt.finish_canceled();
emit_audit(true, "error", Some("canceled"), stream_tool_uses);
return Err("operation canceled".into());
}
match result {
Ok(complete) => {
attempt.finish_success(
complete
.stop_reason
.as_deref()
.map(trace_ext::stop_reason_from_extension),
trace_ext::usage_from_extension_value(complete.usage.as_ref()),
);
emit_audit(true, "ok", None, stream_tool_uses);
Ok(serde_json::json!({
"content": complete.content,
"stop_reason": complete.stop_reason.unwrap_or_else(|| "end_turn".to_string()),
"usage": complete.usage.unwrap_or_else(|| serde_json::json!({}))
}))
}
Err(e) => {
attempt.finish_failed(trace_ext::EXTENSION_PROVIDER_ERROR_CODE);
emit_audit(true, "error", Some("provider_error"), stream_tool_uses);
Err(format!("extension provider: {e}").into())
}
}
} else {
let mut tools_requested: u32 = 0;
let mut attempt = trace_ext::ExtensionAttempt::new(
trace_ext::begin_extension_tracer(
trace,
plugin_id,
provider_id,
model_id,
false,
messages,
system_prompt.as_deref(),
tools_schema,
trace_ext::extension_capability_losses(tools_schema, model_tool_use, true),
)
.await,
);
let result = if let Some(tools) = tools_shared.filter(|_| tools_exposed) {
let registry = tools.read().await.clone();
let session_tools = crate::tools::activation::route_session_set(
session_tool_set,
registry.catalog(),
|| tool_session_id.cloned().unwrap_or_else(local_gate_session),
);
let turn_tools = tool_context.cloned().unwrap_or_else(TurnToolContext::no_chat);
let looped = crate::runtime::stream::await_tool_call(
cancel,
Box::pin(crate::extensions::runtime::process::complete_provider_with_tools(
handler.clone(),
params,
®istry,
&session_tools,
&hook_bus,
|| turn_tools.for_call(None),
turn_tools.limits().max_tool_output,
8,
&mut tools_requested,
)),
)
.await
.0;
match looped {
Some(result) => result,
None => Err("operation canceled".to_string()),
}
} else {
handler.provider_complete(params).await
};
if cancel.is_cancelled() {
attempt.finish_canceled();
emit_audit(false, "error", Some("canceled"), tools_requested);
return Err("operation canceled".into());
}
match result {
Ok(complete) => {
let text = complete
.content
.iter()
.filter_map(|block| block.get("text").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.join("");
if !text.is_empty() {
let _ = tx.send(crate::runtime::types::StreamEvent::Llm(
crate::runtime::types::LlmEvent::Text(text),
));
}
attempt.finish_success(
complete
.stop_reason
.as_deref()
.map(trace_ext::stop_reason_from_extension),
trace_ext::usage_from_extension_value(complete.usage.as_ref()),
);
emit_audit(false, "ok", None, tools_requested);
Ok(serde_json::json!({
"content": complete.content,
"stop_reason": complete.stop_reason.unwrap_or_else(|| "end_turn".to_string()),
"usage": complete.usage.unwrap_or_else(|| serde_json::json!({}))
}))
}
Err(e) => {
attempt.finish_failed(trace_ext::EXTENSION_PROVIDER_ERROR_CODE);
emit_audit(false, "error", Some("provider_error"), tools_requested);
Err(format!("extension provider: {e}").into())
}
}
}
}
#[cfg(test)]
mod cancel_tests {
use super::*;
use crate::extensions::hooks::events::{HookEvent, HookResult};
use crate::extensions::runtime::process::{
ProviderCompleteParams, ProviderCompleteResult, RegisteredProviderModelSpec,
RegisteredProviderSpec,
};
use crate::extensions::runtime::ExtensionHandler;
use serde_json::json;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
struct AsksForSleeper;
#[async_trait::async_trait]
impl ExtensionHandler for AsksForSleeper {
fn id(&self) -> &str {
"asks-for-sleeper"
}
async fn handle(&self, _event: &HookEvent) -> HookResult {
HookResult::Continue
}
async fn shutdown(&self) {}
async fn provider_complete(
&self,
_params: ProviderCompleteParams,
) -> Result<ProviderCompleteResult, String> {
Ok(ProviderCompleteResult {
content: vec![json!({
"type": "tool_use", "id": "t1", "name": "sleeper", "input": {}
})],
stop_reason: Some("tool_use".to_string()),
usage: None,
})
}
}
struct Sleeper(Arc<tokio::sync::Notify>);
#[async_trait::async_trait]
impl crate::Tool for Sleeper {
fn name(&self) -> &str {
"sleeper"
}
fn description(&self) -> &str {
"sleeps"
}
fn parameters(&self) -> Value {
json!({"type": "object"})
}
fn origin(&self) -> crate::tools::ToolOrigin {
crate::tools::ToolOrigin::Builtin
}
async fn execute(&self, _params: Value, _ctx: crate::ToolContext) -> crate::Result<String> {
self.0.notify_one();
tokio::time::sleep(Duration::from_secs(30)).await;
Ok("woke".to_string())
}
}
#[tokio::test]
#[serial_test::serial(synaps_base_dir)]
async fn a_running_tool_call_in_the_extension_loop_is_cancelled() {
let _base = crate::test_env::BaseDirGuard::new();
let started = Arc::new(tokio::sync::Notify::new());
let mut registry = crate::ToolRegistry::new();
registry.register(Arc::new(Sleeper(started.clone())));
let mut manager = ExtensionManager::new_with_tools(
Arc::new(crate::extensions::hooks::HookBus::new()),
Arc::new(tokio::sync::RwLock::new(registry)),
);
manager
.register_provider_handler_for_tests(
"plug",
RegisteredProviderSpec {
id: "prov".to_string(),
display_name: "Fake".to_string(),
description: "fake provider".to_string(),
models: vec![RegisteredProviderModelSpec {
id: "model-x".to_string(),
display_name: None,
capabilities: json!({"streaming": false, "tool_use": true}),
context_window: None,
}],
config_schema: None,
},
Arc::new(AsksForSleeper),
)
.unwrap();
let manager = Arc::new(tokio::sync::RwLock::new(manager));
let tools = Arc::new(vec![json!({
"name": "sleeper", "description": "sleeps", "input_schema": {"type": "object"}
})]);
let messages: Vec<crate::SharedMessage> =
vec![Arc::new(json!({"role": "user", "content": "go"}))];
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
let cancel = CancellationToken::new();
let trace = crate::runtime::trace::TraceContext::disabled();
let route = route_extension_provider(
manager,
"plug",
"prov",
"model-x",
"plug:prov:model-x",
&tools,
&None,
&messages,
&tx,
None,
None,
0,
&cancel,
None,
None,
None,
&trace,
);
let stop = async {
tokio::time::timeout(Duration::from_secs(10), started.notified())
.await
.expect("the tool never started");
cancel.cancel();
std::time::Instant::now()
};
let (result, stopped_at) = tokio::time::timeout(
Duration::from_secs(10),
async { tokio::join!(route, stop) },
)
.await
.expect("the loop waited for the tool");
let waited = stopped_at.elapsed();
assert!(waited < Duration::from_secs(1), "returned {waited:?} after the stop");
let error = result.expect_err("a stopped turn is not a success").to_string();
assert_eq!(error, "operation canceled");
}
}
#[cfg(test)]
mod caller_tests {
use super::*;
use crate::extensions::hooks::events::{HookEvent, HookResult};
use crate::extensions::runtime::process::{
ProviderCompleteParams, ProviderCompleteResult, RegisteredExtensionToolSpec,
RegisteredProviderModelSpec, RegisteredProviderSpec,
};
use crate::extensions::runtime::ExtensionHandler;
use crate::memory_backend::MemoryBinding;
use crate::tools::test_helpers::{create_tool_context, memory_binding};
use agent_core::memory::author::Author;
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use tokio_util::sync::CancellationToken;
#[derive(Default)]
struct AsksWhoOnce(AtomicUsize);
#[async_trait::async_trait]
impl ExtensionHandler for AsksWhoOnce {
fn id(&self) -> &str {
"asks-who"
}
async fn handle(&self, _event: &HookEvent) -> HookResult {
HookResult::Continue
}
async fn shutdown(&self) {}
async fn provider_complete(
&self,
_params: ProviderCompleteParams,
) -> Result<ProviderCompleteResult, String> {
let asks = self.0.fetch_add(1, Ordering::SeqCst).is_multiple_of(2);
Ok(ProviderCompleteResult {
content: vec![if asks {
json!({"type": "tool_use", "id": "t1", "name": "whoami", "input": {}})
} else {
json!({"type": "text", "text": "done"})
}],
stop_reason: Some(if asks { "tool_use" } else { "end_turn" }.to_string()),
usage: None,
})
}
}
#[derive(Default)]
struct Recorder(Mutex<Vec<(Option<String>, Option<Author>)>>);
#[async_trait::async_trait]
impl ExtensionHandler for Recorder {
fn id(&self) -> &str {
"caller-recorder"
}
async fn handle(&self, _event: &HookEvent) -> HookResult {
HookResult::Continue
}
async fn call_tool_as(
&self,
_name: &str,
_input: Value,
session_id: Option<&str>,
_cwd: Option<&std::path::Path>,
caller: Option<&Author>,
) -> Result<Value, String> {
self.0
.lock()
.unwrap()
.push((session_id.map(str::to_string), caller.cloned()));
Ok(json!("here"))
}
async fn shutdown(&self) {}
}
struct Admitted(crate::tools::ExtensionTool);
#[async_trait::async_trait]
impl crate::Tool for Admitted {
fn name(&self) -> &str {
"whoami"
}
fn description(&self) -> &str {
"asks the recording extension who is calling"
}
fn parameters(&self) -> Value {
json!({"type": "object"})
}
fn origin(&self) -> crate::tools::ToolOrigin {
crate::tools::ToolOrigin::Builtin
}
async fn execute(&self, params: Value, ctx: crate::ToolContext) -> crate::Result<String> {
self.0.execute(params, ctx).await
}
}
async fn one_turn(
manager: Arc<tokio::sync::RwLock<ExtensionManager>>,
tools: &Arc<Vec<Value>>,
messages: &[crate::SharedMessage],
trace: &crate::runtime::trace::TraceContext,
chat: &str,
binding: MemoryBinding,
) {
let mut capabilities = create_tool_context().capabilities;
capabilities.memory_backend = Some(binding);
capabilities.session_id = Some(chat.to_string());
let turn_tools =
TurnToolContext::new(capabilities, TurnToolContext::no_chat().limits().clone(), None);
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
let cancel = CancellationToken::new();
let routed = route_extension_provider(
manager,
"plug",
"prov",
"model-x",
"plug:prov:model-x",
tools,
&None,
messages,
&tx,
None,
None,
0,
&cancel,
None,
None,
Some(&turn_tools),
trace,
)
.await;
assert!(routed.is_ok(), "{chat}: {:?}", routed.err().map(|e| e.to_string()));
}
#[tokio::test]
#[serial_test::serial(synaps_base_dir)]
async fn a_tool_call_in_the_extension_loop_names_the_turns_own_author() {
let base = crate::test_env::BaseDirGuard::new();
let recorder = Arc::new(Recorder::default());
let mut registry = crate::ToolRegistry::new();
registry.register(Arc::new(Admitted(crate::tools::ExtensionTool::new(
"caller-recorder",
RegisteredExtensionToolSpec {
name: "whoami".into(),
description: "records the caller".into(),
input_schema: json!({"type": "object"}),
},
recorder.clone(),
))));
let mut manager = ExtensionManager::new_with_tools(
Arc::new(crate::extensions::hooks::HookBus::new()),
Arc::new(tokio::sync::RwLock::new(registry)),
);
manager
.register_provider_handler_for_tests(
"plug",
RegisteredProviderSpec {
id: "prov".to_string(),
display_name: "Fake".to_string(),
description: "fake provider".to_string(),
models: vec![RegisteredProviderModelSpec {
id: "model-x".to_string(),
display_name: None,
capabilities: json!({"streaming": false, "tool_use": true}),
context_window: None,
}],
config_schema: None,
},
Arc::new(AsksWhoOnce::default()),
)
.unwrap();
let manager = Arc::new(tokio::sync::RwLock::new(manager));
let tools = Arc::new(vec![json!({
"name": "whoami", "description": "who", "input_schema": {"type": "object"}
})]);
let messages: Vec<crate::SharedMessage> =
vec![Arc::new(json!({"role": "user", "content": "go"}))];
let trace = crate::runtime::trace::TraceContext::disabled();
let turn = |chat: &'static str, binding: MemoryBinding| {
one_turn(manager.clone(), &tools, &messages, &trace, chat, binding)
};
let dirs = [base.path().join("a"), base.path().join("b")];
let bindings = dirs
.each_ref()
.map(|dir| memory_binding(dir));
turn("chat-a", bindings[0].clone()).await;
turn("chat-b", bindings[1].clone()).await;
turn("chat-a", bindings[0].clone()).await;
let calls = recorder.0.lock().unwrap().clone();
assert_eq!(calls.len(), 3, "{calls:?}");
for ((session_id, caller), (chat, binding)) in
calls.iter().zip([("chat-a", &bindings[0]), ("chat-b", &bindings[1]), ("chat-a", &bindings[0])])
{
assert_eq!(session_id.as_deref(), Some(chat));
let caller = caller.as_ref().unwrap_or_else(|| panic!("{chat}: no caller"));
caller.validate().unwrap();
assert_eq!(caller, binding.author(), "{chat}: its own binding's author");
}
let (of_a, of_b) = (calls[0].1.clone().unwrap(), calls[1].1.clone().unwrap());
assert_ne!(of_a.actor, of_b.actor);
assert_ne!(of_a.group, of_b.group);
}
}