luft_adapters/lib.rs
1//! # luft-adapters
2//!
3//! **ACP (Agent Communication Protocol) backend for Luft.**
4//!
5//! [`AcpAdapter`] connects to an `opencode acp` subprocess as an ACP **client**
6//! and implements the `AgentBackend` trait from `luft-core`.
7//!
8//! ## Architecture
9//!
10//! ```text
11//! Luft Runtime AcpAdapter
12//! │ │
13//! │ AgentTask │ spawn `opencode acp`
14//! ├──────────────────────────────────►│
15//! │ ├──── ACP Session.Create ────► opencode
16//! │ │◄─── SessionUpdate(event) ─── opencode
17//! │ AgentEvent::AgentProgress │
18//! │◄──────────────────────────────────┤
19//! │ ├──── Session.Stop ───────────► opencode
20//! │ AgentResult │
21//! │◄──────────────────────────────────┘
22//! ```
23//!
24//! ## Module Layout
25//!
26//! | Module | Responsibility |
27//! |--------|---------------|
28//! | `acp_adapter` | Config, adapter, one-shot session lifecycle |
29//! | `update_mapper` | ACP `SessionUpdate` → Luft `ProgressDelta` |
30//! | `permission` | Non-interactive `request_permission` decisions |
31//! | `result_collector` | Stop reason + message → `AgentResult` |
32//!
33//! ## Quick Start
34//!
35//! ```no_run
36//! use luft_adapters::{AcpAdapter, AcpConfig, register_acp_backend};
37//! use luft_core::BackendRegistry;
38//!
39//! let mut registry = BackendRegistry::new();
40//! register_acp_backend(&mut registry, AcpConfig::default());
41//! ```
42
43mod acp_adapter;
44mod permission;
45mod result_collector;
46mod update_mapper;
47
48pub use acp_adapter::{AcpAdapter, AcpConfig};
49
50use luft_core::BackendRegistry;
51use std::sync::Arc;
52
53/// Register an [`AcpAdapter`] (the `opencode` backend) with a registry.
54pub fn register_acp_backend(registry: &mut BackendRegistry, config: AcpConfig) {
55 registry.register(Arc::new(AcpAdapter::new(config)));
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use luft_core::BackendRegistry;
62
63 #[test]
64 fn registers_backend_with_opencode_id() {
65 let mut registry = BackendRegistry::new();
66 register_acp_backend(&mut registry, AcpConfig::default());
67 let backend = registry.get("opencode").unwrap();
68 assert_eq!(backend.id(), "opencode");
69 }
70
71 #[test]
72 fn register_overwrites_existing() {
73 let mut registry = BackendRegistry::new();
74 // Two distinguishable configs: same `id` (so they collide on the
75 // registry key) but different `model`. A correct overwrite replaces
76 // the first registration entirely; a no-op-on-collision or append
77 // implementation would keep the first `model`.
78 let first = AcpConfig {
79 model: Some("first".to_string()),
80 ..AcpConfig::default()
81 };
82 let second = AcpConfig {
83 model: Some("second".to_string()),
84 ..AcpConfig::default()
85 };
86 register_acp_backend(&mut registry, first);
87 register_acp_backend(&mut registry, second);
88 let backend = registry.get("opencode").unwrap();
89 assert_eq!(backend.id(), "opencode");
90 // Downcast through `as_any` so we can read the per-adapter config
91 // (not just `id`, which is identical for both registrations).
92 let acp = backend
93 .as_any()
94 .downcast_ref::<AcpAdapter>()
95 .expect("registered backend is an AcpAdapter");
96 assert_eq!(acp.config().model.as_deref(), Some("second"));
97 }
98
99 #[test]
100 fn register_does_not_add_other_ids() {
101 let mut registry = BackendRegistry::new();
102 register_acp_backend(&mut registry, AcpConfig::default());
103 assert!(registry.get("other").is_err());
104 }
105}