Skip to main content

saya_agent/protocol/
contracts.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct AgentRequest {
6    pub prompt: String,
7    pub profile_names: Vec<String>,
8    pub model: String,
9    /// Optional extra system context appended to the base SAYA system prompt (e.g. available database connections).
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub system_prompt: Option<String>,
12    #[serde(default)]
13    pub history: Vec<ChatMessage>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct ChatMessage {
18    pub role: String,
19    pub content: String,
20    #[serde(default, skip_serializing_if = "Vec::is_empty")]
21    pub tool_calls: Vec<ToolCall>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub tool_call_id: Option<String>,
24}
25
26impl ChatMessage {
27    pub fn text(role: &str, content: impl Into<String>) -> Self {
28        Self {
29            role: role.into(),
30            content: content.into(),
31            tool_calls: Vec::new(),
32            tool_call_id: None,
33        }
34    }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
38pub struct ToolCall {
39    pub id: String,
40    pub name: String,
41    pub arguments: serde_json::Value,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct ToolMetadata {
46    pub name: String,
47    pub status: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ChatRequest {
52    pub model: String,
53    pub messages: Vec<ChatMessage>,
54    pub tools: Vec<ToolDefinition>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ChatResponse {
59    pub message: ChatMessage,
60}
61
62// `arguments` carries a `serde_json::Value`, which is not `Eq`, so this enum is
63// `PartialEq` only.
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65#[serde(tag = "type", rename_all = "snake_case")]
66pub enum AgentEvent {
67    AssistantText {
68        text: String,
69    },
70    /// A tool was requested. `arguments` is the raw call payload (e.g. the SQL),
71    /// surfaced so the user can see exactly what will run before approving it.
72    ToolRequested {
73        name: String,
74        #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
75        arguments: serde_json::Value,
76    },
77    ToolCompleted {
78        name: String,
79        summary: String,
80    },
81    ToolDenied {
82        name: String,
83        reason: String,
84    },
85    Complete,
86}
87
88#[async_trait]
89pub trait ApprovalDecider: Send + Sync {
90    /// Decides whether a tool call may run. `arguments` is the raw call payload
91    /// so implementations can show the user what they are approving.
92    async fn approve(&self, tool: &ToolDefinition, arguments: &serde_json::Value) -> bool;
93}
94
95pub struct AllowReadOnlyApproval;
96
97#[async_trait]
98impl ApprovalDecider for AllowReadOnlyApproval {
99    async fn approve(&self, _: &ToolDefinition, _: &serde_json::Value) -> bool {
100        true
101    }
102}
103
104#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
105pub enum ProviderError {
106    #[error("provider request failed: {0}")]
107    Request(String),
108    #[error("provider returned an invalid response")]
109    InvalidResponse,
110    #[error("provider is not configured: {0}")]
111    Configuration(String),
112    #[error("provider stream was cancelled")]
113    Cancelled,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct ToolDefinition {
118    pub name: String,
119    pub description: String,
120    pub read_only: bool,
121    pub parameters: serde_json::Value,
122    pub requires_approval: bool,
123}
124
125#[async_trait]
126pub trait ToolExecutor: Send + Sync {
127    async fn execute(
128        &self,
129        name: &str,
130        arguments: serde_json::Value,
131    ) -> Result<serde_json::Value, String>;
132}