mod default_tools;
pub use default_tools::{register_default_tools, register_skill_tools, register_spawn_agent};
use super::sandbox::Sandbox;
use crate::messages::MessageChannel;
use crate::messages::MessageKind;
use crate::shared::{ToolDefinition, UserInteraction};
use crate::tasks::{TaskBuffer, TaskManager};
use schemars::{JsonSchema, schema_for};
use serde::de::DeserializeOwned;
use serde_json::{Map, Value, json};
use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc, time::Duration};
use tokio_util::sync::CancellationToken;
#[derive(Clone, Debug)]
pub struct AsyncToolConfig {
pub yield_after: Duration,
}
impl AsyncToolConfig {
pub fn new(yield_after: Duration) -> Self {
Self { yield_after }
}
}
#[derive(Clone, Debug)]
pub struct ToolSpec {
pub name: String,
pub description: String,
pub read_only: bool,
pub asynchronous: Option<AsyncToolConfig>,
}
impl ToolSpec {
pub fn new(name: impl Into<String>, description: impl Into<String>, read_only: bool) -> Self {
Self {
name: name.into(),
description: description.into(),
read_only,
asynchronous: None,
}
}
pub fn asynchronous(mut self, config: AsyncToolConfig) -> Self {
self.asynchronous = Some(config);
self
}
}
#[derive(Clone, Default)]
pub struct ToolProgress {
messages: MessageChannel,
session_id: String,
buffer: Option<TaskBuffer>,
}
impl ToolProgress {
pub(crate) fn new(
messages: MessageChannel,
session_id: String,
buffer: Option<TaskBuffer>,
) -> Self {
Self {
messages,
session_id,
buffer,
}
}
pub fn emit_text(&self, chunk: impl AsRef<str>) {
let chunk = chunk.as_ref();
if let Some(buffer) = &self.buffer {
buffer.append(chunk);
}
self.messages.send(
&self.session_id,
MessageKind::ToolOutput {
chunk: chunk.to_string(),
},
);
}
}
#[derive(Clone, Default)]
pub struct ToolCtx {
messages: MessageChannel,
session_id: String,
tasks: TaskManager,
cancellation: CancellationToken,
pub progress: ToolProgress,
}
impl ToolCtx {
pub(crate) fn new(
messages: MessageChannel,
session_id: String,
tasks: TaskManager,
cancellation: CancellationToken,
) -> Self {
Self {
messages,
session_id,
tasks,
cancellation,
progress: ToolProgress::default(),
}
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn cancellation_token(&self) -> CancellationToken {
self.cancellation.clone()
}
pub(crate) fn tasks(&self) -> &TaskManager {
&self.tasks
}
}
type BoxFuture = Pin<Box<dyn Future<Output = Result<ToolOutcome, String>> + Send>>;
type BoxedCall = Arc<dyn Fn(Arc<dyn Sandbox>, Value, ToolCtx) -> BoxFuture + Send + Sync>;
#[derive(Debug, Clone)]
pub enum ToolOutcome {
Completed(Value),
NeedsUserInteraction(UserInteraction),
}
impl From<Value> for ToolOutcome {
fn from(value: Value) -> Self {
Self::Completed(value)
}
}
#[derive(Clone)]
pub struct ToolEntry {
pub name: String,
pub description: String,
pub schema: Value,
pub read_only: bool,
pub asynchronous: Option<AsyncToolConfig>,
call: BoxedCall,
}
#[derive(Clone)]
pub struct ToolRegistry {
tools: HashMap<String, ToolEntry>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
pub fn register<A, F, Fut>(
&mut self,
name: &str,
description: &str,
read_only: bool,
handler: F,
) where
A: DeserializeOwned + JsonSchema + Send + 'static,
F: Fn(Arc<dyn Sandbox>, A) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<ToolOutcome, String>> + Send + 'static,
{
self.register_tool(ToolSpec::new(name, description, read_only), handler);
}
pub fn register_tool<A, F, Fut>(&mut self, spec: ToolSpec, handler: F)
where
A: DeserializeOwned + JsonSchema + Send + 'static,
F: Fn(Arc<dyn Sandbox>, A) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<ToolOutcome, String>> + Send + 'static,
{
let schema = serde_json::to_value(schema_for!(A)).unwrap();
let handler = Arc::new(handler);
let call: BoxedCall =
Arc::new(move |sandbox: Arc<dyn Sandbox>, v: Value, _ctx: ToolCtx| {
let handler = handler.clone();
Box::pin(async move {
let args: A =
serde_json::from_value(v).map_err(|e| format!("参数不合法: {e}"))?;
handler(sandbox, args).await
})
});
self.insert(spec, schema, call);
}
fn insert(&mut self, spec: ToolSpec, schema: Value, call: BoxedCall) {
self.tools.insert(
spec.name.clone(),
ToolEntry {
name: spec.name,
description: spec.description,
schema,
read_only: spec.read_only,
asynchronous: spec.asynchronous,
call,
},
);
}
pub fn register_with_ctx<A, F, Fut>(
&mut self,
name: &str,
description: &str,
read_only: bool,
handler: F,
) where
A: DeserializeOwned + JsonSchema + Send + 'static,
F: Fn(Arc<dyn Sandbox>, A, ToolCtx) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<ToolOutcome, String>> + Send + 'static,
{
self.register_with_ctx_tool(ToolSpec::new(name, description, read_only), handler);
}
pub fn register_with_ctx_tool<A, F, Fut>(&mut self, spec: ToolSpec, handler: F)
where
A: DeserializeOwned + JsonSchema + Send + 'static,
F: Fn(Arc<dyn Sandbox>, A, ToolCtx) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<ToolOutcome, String>> + Send + 'static,
{
let schema = serde_json::to_value(schema_for!(A)).unwrap();
let handler = Arc::new(handler);
let call: BoxedCall = Arc::new(move |sandbox: Arc<dyn Sandbox>, v: Value, ctx: ToolCtx| {
let handler = handler.clone();
Box::pin(async move {
let args: A = serde_json::from_value(v).map_err(|e| format!("参数不合法: {e}"))?;
handler(sandbox, args, ctx).await
})
});
self.insert(spec, schema, call);
}
pub fn is_read_only(&self, name: &str) -> bool {
self.tools.get(name).is_some_and(|t| t.read_only)
}
pub fn contains(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub fn names(&self) -> Vec<String> {
self.tools.keys().cloned().collect()
}
pub fn subset<S: AsRef<str>>(&self, names: &[S]) -> ToolRegistry {
let mut tools = HashMap::new();
for name in names {
let name = name.as_ref();
if let Some(entry) = self.tools.get(name) {
tools.insert(name.to_string(), entry.clone());
}
}
ToolRegistry { tools }
}
pub async fn call(
&self,
name: &str,
args: Value,
sandbox: Arc<dyn Sandbox>,
ctx: ToolCtx,
) -> Result<ToolOutcome, String> {
let tool = self
.tools
.get(name)
.ok_or_else(|| format!("工具不存在: {name}"))?;
let mut ctx = ctx;
ctx.progress = ToolProgress::new(ctx.messages.clone(), ctx.session_id.clone(), None);
let Some(config) = tool.asynchronous.clone() else {
return (tool.call)(sandbox, args, ctx).await;
};
let buffer = TaskBuffer::new();
ctx.progress = ToolProgress::new(
ctx.messages.clone(),
ctx.session_id.clone(),
Some(buffer.clone()),
);
call_asynchronous_tool(
tool.name.clone(),
tool.call.clone(),
sandbox,
args,
ctx,
buffer,
config,
)
.await
}
pub fn definitions(&self) -> Vec<ToolDefinition> {
self.tools
.values()
.map(|t| ToolDefinition {
name: t.name.clone(),
desc: t.description.clone(),
arguments: t.schema.clone(),
})
.collect()
}
}
async fn call_asynchronous_tool(
tool_name: String,
call: BoxedCall,
sandbox: Arc<dyn Sandbox>,
args: Value,
ctx: ToolCtx,
buffer: TaskBuffer,
config: AsyncToolConfig,
) -> Result<ToolOutcome, String> {
let tasks = ctx.tasks.clone();
let mut handle = tokio::spawn({
let buffer = buffer.clone();
async move {
let result = match (call)(sandbox, args, ctx).await {
Ok(ToolOutcome::Completed(value)) => Ok(value),
Ok(ToolOutcome::NeedsUserInteraction(_)) => Err(
"asynchronous tools cannot request user interaction after yielding".to_string(),
),
Err(error) => Err(error),
};
buffer.finish(result);
}
});
tokio::select! {
join_result = &mut handle => {
if let Err(err) = join_result {
return Err(format!("tool task failed: {err}"));
}
let output = buffer.drain_new();
let (_finished, result, error) = buffer.status_parts();
if let Some(error) = error {
return Err(error);
}
Ok(merge_tool_result(
"completed",
output,
result.unwrap_or_else(|| json!({})),
).into())
}
_ = tokio::time::sleep(config.yield_after) => {
let output = buffer.drain_new();
let id = tasks.register(tool_name.clone(), handle, buffer);
Ok(json!({
"id": id,
"tool_name": tool_name,
"status": "running",
"output": output,
"note": "工具仍在后台运行;用 check 工具按 id 回来查看新增输出与最终状态,用 kill 终止。",
}).into())
}
}
}
pub(crate) fn merge_tool_result(status: &str, output: String, result: Value) -> Value {
match result {
Value::Object(mut object) => {
object
.entry("status".to_string())
.or_insert_with(|| Value::String(status.to_string()));
object
.entry("output".to_string())
.or_insert_with(|| Value::String(output));
Value::Object(object)
}
other => {
let mut object = Map::new();
object.insert("status".to_string(), Value::String(status.to_string()));
object.insert("output".to_string(), Value::String(output));
object.insert("result".to_string(), other);
Value::Object(object)
}
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod subset_tests {
use super::*;
fn full() -> ToolRegistry {
let mut r = ToolRegistry::new();
register_default_tools(&mut r);
r
}
#[test]
fn subset_keeps_only_named_tools_and_preserves_callable() {
let r = full();
let sub = r.subset(&["read_file", "bash"]);
assert!(sub.contains("read_file"));
assert!(sub.contains("bash"));
assert!(!sub.contains("write_file"));
let mut names = sub.names();
names.sort();
assert_eq!(names, vec!["bash".to_string(), "read_file".to_string()]);
assert!(sub.definitions().iter().any(|d| d.name == "bash"));
}
#[test]
fn subset_ignores_unknown_names() {
let sub = full().subset(&["read_file", "does_not_exist"]);
assert!(sub.contains("read_file"));
assert!(!sub.contains("does_not_exist"));
assert_eq!(sub.names().len(), 1);
}
}