hermes_bot/agent/mod.rs
1//! Agent abstraction and event model for AI coding agents.
2//!
3//! This module defines the [`Agent`] trait for spawning and communicating with
4//! AI coding agents (like Claude Code CLI). Agents run as subprocesses and
5//! communicate via a bidirectional event stream.
6//!
7//! # Architecture
8//!
9//! - [`Agent`] trait: Interface for spawning agents
10//! - [`AgentHandle`]: Handle for bidirectional communication with a running agent
11//! - [`AgentEvent`]: Events emitted by the agent (text, tool use, completion, etc.)
12//! - [`AgentResponse`]: Final response format for Slack display
13//!
14//! # Implementations
15//!
16//! - [`claude::ClaudeAgent`]: Claude Code CLI integration
17
18pub mod claude;
19pub mod protocol;
20
21use crate::error::Result;
22use async_trait::async_trait;
23use serde_json::Value;
24use std::path::Path;
25use tokio::sync::{mpsc, oneshot};
26
27// ── Event-based agent model ───────────────────────────────────────────
28
29/// Events emitted by an agent during a session.
30///
31/// Agents communicate asynchronously via events. Handlers receive these
32/// events through the `AgentHandle.receiver` channel and respond by:
33/// - Displaying text to the user
34/// - Posting tool activity notifications
35/// - Waiting for user input (questions)
36/// - Finalizing the turn when complete
37///
38/// # Event Flow
39///
40/// 1. `SessionInit` - Session starts (provides session_id)
41/// 2. `Text`, `ToolUse`, `ToolProgress` - Activity during the turn (may repeat)
42/// 3. `QuestionPending` - If Claude asks a question (blocks until answered)
43/// 4. `TurnComplete` - Turn finishes successfully
44///
45/// Alternatively, `ProcessExited` can occur at any time if the agent crashes.
46#[derive(Debug)]
47pub enum AgentEvent {
48 /// Session initialized (carries session_id, model).
49 SessionInit { session_id: String, model: String },
50 /// Text content from Claude.
51 Text(String),
52 /// Claude is using a tool.
53 ToolUse { name: String, input: Value },
54 /// Claude is asking the user a question via control_request (AskUserQuestion tool).
55 QuestionPending {
56 request_id: String,
57 questions: Value,
58 },
59 /// A turn completed.
60 TurnComplete {
61 result: Option<String>,
62 subtype: String,
63 num_turns: u32,
64 duration_ms: u64,
65 is_error: bool,
66 session_id: String,
67 },
68 /// Claude wants to use a tool that requires user approval.
69 ToolApprovalPending {
70 request_id: String,
71 tool_name: String,
72 tool_input: Value,
73 },
74 /// Tool progress heartbeat.
75 ToolProgress { tool_name: String },
76 /// Agent process exited unexpectedly.
77 ProcessExited { code: Option<i32> },
78}
79
80/// Handle to a running agent session for bidirectional communication.
81///
82/// Provides channels for:
83/// - Sending user prompts to the agent (`sender`)
84/// - Receiving events from the agent (`receiver`)
85/// - Sending raw protocol messages like control responses (`stdin_tx`)
86/// - Killing the agent process (`kill_tx`)
87///
88/// The handle is returned by [`Agent::spawn`] and remains valid until the
89/// agent process exits or is killed.
90pub struct AgentHandle {
91 /// Send user messages to the agent.
92 pub sender: mpsc::Sender<String>,
93 /// Receive events from the agent.
94 pub receiver: mpsc::Receiver<AgentEvent>,
95 /// Kill the agent process.
96 pub kill_tx: Option<oneshot::Sender<()>>,
97 /// Session ID (set after SessionInit event).
98 pub session_id: Option<String>,
99 /// Send raw JSON lines to the agent's stdin (for control responses).
100 pub stdin_tx: mpsc::Sender<String>,
101}
102
103/// Trait for agent backends (e.g., Claude Code CLI).
104///
105/// Implementations provide the interface to spawn and communicate with
106/// AI coding agents. The agent runs as a subprocess and communicates via
107/// stdin/stdout using a stream-json protocol.
108///
109/// # Example
110///
111/// ```ignore
112/// let agent = ClaudeAgent;
113/// let handle = agent.spawn(
114/// Path::new("/path/to/repo"),
115/// &["Read", "Write", "Bash"],
116/// Some("You are a helpful assistant"),
117/// None, // New session
118/// Some("claude-opus-4-6"),
119/// ).await?;
120///
121/// // Send a prompt
122/// handle.sender.send("Fix the login bug".to_string()).await?;
123///
124/// // Receive events
125/// while let Some(event) = handle.receiver.recv().await {
126/// match event {
127/// AgentEvent::Text(text) => println!("{}", text),
128/// AgentEvent::TurnComplete { .. } => break,
129/// _ => {}
130/// }
131/// }
132/// ```
133#[async_trait]
134pub trait Agent: Send + Sync {
135 /// Spawns a new agent session in the specified repository.
136 ///
137 /// # Arguments
138 ///
139 /// * `repo_path` - Working directory for the agent (git repo root)
140 /// * `allowed_tools` - List of tools the agent can use without asking permission
141 /// * `system_prompt` - Optional additional system prompt to append
142 /// * `resume_session_id` - If set, resumes an existing session instead of starting new
143 /// * `model` - Model to use (e.g., "claude-opus-4-6"). Only used for new sessions.
144 ///
145 /// # Returns
146 ///
147 /// An `AgentHandle` for communicating with the spawned agent.
148 ///
149 /// # Errors
150 ///
151 /// Returns an error if:
152 /// - The agent binary is not found in PATH
153 /// - The subprocess fails to spawn
154 /// - Required stdio streams cannot be captured
155 async fn spawn(
156 &self,
157 repo_path: &Path,
158 allowed_tools: &[String],
159 system_prompt: Option<&str>,
160 resume_session_id: Option<&str>,
161 model: Option<&str>,
162 ) -> Result<AgentHandle>;
163}