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