Skip to main content

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}