1use serde::{Deserialize, Serialize};
13
14use crate::protocol::{Request, Response};
15use crate::types::{Tool, ToolResultContent};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(tag = "type", rename_all = "snake_case")]
23#[allow(clippy::large_enum_variant)]
24pub enum PluginRequest {
25 Init {
27 cwd: String,
28 session_id: String,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 project_name: Option<String>,
32 },
33 Hook {
35 name: String,
36 data: serde_json::Value,
37 },
38 ToolCall {
40 tool_call_id: String,
41 name: String,
42 arguments: serde_json::Value,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 cwd: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 session_id: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 project_name: Option<String>,
52 },
53 CancelToolCall { tool_call_id: String },
58 SessionStart {
60 cwd: String,
61 session_id: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 project_name: Option<String>,
65 },
66 Idle,
68 ServerResponse {
71 request_id: String,
72 response: Response,
73 },
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(tag = "type", rename_all = "snake_case")]
82pub enum PluginMessage {
83 Register(PluginRegistration),
85 HookResult(HookResult),
87 ToolResult(PluginToolResult),
89 OutputDelta { tool_call_id: String, text: String },
91 ServerRequest {
95 request_id: String,
96 request: Request,
97 },
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PluginRegistration {
102 pub name: String,
104 #[serde(default)]
106 pub tools: Vec<PluginToolDef>,
107 #[serde(default)]
109 pub hooks: Vec<String>,
110 #[serde(default)]
112 pub commands: Vec<PluginCommand>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct PluginToolDef {
117 pub name: String,
119 pub description: String,
121 pub parameters: serde_json::Value,
123 #[serde(default)]
125 pub prompt_snippet: Option<String>,
126 #[serde(default)]
128 pub prompt_guidelines: Vec<String>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct PluginCommand {
133 pub name: String,
135 pub description: String,
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140pub struct HookResult {
141 #[serde(default)]
143 pub message: Option<HookMessage>,
144 #[serde(default)]
146 pub system_prompt: Option<String>,
147 #[serde(default)]
149 pub tool_result_append: Option<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct HookMessage {
154 pub content: String,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct PluginToolResult {
160 pub tool_call_id: String,
161 pub content: Vec<ToolResultContent>,
162 pub is_error: bool,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub summary: Option<String>,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
169 pub post_persist_actions: Vec<crate::types::PostPersistAction>,
170}
171
172impl From<&PluginToolDef> for Tool {
174 fn from(def: &PluginToolDef) -> Self {
175 Tool {
176 name: def.name.clone(),
177 description: def.description.clone(),
178 parameters: def.parameters.clone(),
179 }
180 }
181}
182
183impl From<&PluginToolDef> for crate::tool_prompt::ToolPrompt {
185 fn from(def: &PluginToolDef) -> Self {
186 crate::tool_prompt::ToolPrompt {
187 name: def.name.clone(),
188 snippet: def.prompt_snippet.clone().unwrap_or_default(),
189 guidelines: def.prompt_guidelines.clone(),
190 }
191 }
192}
193
194#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn cancel_tool_call_roundtrip() {
204 let req = PluginRequest::CancelToolCall {
206 tool_call_id: "tc-42".into(),
207 };
208 let json = serde_json::to_string(&req).expect("serialize");
209 assert_eq!(
210 json,
211 r#"{"type":"cancel_tool_call","tool_call_id":"tc-42"}"#
212 );
213
214 let parsed: PluginRequest = serde_json::from_str(&json).expect("deserialize");
215 match parsed {
216 PluginRequest::CancelToolCall { tool_call_id } => {
217 assert_eq!(tool_call_id, "tc-42");
218 }
219 other => panic!("expected CancelToolCall, got {other:?}"),
220 }
221 }
222
223 #[test]
224 fn tool_call_roundtrip_unchanged_by_cancel_variant() {
225 let req = PluginRequest::ToolCall {
228 tool_call_id: "tc-1".into(),
229 name: "bash".into(),
230 arguments: serde_json::json!({"command": "echo hi"}),
231 cwd: Some("/tmp".into()),
232 session_id: None,
233 project_name: None,
234 };
235 let json = serde_json::to_string(&req).expect("serialize");
236 assert!(json.contains(r#""type":"tool_call""#));
237 assert!(json.contains(r#""tool_call_id":"tc-1""#));
238 assert!(json.contains(r#""name":"bash""#));
239 assert!(json.contains(r#""cwd":"/tmp""#));
240 assert!(!json.contains("session_id"));
242 assert!(!json.contains("project_name"));
243 }
244}