Skip to main content

zeph_commands/traits/
agent.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! [`AgentAccess`] — an empty marker supertrait that unifies 15 focused command-domain
5//! sub-traits into a single object-safe trait, bridging `zeph-commands` handlers to
6//! `zeph-core` subsystems that cannot be decomposed into smaller trait objects without
7//! borrow-checker conflicts.
8//!
9//! ## Design rationale
10//!
11//! Commands like `/graph`, `/skill`, `/model`, `/policy`, and `/scheduler` access 10–20 internal
12//! `Agent<C>` fields simultaneously. Decomposing each into a separate trait object field on
13//! [`CommandContext`] would require splitting those fields from `&mut self.channel` (already
14//! held by `ctx.sink`), which the borrow checker cannot express with safe Rust.
15//!
16//! The solution: one dispatch-facing trait object (`dyn AgentAccess`) assembled from 15
17//! cohesive per-domain sub-traits (see `crate::traits`) via a blanket impl. Every method
18//! ultimately delegates to the corresponding `Agent<C>` methods. Each sub-trait is
19//! object-safe because every async method returns `Pin<Box<dyn Future + Send>>`; supertrait
20//! methods are callable directly on a `dyn AgentAccess` value without any upcasting step.
21//!
22//! ## Implementors
23//!
24//! `zeph-core::Agent<C>` implements each of the 15 sub-traits (in `zeph-core::agent::*_commands`
25//! modules) and receives `AgentAccess` for free via the blanket impl below.
26//!
27//! [`CommandContext`]: crate::context::CommandContext
28
29use crate::traits::graph::GraphAccess;
30use crate::traits::integration::IntegrationAccess;
31use crate::traits::lsp::LspAccess;
32use crate::traits::mcp::McpAccess;
33use crate::traits::memory::MemoryAccess;
34use crate::traits::misc::MiscAccess;
35use crate::traits::model::ModelAccess;
36use crate::traits::orchestration::OrchestrationAccess;
37use crate::traits::policy::PolicyAccess;
38use crate::traits::scheduler::SchedulerAccess;
39use crate::traits::session_control::SessionControlAccess;
40use crate::traits::skill::SkillAccess;
41use crate::traits::subagent::SubagentAccess;
42use crate::traits::tracking::TrackingAccess;
43use crate::traits::worktree::WorktreeAccess;
44
45/// Broad access to agent subsystems for command handlers that cannot be served by
46/// individual sub-traits.
47///
48/// An empty marker supertrait unifying 15 focused command-domain sub-traits (see
49/// `crate::traits`) into a single object-safe trait. Implemented automatically for any
50/// type that implements all 15 sub-traits plus `Send` — see the blanket impl below. Never
51/// implement this trait directly; implement the sub-traits instead.
52///
53/// All async methods across the 15 sub-traits return
54/// `Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>>` (or similar) for
55/// object safety — allowing `&mut dyn AgentAccess` storage in [`CommandContext`].
56///
57/// [`CommandContext`]: crate::context::CommandContext
58pub trait AgentAccess:
59    MemoryAccess
60    + GraphAccess
61    + ModelAccess
62    + SkillAccess
63    + PolicyAccess
64    + SchedulerAccess
65    + LspAccess
66    + SessionControlAccess
67    + McpAccess
68    + OrchestrationAccess
69    + SubagentAccess
70    + IntegrationAccess
71    + TrackingAccess
72    + WorktreeAccess
73    + MiscAccess
74    + Send
75{
76}
77
78impl<T> AgentAccess for T where
79    T: MemoryAccess
80        + GraphAccess
81        + ModelAccess
82        + SkillAccess
83        + PolicyAccess
84        + SchedulerAccess
85        + LspAccess
86        + SessionControlAccess
87        + McpAccess
88        + OrchestrationAccess
89        + SubagentAccess
90        + IntegrationAccess
91        + TrackingAccess
92        + WorktreeAccess
93        + MiscAccess
94        + Send
95        + ?Sized
96{
97}
98
99/// A no-op [`AgentAccess`] implementation.
100///
101/// Used when constructing a [`crate::CommandContext`] for a dispatch block that does not invoke
102/// any agent-access commands (e.g., the session/debug-only registry block in `Agent::run`).
103/// Allows the borrow checker to accept a split borrow: `sink` holds `&mut channel` while
104/// `agent` holds this zero-size sentinel instead of `&mut self`.
105///
106/// Implements all 15 [`AgentAccess`] sub-traits (grouped below by trait, mirroring
107/// `crate::traits`) so it obtains `AgentAccess` via the blanket impl above. Sub-traits whose
108/// methods carry trait defaults matching this sentinel's desired no-op behavior are omitted
109/// here and rely on the default (`handle_goal`, `active_goal_snapshot`, `handle_undo`,
110/// `handle_redo`, `handle_web_search`, `handle_agents`, `handle_conv`, `list_worktrees`,
111/// `clean_worktrees`, `change_working_directory`).
112pub struct NullAgent;
113
114impl MemoryAccess for NullAgent {
115    fn memory_tiers<'a>(
116        &'a mut self,
117    ) -> std::pin::Pin<
118        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
119    > {
120        Box::pin(async { Ok(String::new()) })
121    }
122
123    fn memory_promote<'a>(
124        &'a mut self,
125        _ids_str: &'a str,
126    ) -> std::pin::Pin<
127        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
128    > {
129        Box::pin(async { Ok(String::new()) })
130    }
131
132    fn store_command<'a>(
133        &'a mut self,
134        _args: &'a str,
135    ) -> std::pin::Pin<
136        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
137    > {
138        Box::pin(async { Ok(String::new()) })
139    }
140
141    fn guidelines<'a>(
142        &'a mut self,
143    ) -> std::pin::Pin<
144        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
145    > {
146        Box::pin(async { Ok(String::new()) })
147    }
148}
149
150impl GraphAccess for NullAgent {
151    fn graph_stats<'a>(
152        &'a mut self,
153    ) -> std::pin::Pin<
154        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
155    > {
156        Box::pin(async { Ok(String::new()) })
157    }
158
159    fn graph_entities<'a>(
160        &'a mut self,
161    ) -> std::pin::Pin<
162        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
163    > {
164        Box::pin(async { Ok(String::new()) })
165    }
166
167    fn graph_facts<'a>(
168        &'a mut self,
169        _name: &'a str,
170    ) -> std::pin::Pin<
171        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
172    > {
173        Box::pin(async { Ok(String::new()) })
174    }
175
176    fn graph_history<'a>(
177        &'a mut self,
178        _name: &'a str,
179    ) -> std::pin::Pin<
180        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
181    > {
182        Box::pin(async { Ok(String::new()) })
183    }
184
185    fn graph_communities<'a>(
186        &'a mut self,
187    ) -> std::pin::Pin<
188        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
189    > {
190        Box::pin(async { Ok(String::new()) })
191    }
192
193    fn graph_backfill<'a>(
194        &'a mut self,
195        _limit: Option<usize>,
196        _progress_cb: &'a mut (dyn FnMut(String) + Send),
197    ) -> std::pin::Pin<
198        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
199    > {
200        Box::pin(async { Ok(String::new()) })
201    }
202
203    fn knowledge_status<'a>(
204        &'a mut self,
205    ) -> std::pin::Pin<
206        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
207    > {
208        Box::pin(async { Ok(String::new()) })
209    }
210
211    fn knowledge_rollback<'a>(
212        &'a mut self,
213        _batch_id: &'a str,
214    ) -> std::pin::Pin<
215        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
216    > {
217        Box::pin(async { Ok(String::new()) })
218    }
219}
220
221impl ModelAccess for NullAgent {
222    fn handle_caveman<'a>(
223        &'a mut self,
224        _arg: &'a str,
225    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + 'a>> {
226        Box::pin(async { "caveman: unavailable".to_owned() })
227    }
228
229    fn handle_model<'a>(
230        &'a mut self,
231        _arg: &'a str,
232    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + 'a>> {
233        Box::pin(async { String::new() })
234    }
235
236    fn handle_provider<'a>(
237        &'a mut self,
238        _arg: &'a str,
239    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + 'a>> {
240        Box::pin(async { String::new() })
241    }
242
243    fn handle_think_tokens<'a>(
244        &'a mut self,
245        _arg: &'a str,
246    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + 'a>> {
247        Box::pin(async { String::new() })
248    }
249
250    fn handle_reasoning_effort<'a>(
251        &'a mut self,
252        _arg: &'a str,
253    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + 'a>> {
254        Box::pin(async { String::new() })
255    }
256}
257
258impl SkillAccess for NullAgent {
259    fn handle_skill<'a>(
260        &'a mut self,
261        _args: &'a str,
262    ) -> std::pin::Pin<
263        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
264    > {
265        Box::pin(async { Ok(String::new()) })
266    }
267
268    fn handle_skills<'a>(
269        &'a mut self,
270        _args: &'a str,
271    ) -> std::pin::Pin<
272        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
273    > {
274        Box::pin(async { Ok(String::new()) })
275    }
276
277    fn handle_feedback_command<'a>(
278        &'a mut self,
279        _args: &'a str,
280    ) -> std::pin::Pin<
281        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
282    > {
283        Box::pin(async { Ok(String::new()) })
284    }
285}
286
287impl PolicyAccess for NullAgent {
288    fn handle_policy<'a>(
289        &'a mut self,
290        _args: &'a str,
291    ) -> std::pin::Pin<
292        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
293    > {
294        Box::pin(async { Ok(String::new()) })
295    }
296}
297
298impl SchedulerAccess for NullAgent {
299    fn list_scheduled_tasks<'a>(
300        &'a mut self,
301    ) -> std::pin::Pin<
302        Box<
303            dyn std::future::Future<Output = Result<Option<String>, crate::CommandError>>
304                + Send
305                + 'a,
306        >,
307    > {
308        Box::pin(async { Ok(None) })
309    }
310}
311
312impl LspAccess for NullAgent {
313    fn lsp_status<'a>(
314        &'a mut self,
315    ) -> std::pin::Pin<
316        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
317    > {
318        Box::pin(async { Ok(String::new()) })
319    }
320}
321
322impl SessionControlAccess for NullAgent {
323    fn session_recap<'a>(
324        &'a mut self,
325    ) -> std::pin::Pin<
326        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
327    > {
328        Box::pin(async { Ok(String::new()) })
329    }
330
331    fn compact_context<'a>(
332        &'a mut self,
333    ) -> std::pin::Pin<
334        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
335    > {
336        Box::pin(async { Ok(String::new()) })
337    }
338
339    fn reset_conversation<'a>(
340        &'a mut self,
341        _keep_plan: bool,
342        _no_digest: bool,
343    ) -> std::pin::Pin<
344        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
345    > {
346        Box::pin(async { Ok(String::new()) })
347    }
348
349    fn cache_stats(&self) -> String {
350        String::new()
351    }
352
353    fn session_status<'a>(
354        &'a mut self,
355    ) -> std::pin::Pin<
356        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
357    > {
358        Box::pin(async { Ok(String::new()) })
359    }
360
361    fn guardrail_status(&self) -> String {
362        String::new()
363    }
364
365    fn focus_status(&self) -> String {
366        String::new()
367    }
368
369    fn sidequest_status(&self) -> String {
370        String::new()
371    }
372
373    fn load_image<'a>(
374        &'a mut self,
375        _path: &'a str,
376    ) -> std::pin::Pin<
377        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
378    > {
379        Box::pin(async { Ok(String::new()) })
380    }
381}
382
383impl McpAccess for NullAgent {
384    fn handle_mcp<'a>(
385        &'a mut self,
386        _args: &'a str,
387    ) -> std::pin::Pin<
388        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
389    > {
390        Box::pin(async { Ok(String::new()) })
391    }
392}
393
394impl OrchestrationAccess for NullAgent {
395    fn handle_plan<'a>(
396        &'a mut self,
397        _input: &'a str,
398    ) -> std::pin::Pin<
399        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
400    > {
401        Box::pin(async { Ok(String::new()) })
402    }
403
404    fn handle_experiment<'a>(
405        &'a mut self,
406        _input: &'a str,
407    ) -> std::pin::Pin<
408        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
409    > {
410        Box::pin(async { Ok(String::new()) })
411    }
412}
413
414impl SubagentAccess for NullAgent {
415    fn handle_agent_dispatch<'a>(
416        &'a mut self,
417        _input: &'a str,
418    ) -> std::pin::Pin<
419        Box<
420            dyn std::future::Future<Output = Result<Option<String>, crate::CommandError>>
421                + Send
422                + 'a,
423        >,
424    > {
425        Box::pin(async { Ok(None) })
426    }
427}
428
429impl IntegrationAccess for NullAgent {
430    fn handle_plugins<'a>(
431        &'a mut self,
432        _args: &'a str,
433    ) -> std::pin::Pin<
434        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
435    > {
436        Box::pin(async { Ok(String::new()) })
437    }
438
439    fn handle_acp<'a>(
440        &'a mut self,
441        _args: &'a str,
442    ) -> std::pin::Pin<
443        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
444    > {
445        Box::pin(async { Ok(String::new()) })
446    }
447
448    fn handle_cocoon<'a>(
449        &'a mut self,
450        _args: &'a str,
451    ) -> std::pin::Pin<
452        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
453    > {
454        Box::pin(async { Ok(String::new()) })
455    }
456}
457
458impl TrackingAccess for NullAgent {
459    fn handle_trajectory(&mut self, _args: &str) -> String {
460        String::new()
461    }
462
463    fn handle_scope(&self, _args: &str) -> String {
464        String::new()
465    }
466}
467
468impl WorktreeAccess for NullAgent {}
469
470impl MiscAccess for NullAgent {
471    fn handle_loop<'a>(
472        &'a mut self,
473        _args: &'a str,
474    ) -> std::pin::Pin<
475        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
476    > {
477        Box::pin(async { Ok(String::new()) })
478    }
479
480    /// Fire a test notification via all enabled notification channels.
481    ///
482    /// Returns a status message for the user. If all channels are disabled or the
483    /// notifier is not configured, returns a user-visible explanation.
484    ///
485    /// # Errors
486    ///
487    /// Returns `Err` if the notification send fails.
488    fn notify_test<'a>(
489        &'a mut self,
490    ) -> std::pin::Pin<
491        Box<dyn std::future::Future<Output = Result<String, crate::CommandError>> + Send + 'a>,
492    > {
493        Box::pin(async { Ok("Notifications not configured.".to_owned()) })
494    }
495}