use std::{fmt, path::PathBuf, sync::Arc};
use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use crate::ext::ExtRequest;
use crate::{ContentBlock, Error, ExtNotification, Plan, SessionId, ToolCall, ToolCallUpdate};
use crate::{ExtResponse, SessionModeId};
#[async_trait::async_trait(?Send)]
pub trait Client {
async fn request_permission(
&self,
args: RequestPermissionRequest,
) -> Result<RequestPermissionResponse, Error>;
async fn write_text_file(
&self,
args: WriteTextFileRequest,
) -> Result<WriteTextFileResponse, Error>;
async fn read_text_file(
&self,
args: ReadTextFileRequest,
) -> Result<ReadTextFileResponse, Error>;
async fn session_notification(&self, args: SessionNotification) -> Result<(), Error>;
async fn create_terminal(
&self,
args: CreateTerminalRequest,
) -> Result<CreateTerminalResponse, Error>;
async fn terminal_output(
&self,
args: TerminalOutputRequest,
) -> Result<TerminalOutputResponse, Error>;
async fn release_terminal(
&self,
args: ReleaseTerminalRequest,
) -> Result<ReleaseTerminalResponse, Error>;
async fn wait_for_terminal_exit(
&self,
args: WaitForTerminalExitRequest,
) -> Result<WaitForTerminalExitResponse, Error>;
async fn kill_terminal_command(
&self,
args: KillTerminalCommandRequest,
) -> Result<KillTerminalCommandResponse, Error>;
async fn ext_method(&self, args: ExtRequest) -> Result<ExtResponse, Error>;
async fn ext_notification(&self, args: ExtNotification) -> Result<(), Error>;
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(extend("x-side" = "client", "x-method" = SESSION_UPDATE_NOTIFICATION))]
#[serde(rename_all = "camelCase")]
pub struct SessionNotification {
pub session_id: SessionId,
pub update: SessionUpdate,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case", tag = "sessionUpdate")]
pub enum SessionUpdate {
UserMessageChunk { content: ContentBlock },
AgentMessageChunk { content: ContentBlock },
AgentThoughtChunk { content: ContentBlock },
ToolCall(ToolCall),
ToolCallUpdate(ToolCallUpdate),
Plan(Plan),
#[serde(rename_all = "camelCase")]
AvailableCommandsUpdate {
available_commands: Vec<AvailableCommand>,
},
#[serde(rename_all = "camelCase")]
CurrentModeUpdate { current_mode_id: SessionModeId },
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct AvailableCommand {
pub name: String,
pub description: String,
pub input: Option<AvailableCommandInput>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged, rename_all = "camelCase")]
pub enum AvailableCommandInput {
#[schemars(rename = "UnstructuredCommandInput")]
Unstructured {
hint: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME))]
#[serde(rename_all = "camelCase")]
pub struct RequestPermissionRequest {
pub session_id: SessionId,
pub tool_call: ToolCallUpdate,
pub options: Vec<PermissionOption>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PermissionOption {
#[serde(rename = "optionId")]
pub id: PermissionOptionId,
pub name: String,
pub kind: PermissionOptionKind,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct PermissionOptionId(pub Arc<str>);
impl fmt::Display for PermissionOptionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PermissionOptionKind {
AllowOnce,
AllowAlways,
RejectOnce,
RejectAlways,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME))]
#[serde(rename_all = "camelCase")]
pub struct RequestPermissionResponse {
pub outcome: RequestPermissionOutcome,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum RequestPermissionOutcome {
Cancelled,
#[serde(rename_all = "camelCase")]
Selected {
option_id: PermissionOptionId,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(extend("x-side" = "client", "x-method" = FS_WRITE_TEXT_FILE_METHOD_NAME))]
#[serde(rename_all = "camelCase")]
pub struct WriteTextFileRequest {
pub session_id: SessionId,
pub path: PathBuf,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = FS_WRITE_TEXT_FILE_METHOD_NAME))]
#[serde(default)]
pub struct WriteTextFileResponse {
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(extend("x-side" = "client", "x-method" = FS_READ_TEXT_FILE_METHOD_NAME))]
#[serde(rename_all = "camelCase")]
pub struct ReadTextFileRequest {
pub session_id: SessionId,
pub path: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(extend("x-side" = "client", "x-method" = FS_READ_TEXT_FILE_METHOD_NAME))]
#[serde(rename_all = "camelCase")]
pub struct ReadTextFileResponse {
pub content: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct TerminalId(pub Arc<str>);
impl std::fmt::Display for TerminalId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_CREATE_METHOD_NAME))]
pub struct CreateTerminalRequest {
pub session_id: SessionId,
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub env: Vec<crate::EnvVariable>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_byte_limit: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_CREATE_METHOD_NAME))]
pub struct CreateTerminalResponse {
pub terminal_id: TerminalId,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_OUTPUT_METHOD_NAME))]
pub struct TerminalOutputRequest {
pub session_id: SessionId,
pub terminal_id: TerminalId,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_OUTPUT_METHOD_NAME))]
pub struct TerminalOutputResponse {
pub output: String,
pub truncated: bool,
pub exit_status: Option<TerminalExitStatus>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_RELEASE_METHOD_NAME))]
pub struct ReleaseTerminalRequest {
pub session_id: SessionId,
pub terminal_id: TerminalId,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_RELEASE_METHOD_NAME))]
pub struct ReleaseTerminalResponse {
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_KILL_METHOD_NAME))]
pub struct KillTerminalCommandRequest {
pub session_id: SessionId,
pub terminal_id: TerminalId,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_KILL_METHOD_NAME))]
pub struct KillTerminalCommandResponse {
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_WAIT_FOR_EXIT_METHOD_NAME))]
pub struct WaitForTerminalExitRequest {
pub session_id: SessionId,
pub terminal_id: TerminalId,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("x-side" = "client", "x-method" = TERMINAL_WAIT_FOR_EXIT_METHOD_NAME))]
pub struct WaitForTerminalExitResponse {
#[serde(flatten)]
pub exit_status: TerminalExitStatus,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TerminalExitStatus {
pub exit_code: Option<u32>,
pub signal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ClientCapabilities {
#[serde(default)]
pub fs: FileSystemCapability,
#[serde(default)]
pub terminal: bool,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct FileSystemCapability {
#[serde(default)]
pub read_text_file: bool,
#[serde(default)]
pub write_text_file: bool,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientMethodNames {
pub session_request_permission: &'static str,
pub session_update: &'static str,
pub fs_write_text_file: &'static str,
pub fs_read_text_file: &'static str,
pub terminal_create: &'static str,
pub terminal_output: &'static str,
pub terminal_release: &'static str,
pub terminal_wait_for_exit: &'static str,
pub terminal_kill: &'static str,
}
pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames {
session_update: SESSION_UPDATE_NOTIFICATION,
session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME,
fs_write_text_file: FS_WRITE_TEXT_FILE_METHOD_NAME,
fs_read_text_file: FS_READ_TEXT_FILE_METHOD_NAME,
terminal_create: TERMINAL_CREATE_METHOD_NAME,
terminal_output: TERMINAL_OUTPUT_METHOD_NAME,
terminal_release: TERMINAL_RELEASE_METHOD_NAME,
terminal_wait_for_exit: TERMINAL_WAIT_FOR_EXIT_METHOD_NAME,
terminal_kill: TERMINAL_KILL_METHOD_NAME,
};
pub(crate) const SESSION_UPDATE_NOTIFICATION: &str = "session/update";
pub(crate) const SESSION_REQUEST_PERMISSION_METHOD_NAME: &str = "session/request_permission";
pub(crate) const FS_WRITE_TEXT_FILE_METHOD_NAME: &str = "fs/write_text_file";
pub(crate) const FS_READ_TEXT_FILE_METHOD_NAME: &str = "fs/read_text_file";
pub(crate) const TERMINAL_CREATE_METHOD_NAME: &str = "terminal/create";
pub(crate) const TERMINAL_OUTPUT_METHOD_NAME: &str = "terminal/output";
pub(crate) const TERMINAL_RELEASE_METHOD_NAME: &str = "terminal/release";
pub(crate) const TERMINAL_WAIT_FOR_EXIT_METHOD_NAME: &str = "terminal/wait_for_exit";
pub(crate) const TERMINAL_KILL_METHOD_NAME: &str = "terminal/kill";
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
#[schemars(extend("x-docs-ignore" = true))]
pub enum AgentRequest {
WriteTextFileRequest(WriteTextFileRequest),
ReadTextFileRequest(ReadTextFileRequest),
RequestPermissionRequest(RequestPermissionRequest),
CreateTerminalRequest(CreateTerminalRequest),
TerminalOutputRequest(TerminalOutputRequest),
ReleaseTerminalRequest(ReleaseTerminalRequest),
WaitForTerminalExitRequest(WaitForTerminalExitRequest),
KillTerminalCommandRequest(KillTerminalCommandRequest),
ExtMethodRequest(ExtRequest),
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
#[schemars(extend("x-docs-ignore" = true))]
pub enum ClientResponse {
WriteTextFileResponse(#[serde(default)] WriteTextFileResponse),
ReadTextFileResponse(ReadTextFileResponse),
RequestPermissionResponse(RequestPermissionResponse),
CreateTerminalResponse(CreateTerminalResponse),
TerminalOutputResponse(TerminalOutputResponse),
ReleaseTerminalResponse(#[serde(default)] ReleaseTerminalResponse),
WaitForTerminalExitResponse(WaitForTerminalExitResponse),
KillTerminalResponse(#[serde(default)] KillTerminalCommandResponse),
ExtMethodResponse(#[schemars(with = "serde_json::Value")] Arc<RawValue>),
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
#[schemars(extend("x-docs-ignore" = true))]
pub enum AgentNotification {
SessionNotification(SessionNotification),
ExtNotification(ExtNotification),
}