use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::env::{EnvPolicy, EnvSource};
use crate::template::{contains_placeholder, contains_placeholder_text};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LaneKind {
Acp,
Rpc,
Cli,
Serve,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PromptChannel {
#[default]
ArgOrStdin,
Stdin,
ArgOnly,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StreamMode {
#[default]
Stdio,
Pty,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BindingReceiptShape {
#[default]
SessionId,
SessionIdAndUsage,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CliLaneSpec {
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub prompt_channel: PromptChannel,
#[serde(default)]
pub stream: StreamMode,
#[serde(default)]
pub resume_args: Vec<String>,
#[serde(default)]
pub resume_spawn_args: Vec<String>,
#[serde(default)]
pub stdin_template: Option<serde_json::Value>,
#[serde(default)]
pub json_lines: bool,
#[serde(default)]
pub session_id_path: Option<String>,
#[serde(default)]
pub usage_path: Option<String>,
#[serde(default)]
pub open_args: Vec<String>,
#[serde(default)]
pub binding_file_env: Option<String>,
#[serde(default)]
pub binding_receipt_shape: BindingReceiptShape,
#[serde(default)]
pub result_selector: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result_selector_field: Option<String>,
#[serde(default)]
pub result_path: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub execution_modes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub permission_modes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effort_model_suffixes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mcp_servers: Vec<serde_json::Value>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RpcCallSpec {
pub method: String,
#[serde(default)]
pub params: serde_json::Value,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RpcLaneSpec {
#[serde(default)]
pub initialize: Option<RpcCallSpec>,
#[serde(default)]
pub initialized: Option<String>,
#[serde(default)]
pub session_open: Option<RpcCallSpec>,
#[serde(default)]
pub session_resume: Option<RpcCallSpec>,
pub prompt: RpcCallSpec,
#[serde(default)]
pub completion: Option<RpcCompletionSpec>,
#[serde(default)]
pub cancel: Option<RpcCallSpec>,
#[serde(default)]
pub session_close: Option<RpcCallSpec>,
#[serde(default)]
pub text_event_method: Option<String>,
#[serde(default)]
pub text_path: Option<String>,
#[serde(default)]
pub result_text_path: Option<String>,
#[serde(default)]
pub pre_turn: Vec<RpcCallSpec>,
#[serde(default)]
pub session_scope: SessionScope,
#[serde(default)]
pub kill_on_cancel: bool,
#[serde(default)]
pub envelope: Option<RpcEnvelopeSpec>,
#[serde(default)]
pub passthrough: Vec<String>,
#[serde(default)]
pub transport: RpcTransport,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_frame_bytes: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure_value: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure_message_path: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mcp_servers: Vec<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mcp_config: Option<RpcMcpConfig>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RpcMcpConfig {
pub flag: String,
pub root: String,
#[serde(default)]
pub env_vars_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools_approval: Option<RpcMcpToolApproval>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools_approval_servers: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RpcMcpToolApproval {
pub key: String,
pub value: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RpcTransport {
#[default]
Stdio,
WebSocket {
announce: String,
},
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SessionScope {
#[default]
Agent,
Process,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RpcEnvelopeSpec {
pub method_field: String,
#[serde(default)]
pub string_ids: bool,
#[serde(default)]
pub params_inline: bool,
#[serde(default)]
pub response_method: Option<String>,
#[serde(default)]
pub data_field: Option<String>,
#[serde(default)]
pub result_field: Option<String>,
#[serde(default)]
pub error_field: Option<String>,
#[serde(default)]
pub success_field: Option<String>,
#[serde(default)]
pub command_field: Option<String>,
#[serde(default)]
pub discriminant_field: Option<String>,
#[serde(default)]
pub request_marker: Option<String>,
#[serde(default)]
pub notification_marker: Option<String>,
#[serde(default)]
pub const_fields: std::collections::BTreeMap<String, String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RpcCompletionSpec {
pub method: String,
#[serde(default)]
pub turn_id_path: Option<String>,
#[serde(default)]
pub status_path: Option<String>,
#[serde(default)]
pub status_value: Option<String>,
}
impl CliLaneSpec {
pub fn capabilities(&self) -> Capabilities {
Capabilities {
streaming: true,
cancellation: true,
session_resume: !self.resume_args.is_empty()
|| !self.resume_spawn_args.is_empty()
|| self.args.iter().any(|arg| arg.contains("{sessionId}")),
permission_prompts: false,
detail: None,
}
}
}
impl RpcLaneSpec {
pub fn capabilities(&self) -> Capabilities {
Capabilities {
streaming: self.text_event_method.is_some(),
cancellation: self.cancel.is_some() || self.kill_on_cancel,
session_resume: self.session_resume.is_some()
|| self.session_scope == SessionScope::Process,
permission_prompts: false,
detail: None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "lane", rename_all = "kebab-case")]
pub enum LaneSpec {
Acp {
#[serde(default)]
mcp_servers: Vec<serde_json::Value>,
#[serde(default)]
prefer_load: bool,
#[serde(default)]
auth_key: Option<EnvSource>,
#[serde(default)]
enforced_permission: Option<String>,
},
Rpc(Box<RpcLaneSpec>),
Cli(Box<CliLaneSpec>),
Serve {
#[serde(default)]
mcp_servers: Vec<serde_json::Value>,
},
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CommandSpec {
pub program: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub cwd: Option<PathBuf>,
#[serde(default)]
pub env: EnvPolicy,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentSpec {
pub id: String,
#[serde(default)]
pub client_name: Option<String>,
#[serde(flatten)]
pub lane: LaneSpec,
pub command: CommandSpec,
#[serde(default)]
pub max_output_bytes: Option<usize>,
}
impl AgentSpec {
pub fn selector_support(&self, phase: SelectorPhase) -> SelectorSupportSet {
self.lane.selector_support(phase)
}
}
impl LaneSpec {
pub fn selector_support(&self, phase: SelectorPhase) -> SelectorSupportSet {
match self {
Self::Acp { .. } => match phase {
SelectorPhase::Session => SelectorSupportSet::negotiated(),
SelectorPhase::Turn => SelectorSupportSet {
model: SelectorSupport::Negotiated,
effort: SelectorSupport::Negotiated,
mode: SelectorSupport::Unsupported,
permission: SelectorSupport::Unsupported,
},
},
Self::Cli(spec) => {
let prompt_via_stdin = match spec.prompt_channel {
PromptChannel::Stdin => true,
PromptChannel::ArgOnly => false,
PromptChannel::ArgOrStdin => !spec
.args
.iter()
.any(|argument| contains_placeholder_text(argument, "prompt")),
};
let consumes = |names: &[&str]| {
names.iter().any(|name| {
spec.args
.iter()
.any(|argument| contains_placeholder_text(argument, name))
|| (prompt_via_stdin
&& spec
.stdin_template
.as_ref()
.is_some_and(|template| contains_placeholder(template, name)))
})
};
let model = consumes(&["model", "modelProvider", "modelId"]);
let effort =
consumes(&["effort"]) || (model && !spec.effort_model_suffixes.is_empty());
match phase {
SelectorPhase::Session => SelectorSupportSet {
model: selector_support(model),
effort: selector_support(effort),
mode: selector_support(consumes(&["mode"])),
permission: selector_support(consumes(&["permission"])),
},
SelectorPhase::Turn => SelectorSupportSet {
model: selector_support(model),
effort: selector_support(effort),
mode: SelectorSupport::Unsupported,
permission: SelectorSupport::Unsupported,
},
}
}
Self::Rpc(spec) => rpc_selector_support(spec, phase, None),
Self::Serve { .. } => match phase {
SelectorPhase::Session => SelectorSupportSet {
model: SelectorSupport::Applied,
effort: SelectorSupport::Unsupported,
mode: SelectorSupport::Unsupported,
permission: SelectorSupport::Negotiated,
},
SelectorPhase::Turn => SelectorSupportSet {
model: SelectorSupport::Applied,
effort: SelectorSupport::Unsupported,
mode: SelectorSupport::Unsupported,
permission: SelectorSupport::Unsupported,
},
},
}
}
}
impl RpcLaneSpec {
pub(crate) fn session_selector_support(&self, resuming: bool) -> SelectorSupportSet {
rpc_selector_support(self, SelectorPhase::Session, Some(resuming))
}
}
fn rpc_selector_support(
spec: &RpcLaneSpec,
phase: SelectorPhase,
resuming: Option<bool>,
) -> SelectorSupportSet {
let uses_call = |call: &RpcCallSpec, names: &[&str]| {
names
.iter()
.any(|name| contains_placeholder(&call.params, name))
};
let uses_session = |names: &[&str]| {
let setup = if spec.session_scope == SessionScope::Process {
false
} else {
match resuming {
Some(true) => spec
.session_resume
.as_ref()
.or(spec.session_open.as_ref())
.is_some_and(|call| uses_call(call, names)),
Some(false) => spec
.session_open
.as_ref()
.is_some_and(|call| uses_call(call, names)),
None => {
spec.session_open
.as_ref()
.is_some_and(|call| uses_call(call, names))
|| spec
.session_resume
.as_ref()
.is_some_and(|call| uses_call(call, names))
}
}
};
spec.initialize
.as_ref()
.is_some_and(|call| uses_call(call, names))
|| setup
};
let uses_turn = |names: &[&str]| {
uses_call(&spec.prompt, names) || spec.pre_turn.iter().any(|call| uses_call(call, names))
};
match phase {
SelectorPhase::Session => SelectorSupportSet {
model: selector_support(
uses_session(&["model", "modelProvider", "modelId"])
|| uses_turn(&["model", "modelProvider", "modelId"]),
),
effort: selector_support(uses_session(&["effort"]) || uses_turn(&["effort"])),
mode: selector_support(uses_session(&["mode"])),
permission: selector_support(uses_session(&["permission"])),
},
SelectorPhase::Turn => SelectorSupportSet {
model: selector_support(uses_turn(&["model", "modelProvider", "modelId"])),
effort: selector_support(uses_turn(&["effort"])),
mode: SelectorSupport::Unsupported,
permission: SelectorSupport::Unsupported,
},
}
}
fn selector_support(applied: bool) -> SelectorSupport {
if applied {
SelectorSupport::Applied
} else {
SelectorSupport::Unsupported
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SelectorPhase {
Session,
Turn,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SelectorSupport {
Applied,
Negotiated,
#[default]
Unsupported,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct SelectorSupportSet {
pub model: SelectorSupport,
pub effort: SelectorSupport,
pub mode: SelectorSupport,
pub permission: SelectorSupport,
}
impl SelectorSupportSet {
fn negotiated() -> Self {
Self {
model: SelectorSupport::Negotiated,
effort: SelectorSupport::Negotiated,
mode: SelectorSupport::Negotiated,
permission: SelectorSupport::Negotiated,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Capabilities {
pub streaming: bool,
pub cancellation: bool,
pub session_resume: bool,
pub permission_prompts: bool,
#[serde(default)]
pub detail: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum AgentEvent {
MessageChunk { text: String },
ToolCall {
summary: String,
raw: Option<serde_json::Value>,
},
Plan { raw: serde_json::Value },
Truncated,
Raw { raw: serde_json::Value },
Usage {
usage: TokenUsage,
#[serde(default)]
raw: Option<serde_json::Value>,
},
}
pub(crate) use token_usage_detector::agent::from_aliases as token_usage;
pub use token_usage_detector::TokenUsage;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TurnOutcome {
pub stop_reason: String,
pub text: String,
pub truncated: bool,
#[serde(default)]
pub exit_code: Option<i32>,
#[serde(default)]
pub usage: Option<TokenUsage>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentInfo {
pub name: String,
pub version: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SessionConfig {
pub model: Option<String>,
pub mode: Option<String>,
pub effort: Option<String>,
pub permission: Option<String>,
pub instructions: Option<String>,
pub provider: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct SessionRequest {
pub resume: Option<String>,
pub cwd: Option<PathBuf>,
pub env: BTreeMap<String, String>,
pub config: SessionConfig,
}
#[derive(Clone, Debug, Default)]
pub struct TurnRequest {
pub text: String,
pub model: Option<String>,
pub effort: Option<String>,
}