diff --git a/bt-daemon/src/agents/antigravity.rs b/bt-daemon/src/agents/antigravity.rs index 2e39511..1216b92 100644 --- a/bt-daemon/src/agents/antigravity.rs +++ b/bt-daemon/src/agents/antigravity.rs @@ -6,7 +6,7 @@ pub(crate) struct Antigravity; pub(crate) const IDENTITY: Identity = Identity { id: "antigravity", source: "antigravity", - aliases: &["agy"], + aliases: &[], display_name: "Google Antigravity", }; @@ -18,6 +18,7 @@ impl Agent for Antigravity { impl Register for Antigravity { fn register(self: Arc, registrar: &mut Registrar) { - registrar.setup.insert(self); + registrar.setup.insert(self.clone()); + registrar.translate.insert(self); } } diff --git a/bt-daemon/src/agents/claude.rs b/bt-daemon/src/agents/claude.rs index 359b22f..396f68a 100644 --- a/bt-daemon/src/agents/claude.rs +++ b/bt-daemon/src/agents/claude.rs @@ -19,6 +19,7 @@ impl Agent for Claude { impl Register for Claude { fn register(self: Arc, registrar: &mut Registrar) { registrar.setup.insert(self.clone()); - registrar.run.insert(self); + registrar.run.insert(self.clone()); + registrar.translate.insert(self); } } diff --git a/bt-daemon/src/agents/codex.rs b/bt-daemon/src/agents/codex.rs index 80741bf..75f9a74 100644 --- a/bt-daemon/src/agents/codex.rs +++ b/bt-daemon/src/agents/codex.rs @@ -19,6 +19,7 @@ impl Agent for Codex { impl Register for Codex { fn register(self: Arc, registrar: &mut Registrar) { registrar.setup.insert(self.clone()); - registrar.run.insert(self); + registrar.run.insert(self.clone()); + registrar.translate.insert(self); } } diff --git a/bt-daemon/src/agents/cursor.rs b/bt-daemon/src/agents/cursor.rs index d2d411f..56b5038 100644 --- a/bt-daemon/src/agents/cursor.rs +++ b/bt-daemon/src/agents/cursor.rs @@ -19,6 +19,7 @@ impl Agent for Cursor { impl Register for Cursor { fn register(self: Arc, registrar: &mut Registrar) { registrar.setup.insert(self.clone()); - registrar.run.insert(self); + registrar.run.insert(self.clone()); + registrar.translate.insert(self); } } diff --git a/bt-daemon/src/agents/grok.rs b/bt-daemon/src/agents/grok.rs index 7a4f717..aca2b7f 100644 --- a/bt-daemon/src/agents/grok.rs +++ b/bt-daemon/src/agents/grok.rs @@ -18,6 +18,7 @@ impl Agent for Grok { impl Register for Grok { fn register(self: Arc, registrar: &mut Registrar) { - registrar.setup.insert(self); + registrar.setup.insert(self.clone()); + registrar.translate.insert(self); } } diff --git a/bt-daemon/src/agents/mod.rs b/bt-daemon/src/agents/mod.rs index e2cfb03..6303881 100644 --- a/bt-daemon/src/agents/mod.rs +++ b/bt-daemon/src/agents/mod.rs @@ -23,6 +23,7 @@ pub(crate) use pi::Pi; use crate::managed_run::ManagedRun; use crate::setup::Setup; +use crate::translate::TranslatorFactory; use std::sync::{Arc, OnceLock}; /// The names an agent is known by. @@ -72,6 +73,10 @@ impl Agents { .find(|agent| agent.identity().matches(name)) .map(|agent| &**agent) } + + pub fn shared(&self) -> impl Iterator> { + self.agents.iter() + } } /// Every capability registry. Agents add themselves in [`Register::register`]. @@ -79,6 +84,7 @@ impl Agents { pub(crate) struct Registrar { pub setup: Agents, pub run: Agents, + pub translate: Agents, } pub(crate) trait Register { diff --git a/bt-daemon/src/agents/opencode.rs b/bt-daemon/src/agents/opencode.rs index 2c7b169..fe2607c 100644 --- a/bt-daemon/src/agents/opencode.rs +++ b/bt-daemon/src/agents/opencode.rs @@ -19,6 +19,7 @@ impl Agent for OpenCode { impl Register for OpenCode { fn register(self: Arc, registrar: &mut Registrar) { registrar.setup.insert(self.clone()); - registrar.run.insert(self); + registrar.run.insert(self.clone()); + registrar.translate.insert(self); } } diff --git a/bt-daemon/src/agents/pi.rs b/bt-daemon/src/agents/pi.rs index 2eb82b4..ce34b41 100644 --- a/bt-daemon/src/agents/pi.rs +++ b/bt-daemon/src/agents/pi.rs @@ -19,6 +19,7 @@ impl Agent for Pi { impl Register for Pi { fn register(self: Arc, registrar: &mut Registrar) { registrar.setup.insert(self.clone()); - registrar.run.insert(self); + registrar.run.insert(self.clone()); + registrar.translate.insert(self); } } diff --git a/bt-daemon/src/lib.rs b/bt-daemon/src/lib.rs index 66ba37d..3c7b7f3 100644 --- a/bt-daemon/src/lib.rs +++ b/bt-daemon/src/lib.rs @@ -72,6 +72,4 @@ pub use trace_runtime::{ run_trace, HostRouteSelection, RouteRequirements, TraceHostContext, TraceHostServices, }; pub use transcript_import::{import_transcript, import_transcripts, run_import}; -pub use translate::{ - AgentTranslator, Registry, SessionCtx, SpanOp, SpanRow, SpanType, TranslatorFactory, -}; +pub use translate::{AgentTranslator, Registry, SessionCtx, SpanOp, SpanRow, SpanType}; diff --git a/bt-daemon/src/translate/antigravity.rs b/bt-daemon/src/translate/antigravity.rs index ff72cc4..017eca3 100644 --- a/bt-daemon/src/translate/antigravity.rs +++ b/bt-daemon/src/translate/antigravity.rs @@ -19,23 +19,12 @@ use std::io::{BufRead, Seek, SeekFrom}; use std::path::Path; use std::sync::Arc; -pub struct AntigravityTranslatorFactory { - git: Arc, -} - -impl AntigravityTranslatorFactory { - pub(super) fn new(git: Arc) -> Self { - Self { git } - } -} - -impl TranslatorFactory for AntigravityTranslatorFactory { - fn source(&self) -> &str { - "antigravity" - } - +impl TranslatorFactory for crate::agents::Antigravity { fn create(&self, session_id: &str) -> Box { - Box::new(AntigravityTranslator::new(session_id, self.git.clone())) + Box::new(AntigravityTranslator::new( + session_id, + GitMetadataCache::shared(), + )) } } diff --git a/bt-daemon/src/translate/claude.rs b/bt-daemon/src/translate/claude.rs index 81d9878..9bcb617 100644 --- a/bt-daemon/src/translate/claude.rs +++ b/bt-daemon/src/translate/claude.rs @@ -119,23 +119,12 @@ fn value_as_nonempty_string(value: &Value) -> Option { } } -pub struct ClaudeTranslatorFactory { - git: Arc, -} - -impl ClaudeTranslatorFactory { - pub(super) fn new(git: Arc) -> Self { - Self { git } - } -} - -impl TranslatorFactory for ClaudeTranslatorFactory { - fn source(&self) -> &str { - "claude-code" - } - +impl TranslatorFactory for crate::agents::Claude { fn create(&self, session_id: &str) -> Box { - Box::new(ClaudeTranslator::new(session_id, self.git.clone())) + Box::new(ClaudeTranslator::new( + session_id, + GitMetadataCache::shared(), + )) } } diff --git a/bt-daemon/src/translate/codex.rs b/bt-daemon/src/translate/codex.rs index d4a3a5e..39c5c52 100644 --- a/bt-daemon/src/translate/codex.rs +++ b/bt-daemon/src/translate/codex.rs @@ -108,20 +108,7 @@ fn decode(value: &Value) -> Option { serde_json::from_value(value.clone()).ok() } -pub struct CodexTranslatorFactory { - git: Arc, -} - -impl CodexTranslatorFactory { - pub(super) fn new(git: Arc) -> Self { - Self { git } - } -} - -impl TranslatorFactory for CodexTranslatorFactory { - fn source(&self) -> &str { - "codex" - } +impl TranslatorFactory for crate::agents::Codex { fn create(&self, session_id: &str) -> Box { let root_span_id = ids::span_id(session_id, "root"); Box::new(CodexTranslator { @@ -144,7 +131,7 @@ impl TranslatorFactory for CodexTranslatorFactory { compaction_trigger_by_turn: RecentMap::default(), compaction_spans: RecentSet::default(), pending: None, - git: self.git.clone(), + git: GitMetadataCache::shared(), }) } } diff --git a/bt-daemon/src/translate/cursor/mod.rs b/bt-daemon/src/translate/cursor/mod.rs index 7b46e38..969d9e8 100644 --- a/bt-daemon/src/translate/cursor/mod.rs +++ b/bt-daemon/src/translate/cursor/mod.rs @@ -62,20 +62,12 @@ const CURSOR_METADATA_FIELDS: &[&str] = &[ "replacement_context_available", ]; -pub struct CursorTranslatorFactory { - git: Arc, -} -impl CursorTranslatorFactory { - pub(super) fn new(git: Arc) -> Self { - Self { git } - } -} -impl TranslatorFactory for CursorTranslatorFactory { - fn source(&self) -> &str { - "cursor" - } +impl TranslatorFactory for crate::agents::Cursor { fn create(&self, session_id: &str) -> Box { - Box::new(CursorTranslator::new(session_id, self.git.clone())) + Box::new(CursorTranslator::new( + session_id, + GitMetadataCache::shared(), + )) } } diff --git a/bt-daemon/src/translate/debug.rs b/bt-daemon/src/translate/debug.rs index 977a46f..484b439 100644 --- a/bt-daemon/src/translate/debug.rs +++ b/bt-daemon/src/translate/debug.rs @@ -5,15 +5,28 @@ //! exists. use super::{AgentTranslator, SessionCtx, SpanOp, SpanRow, SpanType, TranslatorFactory}; +use crate::agents::{Agent, Identity}; use crate::ids; use crate::wire::Envelope; -pub struct DebugTranslatorFactory; +/// Not a coding agent, so it is never registered in [`crate::agents`]; the +/// translator registry adds it directly. +pub(crate) struct DebugTranslatorFactory; -impl TranslatorFactory for DebugTranslatorFactory { - fn source(&self) -> &str { - "debug" +const IDENTITY: Identity = Identity { + id: "debug", + source: "debug", + aliases: &[], + display_name: "Debug", +}; + +impl Agent for DebugTranslatorFactory { + fn identity(&self) -> &'static Identity { + &IDENTITY } +} + +impl TranslatorFactory for DebugTranslatorFactory { fn create(&self, session_id: &str) -> Box { Box::new(DebugTranslator { root_span_id: ids::span_id(session_id, "root"), diff --git a/bt-daemon/src/translate/git.rs b/bt-daemon/src/translate/git.rs index 7e4a55b..2720e98 100644 --- a/bt-daemon/src/translate/git.rs +++ b/bt-daemon/src/translate/git.rs @@ -2,7 +2,7 @@ use super::{SpanOp, SpanRow}; use serde_json::{Map, Value}; use std::collections::{HashMap, VecDeque}; use std::path::{Path, PathBuf}; -use std::sync::Mutex; +use std::sync::{Arc, Mutex, OnceLock}; use std::time::{Duration, Instant, SystemTime}; const CACHE_CAPACITY: usize = 256; @@ -89,6 +89,14 @@ impl Default for GitMetadataCache { } impl GitMetadataCache { + /// The process-wide cache shared by every translator. Repository entries + /// are revalidated against the git directory before reuse and "not a + /// repository" results expire, so sharing them across registries is safe. + pub(super) fn shared() -> Arc { + static SHARED: OnceLock> = OnceLock::new(); + SHARED.get_or_init(Arc::default).clone() + } + pub(super) fn metadata(&self, cwd: &str) -> Map { if cwd.is_empty() { return Map::new(); diff --git a/bt-daemon/src/translate/grok.rs b/bt-daemon/src/translate/grok.rs index c3e1207..9ddbdfa 100644 --- a/bt-daemon/src/translate/grok.rs +++ b/bt-daemon/src/translate/grok.rs @@ -26,23 +26,9 @@ const MAX_OUTPUT_CHUNKS: usize = 2_048; const MAX_OUTPUT_BYTES: usize = 2 * 1024 * 1024; const MAX_SYSTEM_PROMPT_BYTES: u64 = 2 * 1024 * 1024; -pub struct GrokTranslatorFactory { - git: Arc, -} - -impl GrokTranslatorFactory { - pub(super) fn new(git: Arc) -> Self { - Self { git } - } -} - -impl TranslatorFactory for GrokTranslatorFactory { - fn source(&self) -> &str { - "grok" - } - +impl TranslatorFactory for crate::agents::Grok { fn create(&self, session_id: &str) -> Box { - Box::new(GrokTranslator::new(session_id, self.git.clone())) + Box::new(GrokTranslator::new(session_id, GitMetadataCache::shared())) } } diff --git a/bt-daemon/src/translate/mod.rs b/bt-daemon/src/translate/mod.rs index 07d95bd..1e4ff48 100644 --- a/bt-daemon/src/translate/mod.rs +++ b/bt-daemon/src/translate/mod.rs @@ -21,15 +21,9 @@ pub(crate) use pi::request_config as pi_request_config; mod recent; mod tool; -pub use antigravity::AntigravityTranslatorFactory; -pub use claude::ClaudeTranslatorFactory; -pub use codex::CodexTranslatorFactory; -pub use cursor::CursorTranslatorFactory; -pub use debug::DebugTranslatorFactory; -pub use grok::GrokTranslatorFactory; -pub use opencode::OpenCodeTranslatorFactory; -pub use pi::PiTranslatorFactory; +use debug::DebugTranslatorFactory; +use crate::agents::{registrar, Agent}; use crate::wire::{Envelope, SessionConfig}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -152,24 +146,27 @@ pub trait AgentTranslator: Send { } } -/// Builds translator instances for a given `source`. -pub trait TranslatorFactory: Send + Sync { - fn source(&self) -> &str; +/// Builds translator instances for an agent's hook source. Each agent type +/// implements this in its translator module and registers itself in +/// [`crate::agents`]; the source is the agent's [`Identity::source`]. +/// +/// [`Identity::source`]: crate::agents::Identity::source +pub(crate) trait TranslatorFactory: Agent { fn create(&self, session_id: &str) -> Box; } /// Maps canonical and supported alias source strings to translator factories. pub struct Registry { - factories: HashMap>, + factories: HashMap>, } -/// Map a source alias to its canonical name. Unknown sources pass through. +/// Map an agent name or alias to the source its hooks report. Unknown +/// sources pass through. pub(crate) fn canonical_source_name(source: &str) -> &str { - match source { - "claude" => "claude-code", - "open-code" => "opencode", - other => other, - } + registrar() + .translate + .get(source) + .map_or(source, |agent| agent.identity().source) } impl Registry { @@ -178,20 +175,16 @@ impl Registry { let mut r = Registry { factories: HashMap::new(), }; - r.register(Box::new(DebugTranslatorFactory)); - let git = Arc::new(git::GitMetadataCache::default()); - r.register(Box::new(AntigravityTranslatorFactory::new(git.clone()))); - r.register(Box::new(ClaudeTranslatorFactory::new(git.clone()))); - r.register(Box::new(CodexTranslatorFactory::new(git.clone()))); - r.register(Box::new(CursorTranslatorFactory::new(git.clone()))); - r.register(Box::new(GrokTranslatorFactory::new(git.clone()))); - r.register(Box::new(OpenCodeTranslatorFactory::new(git.clone()))); - r.register(Box::new(PiTranslatorFactory::new(git))); + r.insert(Arc::new(DebugTranslatorFactory)); + for agent in registrar().translate.shared() { + r.insert(agent.clone()); + } r } - pub fn register(&mut self, factory: Box) { - self.factories.insert(factory.source().to_string(), factory); + fn insert(&mut self, factory: Arc) { + self.factories + .insert(factory.identity().source.to_string(), factory); } /// Known sources, for the `initialize` capabilities list. @@ -203,10 +196,11 @@ impl Registry { /// Resolve daemon source aliases to one stable identity. pub fn canonical_source<'a>(&'a self, source: &'a str) -> Option<&'a str> { - let canonical = match canonical_source_name(source) { - canonical @ ("claude-code" | "opencode" | "antigravity" | "codex" | "cursor" - | "grok" | "pi" | "debug") => canonical, - _ => return None, + // Only agent sources and the debug translator are accepted. + let canonical = match registrar().translate.get(source) { + Some(agent) => agent.identity().source, + None if source == "debug" => "debug", + None => return None, }; self.factories.contains_key(canonical).then_some(canonical) } @@ -244,3 +238,43 @@ impl Registry { .unwrap_or_else(|error| panic!("{error}")) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_agent_source_has_a_translator() { + assert_eq!( + Registry::default_agents().sources(), + [ + "antigravity", + "claude-code", + "codex", + "cursor", + "debug", + "grok", + "opencode", + "pi" + ] + ); + } + + #[test] + fn source_aliases_resolve_to_hook_sources() { + let registry = Registry::default_agents(); + for (alias, source) in [ + ("claude", "claude-code"), + ("claude-code", "claude-code"), + ("open-code", "opencode"), + ("opencode", "opencode"), + ("codex", "codex"), + ("debug", "debug"), + ] { + assert_eq!(registry.canonical_source(alias), Some(source)); + } + assert_eq!(registry.canonical_source("agy"), None); + assert_eq!(canonical_source_name("claude"), "claude-code"); + assert_eq!(canonical_source_name("unknown"), "unknown"); + } +} diff --git a/bt-daemon/src/translate/opencode.rs b/bt-daemon/src/translate/opencode.rs index 1b51bdd..4214ec3 100644 --- a/bt-daemon/src/translate/opencode.rs +++ b/bt-daemon/src/translate/opencode.rs @@ -16,27 +16,13 @@ use serde_json::{json, Value}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; -pub struct OpenCodeTranslatorFactory { - git: Arc, -} - -impl OpenCodeTranslatorFactory { - pub(super) fn new(git: Arc) -> Self { - Self { git } - } -} - -impl TranslatorFactory for OpenCodeTranslatorFactory { - fn source(&self) -> &str { - "opencode" - } - +impl TranslatorFactory for crate::agents::OpenCode { fn create(&self, session_id: &str) -> Box { Box::new(OpenCodeTranslator { daemon_session_id: session_id.to_string(), sessions: HashMap::new(), permission_requests: HashMap::new(), - git: self.git.clone(), + git: GitMetadataCache::shared(), last_ts_ms: 0, }) } diff --git a/bt-daemon/src/translate/pi.rs b/bt-daemon/src/translate/pi.rs index f79e0b2..22e1ffd 100644 --- a/bt-daemon/src/translate/pi.rs +++ b/bt-daemon/src/translate/pi.rs @@ -17,18 +17,7 @@ use serde_json::{json, Map, Value}; use std::collections::HashMap; use std::sync::Arc; -pub struct PiTranslatorFactory { - git: Arc, -} -impl PiTranslatorFactory { - pub(super) fn new(git: Arc) -> Self { - Self { git } - } -} -impl TranslatorFactory for PiTranslatorFactory { - fn source(&self) -> &str { - "pi" - } +impl TranslatorFactory for crate::agents::Pi { fn create(&self, session_id: &str) -> Box { Box::new(PiTranslator { session_id: session_id.into(), @@ -48,7 +37,7 @@ impl TranslatorFactory for PiTranslatorFactory { branch_summary: None, last_ts: 0, thinking_level: None, - git: self.git.clone(), + git: GitMetadataCache::shared(), }) } }