Skip to main content

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}