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 _ = event_tx.send(RuntimeEvent::RunFinished {
82 session_id: sid,
83 agent_id: None,
84 trace_id: None,
85 });
86 }
87 }
88 }
89 }
90 }
91 });
92
93 Self {
94 cmd_tx,
95 event_rx,
96 runtime,
97 default_session_id: None,
98 }
99 }
100
101 /// Create an AgentHandle with a default session_id
102 /// The session_id will be used for all send_input calls unless overridden
103 pub fn with_session(runtime: AgentRuntime, session_id: SessionId) -> Self {
104 let (cmd_tx, cmd_rx) = mpsc::channel(32);
105 let (event_tx, event_rx) = mpsc::unbounded_channel();
106 let rt = runtime.clone();
107
108 // Worker task: processes user requests serially
109 tokio::spawn(async move {
110 let mut rx = cmd_rx;
111 while let Some(cmd) = rx.recv().await {
112 match cmd {
113 AgentCommand::RunTurn { session_id, input } => {
114 let tx = event_tx.clone();
115 let sid = session_id.clone();
116 let result = rt
117 .run_turn(session_id, &input, move |event| {
118 let _ = tx.send(event);
119 Ok(())
120 })
121 .await;
122
123 // Handle errors: ensure caller always gets a terminal event
124 match &result {
125 Ok(_) => {
126 // run_turn already emitted RunFinished or RunCancelled
127 }
128 Err(e) if e.is_cancelled() => {
129 // RunCancelled already emitted inside run_turn
130 }
131 Err(e) => {
132 // Non-cancellation error: emit RunFinished so caller isn't stuck
133 tracing::error!(error = %e, "run_turn failed");
134 let _ = event_tx.send(RuntimeEvent::RunFinished {
135 session_id: sid,
136 agent_id: None,
137 trace_id: None,
138 });
139 }
140 }
141 }
142 }
143 }
144 });
145
146 Self {
147 cmd_tx,
148 event_rx,
149 runtime,
150 default_session_id: Some(session_id),
151 }
152 }
153
154 /// Send user input (async, with error return)
155 /// Uses the default session_id if set via with_session(), otherwise creates a new session
156 pub async fn send_input(&self, input: &str) -> Result<(), SendError> {
157 let session_id = match &self.default_session_id {
158 Some(id) => id.clone(),
159 None => self.runtime.create_session().await,
160 };
161 self.cmd_tx
162 .send(AgentCommand::RunTurn {
163 session_id,
164 input: input.to_string(),
165 })
166 .await
167 .map_err(|_| SendError::ChannelClosed)
168 }
169
170 /// Send user input with a specified session_id
171 pub async fn send_input_with_session(
172 &self,
173 input: &str,
174 session_id: SessionId,
175 ) -> Result<(), SendError> {
176 self.cmd_tx
177 .send(AgentCommand::RunTurn {
178 session_id,
179 input: input.to_string(),
180 })
181 .await
182 .map_err(|_| SendError::ChannelClosed)
183 }
184
185 /// Receive the next event (blocking)
186 pub async fn recv_event(&mut self) -> Option<RuntimeEvent> {
187 self.event_rx.recv().await
188 }
189
190 /// Try to receive an event (non-blocking)
191 pub fn try_recv_event(&mut self) -> Option<RuntimeEvent> {
192 self.event_rx.try_recv().ok()
193 }
194
195 /// Cancel current execution — delegates to runtime, always cancels the latest token
196 pub fn cancel(&self) {
197 self.runtime.cancel();
198 }
199
200 /// Get a reference to the underlying runtime
201 pub fn runtime(&self) -> &AgentRuntime {
202 &self.runtime
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 #[test]
209 fn test_agent_handle_creation() {
210 // This test requires a full AgentRuntime, skipped for now
211 // Actual testing is done in integration tests
212 }
213}