From ed5dc8814fa9ac412498af45f84335edd46058ed Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Tue, 6 Oct 2026 20:38:10 +0800 Subject: [PATCH 1/3] refactor(translate): register translators per agent Registry::default_agents constructed each agent's translator factory by hand, and source aliases were resolved through a separate hardcoded alias table and allowlist. Each agent type now implements `Translate` in its translator module and registers it with the others. The registry builds one factory per registered agent around the shared Git metadata cache, and alias resolution uses the agents' identities. The accepted hook sources and aliases are unchanged. A test pins them. Co-Authored-By: Claude Opus 5.5 (1M context) --- bt-daemon/src/agents/antigravity.rs | 5 +- bt-daemon/src/agents/claude.rs | 3 +- bt-daemon/src/agents/codex.rs | 3 +- bt-daemon/src/agents/cursor.rs | 3 +- bt-daemon/src/agents/grok.rs | 3 +- bt-daemon/src/agents/mod.rs | 6 ++ bt-daemon/src/agents/opencode.rs | 3 +- bt-daemon/src/agents/pi.rs | 3 +- bt-daemon/src/translate/antigravity.rs | 6 ++ bt-daemon/src/translate/claude.rs | 6 ++ bt-daemon/src/translate/codex.rs | 6 ++ bt-daemon/src/translate/cursor/mod.rs | 6 ++ bt-daemon/src/translate/git.rs | 2 +- bt-daemon/src/translate/grok.rs | 6 ++ bt-daemon/src/translate/mod.rs | 86 +++++++++++++++++++------- bt-daemon/src/translate/opencode.rs | 6 ++ bt-daemon/src/translate/pi.rs | 6 ++ 17 files changed, 126 insertions(+), 33 deletions(-) diff --git a/bt-daemon/src/agents/antigravity.rs b/bt-daemon/src/agents/antigravity.rs index 2e39511b..1216b92f 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 359b22f7..396f68a5 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 80741bf8..75f9a740 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 d2d411f0..56b5038d 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 7a4f717d..aca2b7f7 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 e2cfb037..a918cbfe 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::Translate; 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 iter(&self) -> impl Iterator { + self.agents.iter().map(|agent| &**agent) + } } /// 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 2c7b1691..fe2607cc 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 2eb82b4f..ce34b41c 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/translate/antigravity.rs b/bt-daemon/src/translate/antigravity.rs index ff72cc4a..c1ba88a1 100644 --- a/bt-daemon/src/translate/antigravity.rs +++ b/bt-daemon/src/translate/antigravity.rs @@ -29,6 +29,12 @@ impl AntigravityTranslatorFactory { } } +impl super::Translate for crate::agents::Antigravity { + fn translator_factory(&self, git: Arc) -> Box { + Box::new(AntigravityTranslatorFactory::new(git)) + } +} + impl TranslatorFactory for AntigravityTranslatorFactory { fn source(&self) -> &str { "antigravity" diff --git a/bt-daemon/src/translate/claude.rs b/bt-daemon/src/translate/claude.rs index 81d98784..9d370467 100644 --- a/bt-daemon/src/translate/claude.rs +++ b/bt-daemon/src/translate/claude.rs @@ -129,6 +129,12 @@ impl ClaudeTranslatorFactory { } } +impl super::Translate for crate::agents::Claude { + fn translator_factory(&self, git: Arc) -> Box { + Box::new(ClaudeTranslatorFactory::new(git)) + } +} + impl TranslatorFactory for ClaudeTranslatorFactory { fn source(&self) -> &str { "claude-code" diff --git a/bt-daemon/src/translate/codex.rs b/bt-daemon/src/translate/codex.rs index d4a3a5e6..f20437d2 100644 --- a/bt-daemon/src/translate/codex.rs +++ b/bt-daemon/src/translate/codex.rs @@ -118,6 +118,12 @@ impl CodexTranslatorFactory { } } +impl super::Translate for crate::agents::Codex { + fn translator_factory(&self, git: Arc) -> Box { + Box::new(CodexTranslatorFactory::new(git)) + } +} + impl TranslatorFactory for CodexTranslatorFactory { fn source(&self) -> &str { "codex" diff --git a/bt-daemon/src/translate/cursor/mod.rs b/bt-daemon/src/translate/cursor/mod.rs index 7b46e387..dc17bced 100644 --- a/bt-daemon/src/translate/cursor/mod.rs +++ b/bt-daemon/src/translate/cursor/mod.rs @@ -70,6 +70,12 @@ impl CursorTranslatorFactory { Self { git } } } +impl super::Translate for crate::agents::Cursor { + fn translator_factory(&self, git: Arc) -> Box { + Box::new(CursorTranslatorFactory::new(git)) + } +} + impl TranslatorFactory for CursorTranslatorFactory { fn source(&self) -> &str { "cursor" diff --git a/bt-daemon/src/translate/git.rs b/bt-daemon/src/translate/git.rs index 7e4a55b4..95209058 100644 --- a/bt-daemon/src/translate/git.rs +++ b/bt-daemon/src/translate/git.rs @@ -76,7 +76,7 @@ impl CacheState { } /// Daemon-wide Git metadata cache shared by all production translators. -pub(super) struct GitMetadataCache { +pub(crate) struct GitMetadataCache { state: Mutex, } diff --git a/bt-daemon/src/translate/grok.rs b/bt-daemon/src/translate/grok.rs index c3e1207d..de496334 100644 --- a/bt-daemon/src/translate/grok.rs +++ b/bt-daemon/src/translate/grok.rs @@ -36,6 +36,12 @@ impl GrokTranslatorFactory { } } +impl super::Translate for crate::agents::Grok { + fn translator_factory(&self, git: Arc) -> Box { + Box::new(GrokTranslatorFactory::new(git)) + } +} + impl TranslatorFactory for GrokTranslatorFactory { fn source(&self) -> &str { "grok" diff --git a/bt-daemon/src/translate/mod.rs b/bt-daemon/src/translate/mod.rs index 07d95bda..38d01593 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 crate::agents::{registrar, Agent}; use crate::wire::{Envelope, SessionConfig}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -152,6 +146,12 @@ pub trait AgentTranslator: Send { } } +/// An agent whose hook events have a translator. Implemented in each +/// agent's translator module and registered in [`crate::agents`]. +pub(crate) trait Translate: Agent { + fn translator_factory(&self, git: Arc) -> Box; +} + /// Builds translator instances for a given `source`. pub trait TranslatorFactory: Send + Sync { fn source(&self) -> &str; @@ -163,13 +163,13 @@ pub struct Registry { 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 { @@ -180,13 +180,9 @@ impl Registry { }; 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))); + for agent in registrar().translate.iter() { + r.register(agent.translator_factory(git.clone())); + } r } @@ -203,10 +199,12 @@ 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, even if a + // host registered a factory under another name. + 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 +242,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 1b51bddc..95e8baa3 100644 --- a/bt-daemon/src/translate/opencode.rs +++ b/bt-daemon/src/translate/opencode.rs @@ -26,6 +26,12 @@ impl OpenCodeTranslatorFactory { } } +impl super::Translate for crate::agents::OpenCode { + fn translator_factory(&self, git: Arc) -> Box { + Box::new(OpenCodeTranslatorFactory::new(git)) + } +} + impl TranslatorFactory for OpenCodeTranslatorFactory { fn source(&self) -> &str { "opencode" diff --git a/bt-daemon/src/translate/pi.rs b/bt-daemon/src/translate/pi.rs index f79e0b26..b3cdccfe 100644 --- a/bt-daemon/src/translate/pi.rs +++ b/bt-daemon/src/translate/pi.rs @@ -25,6 +25,12 @@ impl PiTranslatorFactory { Self { git } } } +impl super::Translate for crate::agents::Pi { + fn translator_factory(&self, git: Arc) -> Box { + Box::new(PiTranslatorFactory::new(git)) + } +} + impl TranslatorFactory for PiTranslatorFactory { fn source(&self) -> &str { "pi" From 359401f0e3edd22a4cfcd953f073d0f362d85cb3 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 7 Oct 2026 01:13:37 +0800 Subject: [PATCH 2/3] refactor(translate): make agent types their own translator factories `Translate` existed only to inject the shared Git metadata cache into a per-agent factory struct. Each agent type now implements the public `TranslatorFactory` directly, and the cache is process-wide. That is safe because repository entries are revalidated against the git directory before reuse and negative results expire. The seven factory structs and their `Translate` impls are gone. `Translate` remains only as an empty marker joining `Agent` and `TranslatorFactory`, implemented for every such type, so the registry can find agents by name. Co-Authored-By: Claude Opus 5.5 (1M context) --- bt-daemon/src/agents/mod.rs | 4 ++-- bt-daemon/src/translate/antigravity.rs | 25 ++++++------------------- bt-daemon/src/translate/claude.rs | 25 ++++++------------------- bt-daemon/src/translate/codex.rs | 22 +++------------------- bt-daemon/src/translate/cursor/mod.rs | 23 ++++++----------------- bt-daemon/src/translate/git.rs | 12 ++++++++++-- bt-daemon/src/translate/grok.rs | 22 +++------------------- bt-daemon/src/translate/mod.rs | 23 ++++++++++++++--------- bt-daemon/src/translate/opencode.rs | 22 +++------------------- bt-daemon/src/translate/pi.rs | 20 +++----------------- 10 files changed, 56 insertions(+), 142 deletions(-) diff --git a/bt-daemon/src/agents/mod.rs b/bt-daemon/src/agents/mod.rs index a918cbfe..c315b3e9 100644 --- a/bt-daemon/src/agents/mod.rs +++ b/bt-daemon/src/agents/mod.rs @@ -74,8 +74,8 @@ impl Agents { .map(|agent| &**agent) } - pub fn iter(&self) -> impl Iterator { - self.agents.iter().map(|agent| &**agent) + pub fn shared(&self) -> impl Iterator> { + self.agents.iter() } } diff --git a/bt-daemon/src/translate/antigravity.rs b/bt-daemon/src/translate/antigravity.rs index c1ba88a1..ccf2c7dd 100644 --- a/bt-daemon/src/translate/antigravity.rs +++ b/bt-daemon/src/translate/antigravity.rs @@ -19,29 +19,16 @@ 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 super::Translate for crate::agents::Antigravity { - fn translator_factory(&self, git: Arc) -> Box { - Box::new(AntigravityTranslatorFactory::new(git)) - } -} - -impl TranslatorFactory for AntigravityTranslatorFactory { +impl TranslatorFactory for crate::agents::Antigravity { fn source(&self) -> &str { - "antigravity" + crate::agents::Agent::identity(self).source } 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 9d370467..f19047b2 100644 --- a/bt-daemon/src/translate/claude.rs +++ b/bt-daemon/src/translate/claude.rs @@ -119,29 +119,16 @@ 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 super::Translate for crate::agents::Claude { - fn translator_factory(&self, git: Arc) -> Box { - Box::new(ClaudeTranslatorFactory::new(git)) - } -} - -impl TranslatorFactory for ClaudeTranslatorFactory { +impl TranslatorFactory for crate::agents::Claude { fn source(&self) -> &str { - "claude-code" + crate::agents::Agent::identity(self).source } 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 f20437d2..75581d01 100644 --- a/bt-daemon/src/translate/codex.rs +++ b/bt-daemon/src/translate/codex.rs @@ -108,25 +108,9 @@ 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 super::Translate for crate::agents::Codex { - fn translator_factory(&self, git: Arc) -> Box { - Box::new(CodexTranslatorFactory::new(git)) - } -} - -impl TranslatorFactory for CodexTranslatorFactory { +impl TranslatorFactory for crate::agents::Codex { fn source(&self) -> &str { - "codex" + crate::agents::Agent::identity(self).source } fn create(&self, session_id: &str) -> Box { let root_span_id = ids::span_id(session_id, "root"); @@ -150,7 +134,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 dc17bced..5372a213 100644 --- a/bt-daemon/src/translate/cursor/mod.rs +++ b/bt-daemon/src/translate/cursor/mod.rs @@ -62,26 +62,15 @@ 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 super::Translate for crate::agents::Cursor { - fn translator_factory(&self, git: Arc) -> Box { - Box::new(CursorTranslatorFactory::new(git)) - } -} - -impl TranslatorFactory for CursorTranslatorFactory { +impl TranslatorFactory for crate::agents::Cursor { fn source(&self) -> &str { - "cursor" + crate::agents::Agent::identity(self).source } 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/git.rs b/bt-daemon/src/translate/git.rs index 95209058..2720e986 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; @@ -76,7 +76,7 @@ impl CacheState { } /// Daemon-wide Git metadata cache shared by all production translators. -pub(crate) struct GitMetadataCache { +pub(super) struct GitMetadataCache { state: Mutex, } @@ -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 de496334..92fad801 100644 --- a/bt-daemon/src/translate/grok.rs +++ b/bt-daemon/src/translate/grok.rs @@ -26,29 +26,13 @@ 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 super::Translate for crate::agents::Grok { - fn translator_factory(&self, git: Arc) -> Box { - Box::new(GrokTranslatorFactory::new(git)) - } -} - -impl TranslatorFactory for GrokTranslatorFactory { +impl TranslatorFactory for crate::agents::Grok { fn source(&self) -> &str { - "grok" + crate::agents::Agent::identity(self).source } 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 38d01593..4e8bc3b2 100644 --- a/bt-daemon/src/translate/mod.rs +++ b/bt-daemon/src/translate/mod.rs @@ -146,11 +146,13 @@ pub trait AgentTranslator: Send { } } -/// An agent whose hook events have a translator. Implemented in each -/// agent's translator module and registered in [`crate::agents`]. -pub(crate) trait Translate: Agent { - fn translator_factory(&self, git: Arc) -> Box; -} +/// An agent that is its own [`TranslatorFactory`]. Each agent implements the +/// factory in its translator module and registers itself in +/// [`crate::agents`]; this only joins the two traits so the registry can look +/// agents up by name. +pub(crate) trait Translate: Agent + TranslatorFactory {} + +impl Translate for T {} /// Builds translator instances for a given `source`. pub trait TranslatorFactory: Send + Sync { @@ -160,7 +162,7 @@ pub trait TranslatorFactory: Send + Sync { /// Maps canonical and supported alias source strings to translator factories. pub struct Registry { - factories: HashMap>, + factories: HashMap>, } /// Map an agent name or alias to the source its hooks report. Unknown @@ -179,14 +181,17 @@ impl Registry { factories: HashMap::new(), }; r.register(Box::new(DebugTranslatorFactory)); - let git = Arc::new(git::GitMetadataCache::default()); - for agent in registrar().translate.iter() { - r.register(agent.translator_factory(git.clone())); + for agent in registrar().translate.shared() { + r.insert(agent.clone()); } r } pub fn register(&mut self, factory: Box) { + self.insert(factory.into()); + } + + fn insert(&mut self, factory: Arc) { self.factories.insert(factory.source().to_string(), factory); } diff --git a/bt-daemon/src/translate/opencode.rs b/bt-daemon/src/translate/opencode.rs index 95e8baa3..4b5b6170 100644 --- a/bt-daemon/src/translate/opencode.rs +++ b/bt-daemon/src/translate/opencode.rs @@ -16,25 +16,9 @@ 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 super::Translate for crate::agents::OpenCode { - fn translator_factory(&self, git: Arc) -> Box { - Box::new(OpenCodeTranslatorFactory::new(git)) - } -} - -impl TranslatorFactory for OpenCodeTranslatorFactory { +impl TranslatorFactory for crate::agents::OpenCode { fn source(&self) -> &str { - "opencode" + crate::agents::Agent::identity(self).source } fn create(&self, session_id: &str) -> Box { @@ -42,7 +26,7 @@ impl TranslatorFactory for OpenCodeTranslatorFactory { 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 b3cdccfe..5675d7e6 100644 --- a/bt-daemon/src/translate/pi.rs +++ b/bt-daemon/src/translate/pi.rs @@ -17,23 +17,9 @@ 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 super::Translate for crate::agents::Pi { - fn translator_factory(&self, git: Arc) -> Box { - Box::new(PiTranslatorFactory::new(git)) - } -} - -impl TranslatorFactory for PiTranslatorFactory { +impl TranslatorFactory for crate::agents::Pi { fn source(&self) -> &str { - "pi" + crate::agents::Agent::identity(self).source } fn create(&self, session_id: &str) -> Box { Box::new(PiTranslator { @@ -54,7 +40,7 @@ impl TranslatorFactory for PiTranslatorFactory { branch_summary: None, last_ts: 0, thinking_level: None, - git: self.git.clone(), + git: GitMetadataCache::shared(), }) } } From cfb5a8cd11742a4b0560f33f812c3ca6ec8d44b1 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 7 Oct 2026 01:19:14 +0800 Subject: [PATCH 3/3] refactor(translate): key translator factories by agent identity `TranslatorFactory::source` restated what each agent's identity already says. The trait now takes `Agent` as its supertrait, and the registry keys factories by `identity().source`. That makes the `Translate` marker unnecessary, so it is removed and the agent registry holds `dyn TranslatorFactory` directly. The debug translator gets its own `debug` identity but stays out of the agent registry. `Agent` is crate-private, so `TranslatorFactory` is too. It and the unused `Registry::register` leave the public API. Nothing outside the crate implemented the trait or registered factories. Co-Authored-By: Claude Opus 5.5 (1M context) --- bt-daemon/src/agents/mod.rs | 4 ++-- bt-daemon/src/lib.rs | 4 +--- bt-daemon/src/translate/antigravity.rs | 4 ---- bt-daemon/src/translate/claude.rs | 4 ---- bt-daemon/src/translate/codex.rs | 3 --- bt-daemon/src/translate/cursor/mod.rs | 3 --- bt-daemon/src/translate/debug.rs | 21 +++++++++++++---- bt-daemon/src/translate/grok.rs | 4 ---- bt-daemon/src/translate/mod.rs | 31 +++++++++----------------- bt-daemon/src/translate/opencode.rs | 4 ---- bt-daemon/src/translate/pi.rs | 3 --- 11 files changed, 31 insertions(+), 54 deletions(-) diff --git a/bt-daemon/src/agents/mod.rs b/bt-daemon/src/agents/mod.rs index c315b3e9..63038811 100644 --- a/bt-daemon/src/agents/mod.rs +++ b/bt-daemon/src/agents/mod.rs @@ -23,7 +23,7 @@ pub(crate) use pi::Pi; use crate::managed_run::ManagedRun; use crate::setup::Setup; -use crate::translate::Translate; +use crate::translate::TranslatorFactory; use std::sync::{Arc, OnceLock}; /// The names an agent is known by. @@ -84,7 +84,7 @@ impl Agents { pub(crate) struct Registrar { pub setup: Agents, pub run: Agents, - pub translate: Agents, + pub translate: Agents, } pub(crate) trait Register { diff --git a/bt-daemon/src/lib.rs b/bt-daemon/src/lib.rs index 66ba37d5..3c7b7f35 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 ccf2c7dd..017eca3a 100644 --- a/bt-daemon/src/translate/antigravity.rs +++ b/bt-daemon/src/translate/antigravity.rs @@ -20,10 +20,6 @@ use std::path::Path; use std::sync::Arc; impl TranslatorFactory for crate::agents::Antigravity { - fn source(&self) -> &str { - crate::agents::Agent::identity(self).source - } - fn create(&self, session_id: &str) -> Box { Box::new(AntigravityTranslator::new( session_id, diff --git a/bt-daemon/src/translate/claude.rs b/bt-daemon/src/translate/claude.rs index f19047b2..9bcb6171 100644 --- a/bt-daemon/src/translate/claude.rs +++ b/bt-daemon/src/translate/claude.rs @@ -120,10 +120,6 @@ fn value_as_nonempty_string(value: &Value) -> Option { } impl TranslatorFactory for crate::agents::Claude { - fn source(&self) -> &str { - crate::agents::Agent::identity(self).source - } - fn create(&self, session_id: &str) -> Box { Box::new(ClaudeTranslator::new( session_id, diff --git a/bt-daemon/src/translate/codex.rs b/bt-daemon/src/translate/codex.rs index 75581d01..39c5c528 100644 --- a/bt-daemon/src/translate/codex.rs +++ b/bt-daemon/src/translate/codex.rs @@ -109,9 +109,6 @@ fn decode(value: &Value) -> Option { } impl TranslatorFactory for crate::agents::Codex { - fn source(&self) -> &str { - crate::agents::Agent::identity(self).source - } fn create(&self, session_id: &str) -> Box { let root_span_id = ids::span_id(session_id, "root"); Box::new(CodexTranslator { diff --git a/bt-daemon/src/translate/cursor/mod.rs b/bt-daemon/src/translate/cursor/mod.rs index 5372a213..969d9e88 100644 --- a/bt-daemon/src/translate/cursor/mod.rs +++ b/bt-daemon/src/translate/cursor/mod.rs @@ -63,9 +63,6 @@ const CURSOR_METADATA_FIELDS: &[&str] = &[ ]; impl TranslatorFactory for crate::agents::Cursor { - fn source(&self) -> &str { - crate::agents::Agent::identity(self).source - } fn create(&self, session_id: &str) -> Box { Box::new(CursorTranslator::new( session_id, diff --git a/bt-daemon/src/translate/debug.rs b/bt-daemon/src/translate/debug.rs index 977a46f2..484b439d 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/grok.rs b/bt-daemon/src/translate/grok.rs index 92fad801..9ddbdfa4 100644 --- a/bt-daemon/src/translate/grok.rs +++ b/bt-daemon/src/translate/grok.rs @@ -27,10 +27,6 @@ const MAX_OUTPUT_BYTES: usize = 2 * 1024 * 1024; const MAX_SYSTEM_PROMPT_BYTES: u64 = 2 * 1024 * 1024; impl TranslatorFactory for crate::agents::Grok { - fn source(&self) -> &str { - crate::agents::Agent::identity(self).source - } - fn create(&self, session_id: &str) -> Box { 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 4e8bc3b2..1e4ff483 100644 --- a/bt-daemon/src/translate/mod.rs +++ b/bt-daemon/src/translate/mod.rs @@ -21,7 +21,7 @@ pub(crate) use pi::request_config as pi_request_config; mod recent; mod tool; -pub use debug::DebugTranslatorFactory; +use debug::DebugTranslatorFactory; use crate::agents::{registrar, Agent}; use crate::wire::{Envelope, SessionConfig}; @@ -146,17 +146,12 @@ pub trait AgentTranslator: Send { } } -/// An agent that is its own [`TranslatorFactory`]. Each agent implements the -/// factory in its translator module and registers itself in -/// [`crate::agents`]; this only joins the two traits so the registry can look -/// agents up by name. -pub(crate) trait Translate: Agent + TranslatorFactory {} - -impl Translate for T {} - -/// 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; } @@ -180,19 +175,16 @@ impl Registry { let mut r = Registry { factories: HashMap::new(), }; - r.register(Box::new(DebugTranslatorFactory)); + r.insert(Arc::new(DebugTranslatorFactory)); for agent in registrar().translate.shared() { r.insert(agent.clone()); } r } - pub fn register(&mut self, factory: Box) { - self.insert(factory.into()); - } - fn insert(&mut self, factory: Arc) { - self.factories.insert(factory.source().to_string(), factory); + self.factories + .insert(factory.identity().source.to_string(), factory); } /// Known sources, for the `initialize` capabilities list. @@ -204,8 +196,7 @@ impl Registry { /// Resolve daemon source aliases to one stable identity. pub fn canonical_source<'a>(&'a self, source: &'a str) -> Option<&'a str> { - // Only agent sources and the debug translator are accepted, even if a - // host registered a factory under another name. + // 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", diff --git a/bt-daemon/src/translate/opencode.rs b/bt-daemon/src/translate/opencode.rs index 4b5b6170..4214ec3a 100644 --- a/bt-daemon/src/translate/opencode.rs +++ b/bt-daemon/src/translate/opencode.rs @@ -17,10 +17,6 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; impl TranslatorFactory for crate::agents::OpenCode { - fn source(&self) -> &str { - crate::agents::Agent::identity(self).source - } - fn create(&self, session_id: &str) -> Box { Box::new(OpenCodeTranslator { daemon_session_id: session_id.to_string(), diff --git a/bt-daemon/src/translate/pi.rs b/bt-daemon/src/translate/pi.rs index 5675d7e6..22e1ffde 100644 --- a/bt-daemon/src/translate/pi.rs +++ b/bt-daemon/src/translate/pi.rs @@ -18,9 +18,6 @@ use std::collections::HashMap; use std::sync::Arc; impl TranslatorFactory for crate::agents::Pi { - fn source(&self) -> &str { - crate::agents::Agent::identity(self).source - } fn create(&self, session_id: &str) -> Box { Box::new(PiTranslator { session_id: session_id.into(),