use serde::{Deserialize, Serialize};
use crate::protocol::{Request, Response};
use crate::types::{Tool, ToolResultContent};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum PluginRequest {
Init {
cwd: String,
session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
project_name: Option<String>,
},
Hook {
name: String,
data: serde_json::Value,
},
ToolCall {
tool_call_id: String,
name: String,
arguments: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
project_name: Option<String>,
},
CancelToolCall { tool_call_id: String },
SessionStart {
cwd: String,
session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
project_name: Option<String>,
},
Idle,
ServerResponse {
request_id: String,
response: Response,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PluginMessage {
Register(PluginRegistration),
HookResult(HookResult),
ToolResult(PluginToolResult),
OutputDelta { tool_call_id: String, text: String },
ServerRequest {
request_id: String,
request: Request,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginRegistration {
pub name: String,
#[serde(default)]
pub tools: Vec<PluginToolDef>,
#[serde(default)]
pub hooks: Vec<String>,
#[serde(default)]
pub commands: Vec<PluginCommand>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginToolDef {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
#[serde(default)]
pub prompt_snippet: Option<String>,
#[serde(default)]
pub prompt_guidelines: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginCommand {
pub name: String,
pub description: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HookResult {
#[serde(default)]
pub message: Option<HookMessage>,
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default)]
pub tool_result_append: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookMessage {
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginToolResult {
pub tool_call_id: String,
pub content: Vec<ToolResultContent>,
pub is_error: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub post_persist_actions: Vec<crate::types::PostPersistAction>,
}
impl From<&PluginToolDef> for Tool {
fn from(def: &PluginToolDef) -> Self {
Tool {
name: def.name.clone(),
description: def.description.clone(),
parameters: def.parameters.clone(),
}
}
}
impl From<&PluginToolDef> for crate::tool_prompt::ToolPrompt {
fn from(def: &PluginToolDef) -> Self {
crate::tool_prompt::ToolPrompt {
name: def.name.clone(),
snippet: def.prompt_snippet.clone().unwrap_or_default(),
guidelines: def.prompt_guidelines.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancel_tool_call_roundtrip() {
let req = PluginRequest::CancelToolCall {
tool_call_id: "tc-42".into(),
};
let json = serde_json::to_string(&req).expect("serialize");
assert_eq!(
json,
r#"{"type":"cancel_tool_call","tool_call_id":"tc-42"}"#
);
let parsed: PluginRequest = serde_json::from_str(&json).expect("deserialize");
match parsed {
PluginRequest::CancelToolCall { tool_call_id } => {
assert_eq!(tool_call_id, "tc-42");
}
other => panic!("expected CancelToolCall, got {other:?}"),
}
}
#[test]
fn tool_call_roundtrip_unchanged_by_cancel_variant() {
let req = PluginRequest::ToolCall {
tool_call_id: "tc-1".into(),
name: "bash".into(),
arguments: serde_json::json!({"command": "echo hi"}),
cwd: Some("/tmp".into()),
session_id: None,
project_name: None,
};
let json = serde_json::to_string(&req).expect("serialize");
assert!(json.contains(r#""type":"tool_call""#));
assert!(json.contains(r#""tool_call_id":"tc-1""#));
assert!(json.contains(r#""name":"bash""#));
assert!(json.contains(r#""cwd":"/tmp""#));
assert!(!json.contains("session_id"));
assert!(!json.contains("project_name"));
}
}