d7f0c45164
新增业务中立的 function-calling harness 与公共提示词协议 统一画布 Agent 工具路由约束和待确认控制语义 保留失败前输出并事务化收口 memory、deadline 与取消状态 迁移画布 Agent 和 API 编排并同步架构文档与项目记忆
278 lines
7.2 KiB
Rust
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);
|
|
}
|
|
}
|