1mod capability;
10mod history;
11mod one_shot;
12mod pty_identity;
13mod resume;
14mod session_options;
15
16use gate4agent_types::{AdapterBinding, AdapterFamily, AdapterId, AdapterVerification, AgentId};
17use std::collections::BTreeMap;
18use std::sync::OnceLock;
19use thiserror::Error;
20
21pub use capability::{
22 capability_probe_plan, parse_capability_models, CapabilityProbeAdapterError,
23 CapabilityProbePlan, CAPABILITY_PROBE_OUTPUT_MAX_BYTES, CAPABILITY_PROBE_REVISION,
24};
25
26pub use history::{
27 history_source_variants, parse_history, HistoryAdapterError, HistoryDocument, HistoryMessage,
28 HistoryRole, HistorySession, HistorySourceLayout, HistorySourceVariant,
29 HISTORY_DOCUMENT_MAX_BYTES, HISTORY_MESSAGE_MAX_CHARS, HISTORY_METADATA_MAX_BYTES,
30 HISTORY_STORED_MESSAGES_MAX,
31};
32pub use one_shot::{
33 one_shot_spec, one_shot_specs, resolve_one_shot_plan,
34 resolve_one_shot_plan_with_persistence, OneShotAdapterError, OneShotAdapterSpec,
35 OneShotModelSource, OneShotModelSpec, OneShotPlan, OneShotPromptDelivery,
36 OneShotSessionPersistence, OneShotThinkingLevel, CLAUDE_CODE_INLINE_REVISION,
37 CODEX_CLI_INLINE_REVISION, KIMI_CODE_INLINE_REVISION, ONE_SHOT_OUTPUT_MAX_BYTES,
38 ONE_SHOT_REVISION, ONE_SHOT_THINKING_OPTION_ID, ONE_SHOT_TIMEOUT_SECONDS,
39};
40pub use pty_identity::{
41 CodexPtySessionIdentityExtractor, KimiPtySessionIdentityExtractor,
42 KIMI_PTY_SESSION_ID_MAX_BYTES,
43};
44pub use resume::{
45 build_resume_plan, build_resume_plan_for_identity, ResumeAdapterError, ResumePlan,
46 RESUME_SESSION_ID_MAX_BYTES,
47};
48pub use session_options::{
49 merge_session_option_models, parse_session_option_models, plan_mid_session_action,
50 plan_mid_session_option, resolve_session_option_launch, session_option_catalog,
51 AgentSessionOptionCatalog, ResolvedSessionOptionLaunch, SessionOption,
52 SessionOptionAdapterError, SessionOptionApply, SessionOptionArgumentOverride,
53 SessionOptionCategory, SessionOptionChoice, SessionOptionInteractionDetection,
54 SessionOptionKind, SessionOptionLaunchApplication, SessionOptionMidSessionApplication,
55 SessionOptionMidSessionPlan, SessionOptionModel, SessionOptionModelListSpec,
56 SESSION_OPTION_CATALOG_REVISION, SESSION_OPTION_MODELS_MAX,
57 SESSION_OPTION_MODEL_LIST_MAX_BYTES,
58};
59
60pub const BUILTIN_ADAPTER_REVISION: &str = "gate4agent-adapter/v1";
61
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct AdapterDescriptor {
64 pub family: AdapterFamily,
65 pub binding: AdapterBinding,
66 pub agents: Vec<AgentId>,
67}
68
69#[derive(Clone, Debug, Default)]
75pub struct AdapterRegistry {
76 descriptors: BTreeMap<(AdapterFamily, AdapterId), AdapterDescriptor>,
77}
78
79impl AdapterRegistry {
80 pub fn new(
81 descriptors: impl IntoIterator<Item = AdapterDescriptor>,
82 ) -> Result<Self, AdapterRegistryError> {
83 let mut registry = Self::default();
84 for descriptor in descriptors {
85 registry.insert(descriptor)?;
86 }
87 Ok(registry)
88 }
89
90 pub fn insert(&mut self, descriptor: AdapterDescriptor) -> Result<(), AdapterRegistryError> {
91 descriptor
92 .binding
93 .validate()
94 .map_err(|error| AdapterRegistryError::InvalidBinding {
95 family: descriptor.family,
96 adapter_id: descriptor.binding.id.clone(),
97 message: error.to_string(),
98 })?;
99 if descriptor.agents.is_empty() {
100 return Err(AdapterRegistryError::MissingAgents {
101 family: descriptor.family,
102 adapter_id: descriptor.binding.id,
103 });
104 }
105 let key = (descriptor.family, descriptor.binding.id.clone());
106 if self.descriptors.contains_key(&key) {
107 return Err(AdapterRegistryError::Duplicate {
108 family: key.0,
109 adapter_id: key.1,
110 });
111 }
112 self.descriptors.insert(key, descriptor);
113 Ok(())
114 }
115
116 pub fn get(&self, family: AdapterFamily, id: &AdapterId) -> Option<&AdapterDescriptor> {
117 self.descriptors.get(&(family, id.clone()))
118 }
119
120 pub fn binding(&self, family: AdapterFamily, id: &str) -> Option<&AdapterBinding> {
121 self.descriptors
122 .get(&(family, AdapterId::new(id).ok()?))
123 .map(|descriptor| &descriptor.binding)
124 }
125
126 pub fn supports(&self, family: AdapterFamily, binding: &AdapterBinding) -> bool {
127 self.get(family, &binding.id)
128 .is_some_and(|descriptor| descriptor.binding.revision == binding.revision)
129 }
130
131 pub fn iter(&self) -> impl ExactSizeIterator<Item = &AdapterDescriptor> {
132 self.descriptors.values()
133 }
134}
135
136#[derive(Clone, Debug, Default)]
143pub struct AdapterRuntimeRegistry<T> {
144 runtimes: BTreeMap<(AdapterFamily, AdapterId), AdapterRuntime<T>>,
145}
146
147#[derive(Clone, Debug)]
148struct AdapterRuntime<T> {
149 binding: AdapterBinding,
150 implementation: T,
151}
152
153impl<T> AdapterRuntimeRegistry<T> {
154 pub fn insert(
155 &mut self,
156 family: AdapterFamily,
157 binding: AdapterBinding,
158 implementation: T,
159 ) -> Result<(), AdapterRuntimeRegistryError> {
160 binding
161 .validate()
162 .map_err(|error| AdapterRuntimeRegistryError::InvalidBinding {
163 family,
164 adapter_id: binding.id.clone(),
165 message: error.to_string(),
166 })?;
167 let key = (family, binding.id.clone());
168 if self.runtimes.contains_key(&key) {
169 return Err(AdapterRuntimeRegistryError::Duplicate {
170 family,
171 adapter_id: binding.id,
172 });
173 }
174 self.runtimes.insert(
175 key,
176 AdapterRuntime {
177 binding,
178 implementation,
179 },
180 );
181 Ok(())
182 }
183
184 pub fn resolve(
185 &self,
186 family: AdapterFamily,
187 binding: &AdapterBinding,
188 ) -> Result<&T, AdapterRuntimeRegistryError> {
189 let Some(runtime) = self.runtimes.get(&(family, binding.id.clone())) else {
190 return Err(AdapterRuntimeRegistryError::Unavailable {
191 family,
192 adapter_id: binding.id.clone(),
193 revision: binding.revision.clone(),
194 });
195 };
196 if runtime.binding.revision != binding.revision {
197 return Err(AdapterRuntimeRegistryError::RevisionMismatch {
198 family,
199 adapter_id: binding.id.clone(),
200 requested: binding.revision.clone(),
201 available: runtime.binding.revision.clone(),
202 });
203 }
204 Ok(&runtime.implementation)
205 }
206
207 pub fn len(&self) -> usize {
208 self.runtimes.len()
209 }
210
211 pub fn is_empty(&self) -> bool {
212 self.runtimes.is_empty()
213 }
214}
215
216#[derive(Clone, Debug, Error, Eq, PartialEq)]
217pub enum AdapterRegistryError {
218 #[error("duplicate {family:?} adapter ID: {adapter_id}")]
219 Duplicate {
220 family: AdapterFamily,
221 adapter_id: AdapterId,
222 },
223 #[error("{family:?} adapter {adapter_id} has no bound agents")]
224 MissingAgents {
225 family: AdapterFamily,
226 adapter_id: AdapterId,
227 },
228 #[error("invalid {family:?} adapter {adapter_id}: {message}")]
229 InvalidBinding {
230 family: AdapterFamily,
231 adapter_id: AdapterId,
232 message: String,
233 },
234}
235
236#[derive(Clone, Debug, Error, Eq, PartialEq)]
237pub enum AdapterRuntimeRegistryError {
238 #[error("duplicate runtime for {family:?} adapter {adapter_id}")]
239 Duplicate {
240 family: AdapterFamily,
241 adapter_id: AdapterId,
242 },
243 #[error("invalid runtime binding for {family:?} adapter {adapter_id}: {message}")]
244 InvalidBinding {
245 family: AdapterFamily,
246 adapter_id: AdapterId,
247 message: String,
248 },
249 #[error("runtime unavailable for {family:?} adapter {adapter_id} at revision {revision}")]
250 Unavailable {
251 family: AdapterFamily,
252 adapter_id: AdapterId,
253 revision: String,
254 },
255 #[error(
256 "runtime revision mismatch for {family:?} adapter {adapter_id}: requested {requested}, available {available}"
257 )]
258 RevisionMismatch {
259 family: AdapterFamily,
260 adapter_id: AdapterId,
261 requested: String,
262 available: String,
263 },
264}
265
266pub fn builtin_adapter_registry() -> &'static AdapterRegistry {
267 static REGISTRY: OnceLock<AdapterRegistry> = OnceLock::new();
268 REGISTRY.get_or_init(|| {
269 AdapterRegistry::new(builtin_descriptors())
270 .expect("built-in provider adapter registry must be valid")
271 })
272}
273
274fn builtin_descriptors() -> Vec<AdapterDescriptor> {
275 let mut descriptors = Vec::new();
276 for id in ["claude-code", "codex", "kimi"] {
277 descriptors.push(descriptor(AdapterFamily::PtySemantic, id));
278 }
279 for id in ["claude-code", "codex", "kimi"] {
280 descriptors.push(descriptor(AdapterFamily::Pipe, id));
281 }
282 for id in ["claude", "codex", "kimi"] {
283 let (revision, verification) = match id {
284 "claude" => (CLAUDE_CODE_INLINE_REVISION, AdapterVerification::VendorCanary),
285 "codex" => (CODEX_CLI_INLINE_REVISION, AdapterVerification::VendorCanary),
286 "kimi" => (KIMI_CODE_INLINE_REVISION, AdapterVerification::VendorCanary),
287 _ => (ONE_SHOT_REVISION, AdapterVerification::SyntheticFixture),
288 };
289 descriptors.push(descriptor_with_revision_and_verification(
290 AdapterFamily::OneShot,
291 id,
292 revision,
293 verification,
294 ));
295 }
296 for id in ["claude-code", "codex", "grok", "kimi"] {
297 descriptors.push(descriptor(AdapterFamily::Acp, id));
298 }
299 for id in ["claude-code", "codex", "grok", "kimi"] {
300 descriptors.push(descriptor(AdapterFamily::History, id));
301 descriptors.push(descriptor(AdapterFamily::Resume, id));
302 }
303 for id in ["claude-code", "codex"] {
304 descriptors.push(descriptor_with_revision(
305 AdapterFamily::SessionOptions,
306 id,
307 SESSION_OPTION_CATALOG_REVISION,
308 ));
309 }
310 descriptors
311}
312
313fn descriptor(family: AdapterFamily, id: &str) -> AdapterDescriptor {
314 descriptor_with_revision(family, id, BUILTIN_ADAPTER_REVISION)
315}
316
317fn descriptor_with_revision(family: AdapterFamily, id: &str, revision: &str) -> AdapterDescriptor {
318 descriptor_with_revision_and_verification(
319 family,
320 id,
321 revision,
322 AdapterVerification::SyntheticFixture,
323 )
324}
325
326fn descriptor_with_revision_and_verification(
327 family: AdapterFamily,
328 id: &str,
329 revision: &str,
330 verification: AdapterVerification,
331) -> AdapterDescriptor {
332 let agent_id = if id == "claude-code" { "claude" } else { id };
333 AdapterDescriptor {
334 family,
335 binding: AdapterBinding::new(
336 AdapterId::new(id).expect("hardcoded adapter ID"),
337 revision,
338 verification,
339 )
340 .expect("hardcoded adapter binding"),
341 agents: vec![AgentId::new(agent_id).expect("hardcoded agent ID")],
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn family_is_part_of_the_registry_key() {
351 let registry = builtin_adapter_registry();
352 let claude = AdapterId::new("claude-code").unwrap();
353 assert!(registry.get(AdapterFamily::PtySemantic, &claude).is_some());
354 assert!(registry.get(AdapterFamily::Pipe, &claude).is_some());
355 assert!(registry.get(AdapterFamily::Acp, &claude).is_some());
356 assert!(registry.get(AdapterFamily::History, &claude).is_some());
357 for id in ["codex", "kimi"] {
358 let id = AdapterId::new(id).unwrap();
359 assert!(registry.get(AdapterFamily::PtySemantic, &id).is_some());
360 assert!(registry.get(AdapterFamily::Pipe, &id).is_some());
361 assert!(registry.get(AdapterFamily::Acp, &id).is_some());
362 assert!(registry.get(AdapterFamily::History, &id).is_some());
363 }
364 let grok = AdapterId::new("grok").unwrap();
365 assert!(registry.get(AdapterFamily::PtySemantic, &grok).is_none());
366 assert!(registry.get(AdapterFamily::Pipe, &grok).is_none());
367 assert!(registry.get(AdapterFamily::Acp, &grok).is_some());
368 assert!(registry.get(AdapterFamily::History, &grok).is_some());
369 }
370
371 #[test]
375 fn hook_families_are_retired_and_unregistered() {
376 let registry = builtin_adapter_registry();
377 assert_eq!(
378 registry
379 .iter()
380 .filter(|descriptor| descriptor.family == AdapterFamily::Hook
381 || descriptor.family == AdapterFamily::ManagedHook)
382 .count(),
383 0
384 );
385 }
386
387 #[test]
388 fn history_registry_matches_the_fleet_source_inventory() {
389 let actual = builtin_adapter_registry()
390 .iter()
391 .filter(|descriptor| descriptor.family == AdapterFamily::History)
392 .map(|descriptor| descriptor.binding.id.as_str())
393 .collect::<std::collections::BTreeSet<_>>();
394 let expected = ["claude-code", "codex", "grok", "kimi"]
395 .into_iter()
396 .collect::<std::collections::BTreeSet<_>>();
397 assert_eq!(actual, expected);
398 }
399
400 #[test]
401 fn one_shot_registry_tracks_live_and_reference_contract_revisions() {
402 let actual = builtin_adapter_registry()
403 .iter()
404 .filter(|descriptor| descriptor.family == AdapterFamily::OneShot)
405 .map(|descriptor| {
406 (
407 descriptor.binding.id.as_str(),
408 descriptor.binding.revision.as_str(),
409 )
410 })
411 .collect::<std::collections::BTreeSet<_>>();
412 let expected = [
413 ("claude", CLAUDE_CODE_INLINE_REVISION),
414 ("codex", CODEX_CLI_INLINE_REVISION),
415 ("kimi", KIMI_CODE_INLINE_REVISION),
416 ]
417 .into_iter()
418 .collect();
419 assert_eq!(actual, expected);
420 for id in ["claude", "codex", "kimi"] {
421 assert_eq!(
422 builtin_adapter_registry()
423 .binding(AdapterFamily::OneShot, id)
424 .unwrap()
425 .verification,
426 AdapterVerification::VendorCanary,
427 "{id}"
428 );
429 }
430 }
431
432 #[test]
433 fn resume_registry_matches_the_supported_live_inventory() {
434 let actual = builtin_adapter_registry()
435 .iter()
436 .filter(|descriptor| descriptor.family == AdapterFamily::Resume)
437 .map(|descriptor| descriptor.binding.id.as_str())
438 .collect::<std::collections::BTreeSet<_>>();
439 let expected = ["claude-code", "codex", "grok", "kimi"].into_iter().collect();
440 assert_eq!(actual, expected);
441 }
442
443 #[test]
444 fn session_option_registry_matches_the_fleet_catalog_inventory() {
445 let actual = builtin_adapter_registry()
446 .iter()
447 .filter(|descriptor| descriptor.family == AdapterFamily::SessionOptions)
448 .map(|descriptor| {
449 (
450 descriptor.binding.id.as_str(),
451 descriptor.binding.revision.as_str(),
452 )
453 })
454 .collect::<std::collections::BTreeSet<_>>();
455 let expected = ["claude-code", "codex"]
456 .into_iter()
457 .map(|id| (id, SESSION_OPTION_CATALOG_REVISION))
458 .collect();
459 assert_eq!(actual, expected);
460 }
461
462 #[test]
463 fn capability_probe_registry_is_empty_for_the_current_fleet() {
464 let actual = builtin_adapter_registry()
465 .iter()
466 .filter(|descriptor| descriptor.family == AdapterFamily::CapabilityProbe)
467 .count();
468 assert_eq!(actual, 0);
469 }
470
471 #[test]
472 fn binding_revision_must_match_the_registered_implementation() {
473 let registry = builtin_adapter_registry();
474 let binding = AdapterBinding::new(
475 AdapterId::new("codex").unwrap(),
476 "other-revision",
477 AdapterVerification::Reference,
478 )
479 .unwrap();
480 assert!(!registry.supports(AdapterFamily::Pipe, &binding));
481 }
482
483 #[test]
484 fn runtime_resolution_is_family_and_revision_exact() {
485 let binding = builtin_adapter_registry()
486 .binding(AdapterFamily::Pipe, "codex")
487 .unwrap()
488 .clone();
489 let mut registry = AdapterRuntimeRegistry::default();
490 registry
491 .insert(AdapterFamily::Pipe, binding.clone(), "pipe-runtime")
492 .unwrap();
493
494 assert_eq!(
495 registry.resolve(AdapterFamily::Pipe, &binding).unwrap(),
496 &"pipe-runtime"
497 );
498 assert!(matches!(
499 registry.resolve(AdapterFamily::PtySemantic, &binding),
500 Err(AdapterRuntimeRegistryError::Unavailable { .. })
501 ));
502
503 let other_revision =
504 AdapterBinding::new(binding.id, "other-revision", AdapterVerification::Reference)
505 .unwrap();
506 assert!(matches!(
507 registry.resolve(AdapterFamily::Pipe, &other_revision),
508 Err(AdapterRuntimeRegistryError::RevisionMismatch { .. })
509 ));
510 }
511}