Skip to main content

agent_works/
handle.rs

1use tokio::sync::mpsc;
2
3use agent_base::{AgentRuntime, RuntimeEvent, SessionId};
4
5/// Agent session handle — unified input/output/cancel interface
6///
7/// `AgentHandle` wraps a command queue, a Worker task, an event stream, and cancellation.
8/// All callers (CLI / UI / HTTP API) interact with agent-base through it.
9///
10/// # Example
11///
12/// ```rust,no_run
13/// use agent_works::AgentHandle;
14/// use agent_base::AgentRuntime;
15///
16/// # async fn example(runtime: AgentRuntime) {
17/// let mut handle = AgentHandle::new(runtime);
18///
19/// // Send user input
20/// handle.send_input("check disk space").await.unwrap();
21///
22/// // Receive events
23/// while let Some(event) = handle.recv_event().await {
24///     // Handle event...
25///     if matches!(event, agent_base::RuntimeEvent::RunFinished { .. }
26///         | agent_base::RuntimeEvent::RunCancelled { .. }) {
27///         break;
28///     }
29/// }
30///
31/// // Cancel current execution
32/// handle.cancel();
33/// # }
34/// ```
35pub struct AgentHandle {
36    cmd_tx: mpsc::Sender<AgentCommand>,
37    event_rx: mpsc::UnboundedReceiver<RuntimeEvent>,
38    runtime: AgentRuntime,
39    default_session_id: Option<SessionId>,
40}
41
42enum AgentCommand {
43    RunTurn {
44        session_id: SessionId,
45        input: String,
46    },
47}
48
49#[derive(Debug)]
50pub enum SendError {
51    ChannelClosed,
52}
53
54impl AgentHandle {
55    /// Create a new AgentHandle, spawning a background Worker
56    pub fn new(runtime: AgentRuntime) -> Self {
57        let (cmd_tx, cmd_rx) = mpsc::channel(32);
58        let (event_tx, event_rx) = mpsc::unbounded_channel();
59        let rt = runtime.clone();
60
61        // Worker task: processes user requests serially
62        tokio::spawn(async move {
63            let mut rx = cmd_rx;
64            while let Some(cmd) = rx.recv().await {
65                match cmd {
66                    AgentCommand::RunTurn { session_id, input } => {
67                        let tx = event_tx.clone();
68                        let sid = session_id.clone();
69                        let result = rt
70                            .run_turn(session_id, &input, move |event| {
71                                let _ = tx.send(event);
72                                Ok(())
73                            })
74                            .await;
75
76                        match &result {
77                            Ok(_) => {}
78                            Err(e) if e.is_cancelled() => {}
79                            Err(e) => {
80                                tracing::error!(error = %e, "run_turn failed");
81                                let _ =
82                                    event_tx.send(RuntimeEvent::RunFinished { session_id: sid });
83                            }
84                        }
85                    }
86                }
87            }
88        });
89
90        Self {
91            cmd_tx,
92            event_rx,
93            runtime,
94            default_session_id: None,
95        }
96    }
97
98    /// Create an AgentHandle with a default session_id
99    /// The session_id will be used for all send_input calls unless overridden
100    pub fn with_session(runtime: AgentRuntime, session_id: SessionId) -> Self {
101        let (cmd_tx, cmd_rx) = mpsc::channel(32);
102        let (event_tx, event_rx) = mpsc::unbounded_channel();
103        let rt = runtime.clone();
104
105        // Worker task: processes user requests serially
106        tokio::spawn(async move {
107            let mut rx = cmd_rx;
108            while let Some(cmd) = rx.recv().await {
109                match cmd {
110                    AgentCommand::RunTurn { session_id, input } => {
111                        let tx = event_tx.clone();
112                        let sid = session_id.clone();
113                        let result = rt
114                            .run_turn(session_id, &input, move |event| {
115                                let _ = tx.send(event);
116                                Ok(())
117                            })
118                            .await;
119
120                        // Handle errors: ensure caller always gets a terminal event
121                        match &result {
122                            Ok(_) => {
123                                // run_turn already emitted RunFinished or RunCancelled
124                            }
125                            Err(e) if e.is_cancelled() => {
126                                // RunCancelled already emitted inside run_turn
127                            }
128                            Err(e) => {
129                                // Non-cancellation error: emit RunFinished so caller isn't stuck
130                                tracing::error!(error = %e, "run_turn failed");
131                                let _ =
132                                    event_tx.send(RuntimeEvent::RunFinished { session_id: sid });
133                            }
134                        }
135                    }
136                }
137            }
138        });
139
140        Self {
141            cmd_tx,
142            event_rx,
143            runtime,
144            default_session_id: Some(session_id),
145        }
146    }
147
148    /// Send user input (async, with error return)
149    /// Uses the default session_id if set via with_session(), otherwise creates a new session
150    pub async fn send_input(&self, input: &str) -> Result<(), SendError> {
151        let session_id = match &self.default_session_id {
152            Some(id) => id.clone(),
153            None => self.runtime.create_session().await,
154        };
155        self.cmd_tx
156            .send(AgentCommand::RunTurn {
157                session_id,
158                input: input.to_string(),
159            })
160            .await
161            .map_err(|_| SendError::ChannelClosed)
162    }
163
164    /// Send user input with a specified session_id
165    pub async fn send_input_with_session(
166        &self,
167        input: &str,
168        session_id: SessionId,
169    ) -> Result<(), SendError> {
170        self.cmd_tx
171            .send(AgentCommand::RunTurn {
172                session_id,
173                input: input.to_string(),
174            })
175            .await
176            .map_err(|_| SendError::ChannelClosed)
177    }
178
179    /// Receive the next event (blocking)
180    pub async fn recv_event(&mut self) -> Option<RuntimeEvent> {
181        self.event_rx.recv().await
182    }
183
184    /// Try to receive an event (non-blocking)
185    pub fn try_recv_event(&mut self) -> Option<RuntimeEvent> {
186        self.event_rx.try_recv().ok()
187    }
188
189    /// Cancel current execution — delegates to runtime, always cancels the latest token
190    pub fn cancel(&self) {
191        self.runtime.cancel();
192    }
193
194    /// Get a reference to the underlying runtime
195    pub fn runtime(&self) -> &AgentRuntime {
196        &self.runtime
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    #[test]
203    fn test_agent_handle_creation() {
204        // This test requires a full AgentRuntime, skipped for now
205        // Actual testing is done in integration tests
206    }
207}