1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6pub struct Interaction {
7 pub id: String,
9 pub action: String,
11 #[serde(default, skip_serializing_if = "String::is_empty")]
13 pub message: String,
14 #[serde(default, skip_serializing_if = "Value::is_null")]
16 pub parameters: Value,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub response_schema: Option<Value>,
20}
21
22impl Interaction {
23 pub fn new(id: impl Into<String>, action: impl Into<String>) -> Self {
25 Self {
26 id: id.into(),
27 action: action.into(),
28 message: String::new(),
29 parameters: Value::Null,
30 response_schema: None,
31 }
32 }
33
34 pub fn with_message(mut self, message: impl Into<String>) -> Self {
36 self.message = message.into();
37 self
38 }
39
40 pub fn with_parameters(mut self, parameters: Value) -> Self {
42 self.parameters = parameters;
43 self
44 }
45
46 pub fn with_response_schema(mut self, schema: Value) -> Self {
48 self.response_schema = Some(schema);
49 self
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct InteractionResponse {
56 pub interaction_id: String,
58 pub result: Value,
60}
61
62impl InteractionResponse {
63 pub fn new(interaction_id: impl Into<String>, result: Value) -> Self {
65 Self {
66 interaction_id: interaction_id.into(),
67 result,
68 }
69 }
70
71 pub fn is_approved(result: &Value) -> bool {
73 match result {
74 Value::Bool(b) => *b,
75 Value::String(s) => {
76 let lower = s.to_lowercase();
77 matches!(
78 lower.as_str(),
79 "true" | "yes" | "approved" | "allow" | "confirm" | "ok" | "accept"
80 )
81 }
82 Value::Object(obj) => {
83 obj.get("approved")
84 .and_then(|v| v.as_bool())
85 .unwrap_or(false)
86 || obj
87 .get("allowed")
88 .and_then(|v| v.as_bool())
89 .unwrap_or(false)
90 }
91 _ => false,
92 }
93 }
94
95 pub fn is_denied(result: &Value) -> bool {
97 match result {
98 Value::Bool(b) => !*b,
99 Value::String(s) => {
100 let lower = s.to_lowercase();
101 matches!(
102 lower.as_str(),
103 "false" | "no" | "denied" | "deny" | "reject" | "cancel" | "abort"
104 )
105 }
106 Value::Object(obj) => {
107 obj.get("approved")
108 .and_then(|v| v.as_bool())
109 .map(|v| !v)
110 .unwrap_or(false)
111 || obj.get("denied").and_then(|v| v.as_bool()).unwrap_or(false)
112 }
113 _ => false,
114 }
115 }
116
117 pub fn approved(&self) -> bool {
119 Self::is_approved(&self.result)
120 }
121
122 pub fn denied(&self) -> bool {
124 Self::is_denied(&self.result)
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub struct FrontendToolInvocation {
140 pub call_id: String,
142 pub tool_name: String,
144 #[serde(default, skip_serializing_if = "Value::is_null")]
146 pub arguments: Value,
147 pub origin: InvocationOrigin,
149 pub routing: ResponseRouting,
151}
152
153impl FrontendToolInvocation {
154 pub fn new(
156 call_id: impl Into<String>,
157 tool_name: impl Into<String>,
158 arguments: Value,
159 origin: InvocationOrigin,
160 routing: ResponseRouting,
161 ) -> Self {
162 Self {
163 call_id: call_id.into(),
164 tool_name: tool_name.into(),
165 arguments,
166 origin,
167 routing,
168 }
169 }
170
171 pub fn to_interaction(&self) -> Interaction {
174 Interaction::new(&self.call_id, format!("tool:{}", self.tool_name))
175 .with_parameters(self.arguments.clone())
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181#[serde(tag = "type", rename_all = "snake_case")]
182pub enum InvocationOrigin {
183 ToolCallIntercepted {
185 backend_call_id: String,
187 backend_tool_name: String,
189 backend_arguments: Value,
191 },
192 PluginInitiated {
194 plugin_id: String,
196 },
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201#[serde(from = "ResponseRoutingWire", into = "ResponseRoutingWire")]
202pub enum ResponseRouting {
203 ReplayOriginalTool,
208 UseAsToolResult,
212 PassToLLM,
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217#[serde(tag = "strategy", rename_all = "snake_case")]
218enum ResponseRoutingWire {
219 ReplayOriginalTool {
221 #[serde(default, skip_serializing_if = "Vec::is_empty")]
222 state_patches: Vec<Value>,
223 },
224 UseAsToolResult,
228 PassToLLM,
230}
231
232impl From<ResponseRoutingWire> for ResponseRouting {
233 fn from(value: ResponseRoutingWire) -> Self {
234 match value {
235 ResponseRoutingWire::ReplayOriginalTool { .. } => Self::ReplayOriginalTool,
236 ResponseRoutingWire::UseAsToolResult => Self::UseAsToolResult,
237 ResponseRoutingWire::PassToLLM => Self::PassToLLM,
238 }
239 }
240}
241
242impl From<ResponseRouting> for ResponseRoutingWire {
243 fn from(value: ResponseRouting) -> Self {
244 match value {
245 ResponseRouting::ReplayOriginalTool => Self::ReplayOriginalTool {
246 state_patches: Vec::new(),
247 },
248 ResponseRouting::UseAsToolResult => Self::UseAsToolResult,
249 ResponseRouting::PassToLLM => Self::PassToLLM,
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::ResponseRouting;
257 use serde_json::json;
258
259 #[test]
260 fn replay_original_tool_serializes_without_state_patches() {
261 let value =
262 serde_json::to_value(ResponseRouting::ReplayOriginalTool).expect("serialize routing");
263 assert_eq!(value, json!({ "strategy": "replay_original_tool" }));
264 }
265
266 #[test]
267 fn replay_original_tool_deserializes_legacy_state_patches_shape() {
268 let value = json!({
269 "strategy": "replay_original_tool",
270 "state_patches": [{
271 "op": "set",
272 "path": ["permissions", "approved_calls", "call_1"],
273 "value": true
274 }]
275 });
276 let routing: ResponseRouting =
277 serde_json::from_value(value).expect("deserialize legacy replay routing");
278 assert_eq!(routing, ResponseRouting::ReplayOriginalTool);
279 }
280}