1use crate::{normalize_executable_name, AdapterBinding, AgentId, AgentReadinessSpec};
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
6#[serde(rename_all = "kebab-case")]
7pub enum RuntimePlatform {
8 Windows,
9 MacOs,
10 Linux,
11 Wsl,
12}
13
14impl RuntimePlatform {
15 pub fn current() -> Self {
16 if cfg!(target_os = "windows") {
17 Self::Windows
18 } else if cfg!(target_os = "macos") {
19 Self::MacOs
20 } else {
21 Self::Linux
22 }
23 }
24}
25
26#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
27pub struct DetectionSpec {
28 pub command: String,
29 #[serde(default)]
30 pub aliases: Vec<String>,
31 #[serde(default)]
32 pub required_commands: Vec<String>,
33 #[serde(default)]
34 pub unsupported_platforms: Vec<RuntimePlatform>,
35}
36
37#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
38pub struct LaunchSpec {
39 pub program: String,
41 #[serde(default)]
42 pub fixed_args: Vec<String>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46#[serde(tag = "kind", rename_all = "kebab-case")]
47pub enum ProcessMatcher {
48 Exact { name: String },
49 Prefix { prefix: String },
50}
51
52impl ProcessMatcher {
53 pub fn matches(&self, process_or_path: &str, platform: RuntimePlatform) -> bool {
54 let process = normalize_executable_name(process_or_path, platform);
55 match self {
56 Self::Exact { name } => {
57 let expected = normalize_executable_name(name, platform);
58 process == expected
59 }
60 Self::Prefix { prefix } => {
61 let expected = if platform == RuntimePlatform::Windows {
62 prefix.to_ascii_lowercase()
63 } else {
64 prefix.clone()
65 };
66 process.starts_with(&expected)
67 }
68 }
69 }
70}
71
72#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
73#[serde(tag = "kind", rename_all = "kebab-case")]
74pub enum InitialPromptMode {
75 None,
76 Positional { option_terminator: bool },
77 Flag { flag: String },
78 InteractiveFlag { flag: String },
79 AgentNativeQuery,
80 AfterReady,
81}
82
83#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
84#[serde(tag = "kind", rename_all = "kebab-case")]
85pub enum NativeDraftMode {
86 Flag { flag: String },
87}
88
89#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
90pub struct PromptSpec {
91 pub initial: InitialPromptMode,
92 #[serde(default)]
93 pub native_draft: Option<NativeDraftMode>,
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
97#[serde(rename_all = "kebab-case")]
98pub enum AgentCommandMode {
99 SlashLine,
100}
101
102#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
103pub struct AgentCapabilities {
104 #[serde(default)]
105 pub agent_commands: Option<AgentCommandMode>,
106 #[serde(default)]
107 pub transports: AgentTransportCapabilities,
108 #[serde(default)]
109 pub adapters: AgentAdapterCapabilities,
110}
111
112#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
113pub struct AgentAdapterCapabilities {
114 #[serde(default)]
117 pub pty_sidecar: Option<AdapterBinding>,
118 #[serde(default)]
119 pub hook: Option<AdapterBinding>,
120 #[serde(default)]
123 pub managed_hook: Option<AdapterBinding>,
124 #[serde(default)]
127 pub one_shot: Option<AdapterBinding>,
128 #[serde(default)]
129 pub history: Option<AdapterBinding>,
130 #[serde(default)]
131 pub resume: Option<AdapterBinding>,
132 #[serde(default)]
133 pub session_options: Option<AdapterBinding>,
134 #[serde(default)]
135 pub capability_probe: Option<AdapterBinding>,
136}
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
139#[serde(rename_all = "kebab-case")]
140pub enum PipePromptDelivery {
141 None,
142 StdinClose,
143 Positional,
144}
145
146#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "kebab-case")]
148pub enum PipeProtocol {
149 #[default]
150 SemanticNdjson,
151 StructuredJsonl,
155 OneShotText,
156}
157
158#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
161pub struct PipeTransportSpec {
162 pub adapter: AdapterBinding,
163 #[serde(default)]
164 pub protocol: PipeProtocol,
165 #[serde(default)]
166 pub launch_override: Option<LaunchSpec>,
167 #[serde(default = "default_pipe_prompt_delivery")]
168 pub prompt_delivery: PipePromptDelivery,
169}
170
171fn default_pipe_prompt_delivery() -> PipePromptDelivery {
172 PipePromptDelivery::None
173}
174
175#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
176pub struct AcpTransportSpec {
177 pub adapter: AdapterBinding,
178 #[serde(default)]
179 pub launch_override: Option<LaunchSpec>,
180}
181
182#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
183pub struct AgentTransportCapabilities {
184 #[serde(default = "default_true")]
185 pub pty: bool,
186 #[serde(default)]
187 pub pty_adapter: Option<AdapterBinding>,
188 #[serde(default)]
189 pub pipe: Option<PipeTransportSpec>,
190 #[serde(default)]
191 pub acp: Option<AcpTransportSpec>,
192}
193
194impl Default for AgentTransportCapabilities {
195 fn default() -> Self {
196 Self {
197 pty: true,
198 pty_adapter: None,
199 pipe: None,
200 acp: None,
201 }
202 }
203}
204
205fn default_true() -> bool {
206 true
207}
208
209#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
214#[serde(rename_all = "kebab-case")]
215pub enum SpecVerification {
216 Gate4AgentVerified,
217 Reference,
218}
219
220#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
221pub struct AgentSpec {
222 pub id: AgentId,
223 pub revision: String,
225 pub display_name: String,
226 pub detection: DetectionSpec,
227 pub launch: LaunchSpec,
228 pub expected_processes: Vec<ProcessMatcher>,
229 pub prompt: PromptSpec,
230 pub readiness: AgentReadinessSpec,
231 #[serde(default)]
232 pub capabilities: AgentCapabilities,
233 pub verification: SpecVerification,
234}
235
236impl AgentSpec {
237 pub fn supports_platform(&self, platform: RuntimePlatform) -> bool {
238 !self.detection.unsupported_platforms.contains(&platform)
239 }
240}