Files
Genarrative/server-rs/crates/platform-agent-harness/src/tool.rs
T
kdletters d7f0c45164
Project CI / Repository checks (push) Successful in 1m3s
Project CI / Frontend tests (push) Successful in 1m59s
Project CI / Native shell tests (push) Successful in 2m27s
Project CI / Backend tests (push) Successful in 3m5s
抽取画布 Agent 公共执行框架
新增业务中立的 function-calling harness 与公共提示词协议

统一画布 Agent 工具路由约束和待确认控制语义

保留失败前输出并事务化收口 memory、deadline 与取消状态

迁移画布 Agent 和 API 编排并同步架构文档与项目记忆
2026-07-28 21:34:34 +08:00

278 lines
7.2 KiB
Rust

//! Function-calling 工具及其结构化执行结果的公共契约。
use serde::{Deserialize, Serialize};
use std::pin::Pin;
// Treat explicit top-level JSON `null` tool arguments as omitted fields, for compatibility with llm
pub fn null_tool_args_as_missing(mut args: serde_json::Value) -> serde_json::Value {
if let serde_json::Value::Object(fields) = &mut args {
fields.retain(|_, value| !value.is_null());
}
args
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub args: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallResult {
pub tool_name: String,
pub output: serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolFailureKind {
InvalidArgs,
Timeout,
Cancelled,
NotFound,
PermissionDenied,
RateLimited,
Provider,
Network,
Internal,
Other,
}
impl ToolFailureKind {
pub fn default_retryable(self) -> bool {
matches!(
self,
Self::Timeout | Self::RateLimited | Self::Provider | Self::Network
)
}
pub fn default_fatal(self) -> bool {
matches!(self, Self::Internal)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolFailure {
pub kind: ToolFailureKind,
pub message: String,
pub retryable: bool,
pub fatal: bool,
}
impl ToolFailure {
pub fn new(kind: ToolFailureKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
retryable: kind.default_retryable(),
fatal: kind.default_fatal(),
}
}
pub fn invalid_args(message: impl Into<String>) -> Self {
Self::new(ToolFailureKind::InvalidArgs, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ToolFailureKind::Internal, message)
}
pub fn other(message: impl Into<String>) -> Self {
Self::new(ToolFailureKind::Other, message)
}
pub fn with_retryable(mut self, retryable: bool) -> Self {
self.retryable = retryable;
self
}
pub fn with_fatal(mut self, fatal: bool) -> Self {
self.fatal = fatal;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ToolOutcome {
InternalOk,
InternalError(ToolFailure),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolExecutionResult {
pub output: serde_json::Value,
pub outcome: ToolOutcome,
}
impl ToolExecutionResult {
pub fn success(output: serde_json::Value) -> Self {
Self {
output,
outcome: ToolOutcome::InternalOk,
}
}
pub fn failed(output: serde_json::Value, failure: ToolFailure) -> Self {
Self {
output,
outcome: ToolOutcome::InternalError(failure),
}
}
pub fn failure(&self) -> Option<&ToolFailure> {
match &self.outcome {
ToolOutcome::InternalOk => None,
ToolOutcome::InternalError(failure) => Some(failure),
}
}
pub fn is_fatal(&self) -> bool {
self.failure().is_some_and(|failure| failure.fatal)
}
}
pub trait Tool: Sized {
const NAME: &'static str;
type Error: std::error::Error + 'static;
type Args: for<'a> Deserialize<'a>;
type Output: Serialize;
fn tool_name(&self) -> &'static str {
Self::NAME
}
/// Human-readable description of what the tool does.
fn description(&self) -> String;
/// JSON Schema describing the tool's parameters.
fn parameters(&self) -> serde_json::Value;
fn call(
&self,
args: Self::Args,
) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
fn requires_user_confirmation(&self) -> bool {
false
}
fn classify_error(&self, error: &Self::Error) -> ToolFailure {
ToolFailure::other(error.to_string())
}
}
/// Wrapper trait to allow for dynamic dispatch of simple tools.
pub trait ToolDyn: Send + Sync {
fn tool_name(&self) -> &'static str;
fn description(&self) -> String;
fn parameters(&self) -> serde_json::Value;
fn requires_user_confirmation(&self) -> bool;
fn call(
&self,
args: serde_json::Value,
) -> Pin<Box<dyn Future<Output = ToolExecutionResult> + Send + '_>>;
}
impl<T: Tool + Send + Sync> ToolDyn for T {
fn tool_name(&self) -> &'static str {
T::NAME
}
fn description(&self) -> String {
self.description()
}
fn parameters(&self) -> serde_json::Value {
self.parameters()
}
fn requires_user_confirmation(&self) -> bool {
T::requires_user_confirmation(self)
}
fn call(
&self,
args: serde_json::Value,
) -> Pin<Box<dyn Future<Output = ToolExecutionResult> + Send + '_>> {
Box::pin(async move {
let parsed: T::Args = match serde_json::from_value(null_tool_args_as_missing(args)) {
Ok(parsed) => parsed,
Err(error) => {
return ToolExecutionResult::failed(
serde_json::Value::Null,
ToolFailure::invalid_args(format!("bad args for {}: {error}", T::NAME)),
);
}
};
let output = match self.call(parsed).await {
Ok(output) => output,
Err(error) => {
return ToolExecutionResult::failed(
serde_json::Value::Null,
self.classify_error(&error),
);
}
};
match serde_json::to_value(&output) {
Ok(output) => ToolExecutionResult::success(output),
Err(error) => ToolExecutionResult::failed(
serde_json::Value::Null,
ToolFailure::internal(error.to_string()),
),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
use serde_json::json;
use std::convert::Infallible;
#[derive(Deserialize)]
struct DefaultBackedArgs {
#[serde(default = "default_duration")]
duration: u32,
}
fn default_duration() -> u32 {
4
}
struct DefaultBackedTool;
impl Tool for DefaultBackedTool {
const NAME: &'static str = "default-backed-tool";
type Error = Infallible;
type Args = DefaultBackedArgs;
type Output = u32;
fn description(&self) -> String {
"test default-backed tool".to_string()
}
fn parameters(&self) -> serde_json::Value {
json!({ "type": "object" })
}
fn call(
&self,
args: Self::Args,
) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send {
async move { Ok(args.duration) }
}
}
#[tokio::test]
async fn dyn_tool_treats_explicit_null_as_an_omitted_default_backed_field() {
let result =
<DefaultBackedTool as ToolDyn>::call(&DefaultBackedTool, json!({ "duration": null }))
.await;
assert_eq!(result.output, json!(4));
assert_eq!(result.outcome, ToolOutcome::InternalOk);
}
}