1use std::time::Duration;
40
41use pyo3::prelude::*;
42use tokio::sync::{mpsc, oneshot};
43
44use crate::error::Error;
45
46pub(crate) mod bridge_state;
47pub(crate) mod command_loop;
48mod config;
49pub(crate) mod ffi_dispatch;
50mod handlers;
51pub(crate) mod py_scripts;
52pub(crate) mod streaming;
53pub(crate) mod venv;
54
55#[cfg(test)]
56mod tests;
57
58pub(crate) use bridge_state::{AgentBridgeState, AgentId, bridge_state, next_agent_id};
60pub use config::{BackendLogLevel, RuntimeConfig};
61pub(crate) use ffi_dispatch::{
62 dispatch_rust_hook, dispatch_rust_policy_confirm, dispatch_rust_tool,
63 initializing_hook_runners, set_agent_conversation_id,
64};
65
66pub const DEFAULT_INTER_AGENT_DELAY: Duration = Duration::from_millis(500);
68
69const DEFAULT_CHANNEL_CAPACITY: usize = 64;
71
72const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
74
75pub(crate) enum PyCommand {
80 CreateAgent {
85 agent_id: u64,
86 config_json: String,
87 reply: oneshot::Sender<Result<(AgentId, Vec<handlers::agent::RawToolInfo>), Error>>,
88 },
89 Chat {
91 agent_id: AgentId,
92 prompt: String,
93 reply: oneshot::Sender<Result<crate::streaming::ChatResponseHandle, Error>>,
94 },
95 ShutdownAgent {
97 agent_id: AgentId,
98 reply: oneshot::Sender<Result<(), Error>>,
99 },
100 Cancel {
102 agent_id: AgentId,
103 reply: oneshot::Sender<Result<(), Error>>,
104 },
105 WaitForIdle {
107 agent_id: AgentId,
108 reply: oneshot::Sender<Result<(), Error>>,
109 },
110 Send {
112 agent_id: AgentId,
113 prompt: String,
114 reply: oneshot::Sender<Result<(), Error>>,
115 },
116 SignalIdle {
118 agent_id: AgentId,
119 reply: oneshot::Sender<Result<(), Error>>,
120 },
121 WaitForWakeup {
123 agent_id: AgentId,
124 timeout_secs: f64,
125 reply: oneshot::Sender<Result<bool, Error>>,
126 },
127 Shutdown,
129 GetHistory {
131 agent_id: AgentId,
132 reply: oneshot::Sender<Result<Vec<crate::types::ConversationMessage>, Error>>,
133 },
134 GetTurnCount {
136 agent_id: AgentId,
137 reply: oneshot::Sender<Result<u32, Error>>,
138 },
139 GetActiveAgentCount {
147 reply: oneshot::Sender<Result<usize, Error>>,
148 },
149 GetTotalUsage {
151 agent_id: AgentId,
152 reply: oneshot::Sender<Result<crate::types::UsageMetadata, Error>>,
153 },
154 GetLastTurnUsage {
156 agent_id: AgentId,
157 reply: oneshot::Sender<Result<crate::types::UsageMetadata, Error>>,
158 },
159 ClearHistory {
161 agent_id: AgentId,
162 reply: oneshot::Sender<Result<(), Error>>,
163 },
164 GetCompactionIndices {
166 agent_id: AgentId,
167 reply: oneshot::Sender<Result<Vec<u32>, Error>>,
168 },
169 GetLastResponse {
171 agent_id: AgentId,
172 reply: oneshot::Sender<Result<Option<String>, Error>>,
173 },
174 Delete {
179 agent_id: AgentId,
180 reply: oneshot::Sender<Result<(), Error>>,
181 },
182 Disconnect {
186 agent_id: AgentId,
187 reply: oneshot::Sender<Result<(), Error>>,
188 },
189 IsIdle {
193 agent_id: AgentId,
194 reply: oneshot::Sender<Result<bool, Error>>,
195 },
196}
197
198pub struct PythonRuntime {
203 cmd_tx: Option<mpsc::Sender<PyCommand>>,
204 thread: Option<std::thread::JoinHandle<()>>,
205 config: RuntimeConfig,
206}
207
208impl std::fmt::Debug for PythonRuntime {
209 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210 f.debug_struct("PythonRuntime")
211 .field("config", &self.config)
212 .field(
213 "thread_running",
214 &self.thread.as_ref().is_some_and(|t| !t.is_finished()),
215 )
216 .finish_non_exhaustive()
217 }
218}
219
220impl PythonRuntime {
221 pub fn new(config: RuntimeConfig) -> Result<Self, Error> {
231 let (cmd_tx, cmd_rx) = mpsc::channel(config.channel_capacity);
232
233 let thread_config = config.clone();
234 let thread = std::thread::Builder::new()
235 .name("agy-bridge-python-runtime".into())
236 .spawn(move || {
237 python_thread_main(cmd_rx, &thread_config);
238 })
239 .map_err(|e| Error::BackendError {
240 message: format!("Failed to spawn Python runtime thread: {e}"),
241 })?;
242
243 Ok(Self {
244 cmd_tx: Some(cmd_tx),
245 thread: Some(thread),
246 config,
247 })
248 }
249
250 async fn send_command<T>(
259 &self,
260 operation: &str,
261 build_cmd: impl FnOnce(oneshot::Sender<Result<T, Error>>) -> PyCommand,
262 ) -> Result<T, Error> {
263 let Some(ref tx) = self.cmd_tx else {
264 return Err(Error::ChannelClosed {
265 message: format!("Python runtime thread is shut down (sending {operation})"),
266 });
267 };
268
269 let (reply_tx, reply_rx) = oneshot::channel();
270 let cmd = build_cmd(reply_tx);
271
272 tx.send(cmd).await.map_err(|e| Error::ChannelClosed {
273 message: format!("Python runtime thread has exited (sending {operation}): {e}"),
274 })?;
275
276 let result = reply_rx.await.map_err(|e| Error::ChannelClosed {
277 message: format!("Reply channel dropped for {operation}: {e}"),
278 })??;
279
280 Ok(result)
281 }
282
283 pub(crate) async fn active_agent_count(&self) -> Result<usize, Error> {
292 self.send_command("active_agent_count", |reply| {
293 PyCommand::GetActiveAgentCount { reply }
294 })
295 .await
296 }
297
298 pub async fn shutdown(mut self) -> Result<(), Error> {
306 if let Some(tx) = self.cmd_tx.take()
308 && let Err(e) = tx.send(PyCommand::Shutdown).await
309 {
310 tracing::warn!("Shutdown command send failed (thread may already be exiting): {e}");
311 }
312
313 let Some(thread) = self.thread.take() else {
316 tracing::warn!("PythonRuntime::shutdown() called but thread handle already taken");
317 return Ok(());
318 };
319
320 let shutdown_timeout = self.config.shutdown_timeout;
321 let join_result = tokio::time::timeout(
322 shutdown_timeout,
323 tokio::task::spawn_blocking(move || thread.join()),
324 )
325 .await;
326
327 match join_result {
328 Ok(Ok(Ok(()))) => {
329 tracing::info!("Python runtime thread joined successfully");
330 Ok(())
331 }
332 Ok(Ok(Err(panic_payload))) => {
333 let panic_msg = panic_payload.downcast_ref::<&str>().map_or_else(
334 || {
335 panic_payload
336 .downcast_ref::<String>()
337 .map_or_else(|| format!("{panic_payload:?}"), Clone::clone)
338 },
339 |s| (*s).to_string(),
340 );
341 tracing::error!(
342 panic_message = %panic_msg,
343 "Python runtime thread panicked during shutdown"
344 );
345 Err(Error::BackendError {
346 message: format!("Python runtime thread panicked during shutdown: {panic_msg}"),
347 })
348 }
349 Ok(Err(join_err)) => {
350 tracing::error!("spawn_blocking join error: {join_err}");
351 Err(Error::BackendError {
352 message: format!("Failed to join Python thread: {join_err}"),
353 })
354 }
355 Err(_elapsed) => {
356 tracing::error!(
357 timeout_secs = shutdown_timeout.as_secs(),
358 "Python runtime thread did not exit within shutdown timeout"
359 );
360 Err(Error::Timeout {
361 duration: shutdown_timeout,
362 operation: "PythonRuntime::shutdown (thread join)".to_string(),
363 })
364 }
365 }
366 }
367}
368
369impl Drop for PythonRuntime {
370 fn drop(&mut self) {
371 let Some(thread) = self.thread.take() else {
374 return;
375 };
376
377 let tx = self.cmd_tx.take();
379 if let Some(ref tx) = tx
380 && let Err(e) = tx.try_send(PyCommand::Shutdown)
381 {
382 tracing::debug!(
383 error = %e,
384 "PythonRuntime::drop: could not eagerly signal shutdown; \
385 relying on channel close"
386 );
387 }
388 drop(tx);
390
391 let deadline = std::time::Instant::now() + self.config.shutdown_timeout;
396 while !thread.is_finished() && std::time::Instant::now() < deadline {
397 std::thread::sleep(std::time::Duration::from_millis(5));
398 }
399
400 if thread.is_finished() {
401 if thread.join().is_err() {
402 tracing::error!("Python runtime thread panicked during drop cleanup");
403 } else {
404 tracing::debug!("Python runtime thread joined cleanly on drop");
405 }
406 } else {
407 tracing::warn!(
411 "Python runtime thread still running after shutdown timeout during drop — \
412 detaching; agent cleanup will complete asynchronously"
413 );
414 }
415 }
416}
417
418fn python_thread_main(cmd_rx: mpsc::Receiver<PyCommand>, config: &RuntimeConfig) {
420 Python::initialize();
421
422 Python::attach(|py| {
427 if let Err(e) = venv::configure_python_sys_path(py) {
428 tracing::error!(
429 error = %e,
430 "Failed to configure Python sys.path in runtime thread — \
431 venv imports will likely fail"
432 );
433 }
434 });
435
436 if let Err(e) = run_live_thread(cmd_rx, config) {
437 tracing::error!(error = %e, "Python runtime thread failed");
438 }
439
440 tracing::info!("Python runtime thread exiting");
441}
442
443fn run_live_thread(cmd_rx: mpsc::Receiver<PyCommand>, config: &RuntimeConfig) -> Result<(), Error> {
446 Python::attach(|py| {
447 let asyncio = py.import("asyncio").map_err(|e| Error::BackendError {
448 message: format!("Failed to import asyncio: {e}"),
449 })?;
450 let event_loop =
451 asyncio
452 .call_method0("new_event_loop")
453 .map_err(|e| Error::BackendError {
454 message: format!("Failed to create new asyncio event loop: {e}"),
455 })?;
456 asyncio
457 .call_method1("set_event_loop", (&event_loop,))
458 .map_err(|e| Error::BackendError {
459 message: format!("Failed to set asyncio event loop: {e}"),
460 })?;
461
462 let sys = py.import("sys").map_err(|e| Error::BackendError {
467 message: format!("Failed to import sys: {e}"),
468 })?;
469 let sys_modules = sys.getattr("modules").map_err(|e| Error::BackendError {
470 message: format!("Failed to get sys.modules: {e}"),
471 })?;
472 let globals_mod = if sys_modules
473 .contains(command_loop::AGY_BRIDGE_GLOBALS_MODULE)
474 .map_err(|e| Error::BackendError {
475 message: format!("Failed to check sys.modules: {e}"),
476 })? {
477 sys_modules
478 .get_item(command_loop::AGY_BRIDGE_GLOBALS_MODULE)
479 .map_err(|e| Error::BackendError {
480 message: format!("Failed to get _agy_bridge_globals: {e}"),
481 })?
482 } else {
483 let types = py.import("types").map_err(|e| Error::BackendError {
484 message: format!("Failed to import types: {e}"),
485 })?;
486 let module = types
487 .getattr("ModuleType")
488 .map_err(|e| Error::BackendError {
489 message: format!("Failed to get ModuleType: {e}"),
490 })?
491 .call1((command_loop::AGY_BRIDGE_GLOBALS_MODULE,))
492 .map_err(|e| Error::BackendError {
493 message: format!("Failed to create ModuleType: {e}"),
494 })?;
495 sys_modules
496 .set_item(command_loop::AGY_BRIDGE_GLOBALS_MODULE, &module)
497 .map_err(|e| Error::BackendError {
498 message: format!("Failed to register _agy_bridge_globals: {e}"),
499 })?;
500 module
501 };
502 globals_mod
503 .setattr("EVENT_LOOP", &event_loop)
504 .map_err(|e| Error::BackendError {
505 message: format!("Failed to set EVENT_LOOP in globals: {e}"),
506 })?;
507 register_thread_event_loop(py, &globals_mod, &event_loop)?;
508
509 tracing::info!("Python asyncio event loop created on runtime thread");
510
511 let inter_agent_delay = config.inter_agent_delay;
512 let stream_limits = streaming::StreamLimits::from_config(config);
513 let event_loop_obj = event_loop.clone().unbind();
514 let run_fut =
515 pyo3_async_runtimes::tokio::run_until_complete(event_loop.clone(), async move {
516 command_loop::run_async_command_loop(
517 event_loop_obj,
518 cmd_rx,
519 inter_agent_delay,
520 stream_limits,
521 )
522 .await
523 });
524
525 unregister_thread_event_loop(py, &globals_mod);
526
527 if let Err(e) = run_fut {
528 if let Err(close_err) = event_loop.call_method0("close") {
530 tracing::warn!("Failed to close asyncio event loop: {close_err}");
531 }
532 return Err(Error::BackendError {
533 message: format!("Python runtime command loop failed: {e}"),
534 });
535 }
536
537 if let Err(e) = event_loop.call_method0("close") {
538 tracing::warn!("Failed to close asyncio event loop: {e}");
539 }
540
541 Ok(())
542 })
543}
544
545fn register_thread_event_loop(
555 py: Python<'_>,
556 globals_mod: &Bound<'_, PyAny>,
557 event_loop: &Bound<'_, PyAny>,
558) -> Result<(), Error> {
559 let threading = py.import("threading").map_err(|e| Error::BackendError {
560 message: format!("Failed to import threading for event-loop registration: {e}"),
561 })?;
562 let thread_id = threading
563 .call_method0("get_ident")
564 .map_err(|e| Error::BackendError {
565 message: format!("Failed to read threading.get_ident(): {e}"),
566 })?;
567
568 let loops = if globals_mod
569 .hasattr("EVENT_LOOPS")
570 .map_err(|e| Error::BackendError {
571 message: format!("Failed to check for EVENT_LOOPS attribute: {e}"),
572 })? {
573 globals_mod
574 .getattr("EVENT_LOOPS")
575 .map_err(|e| Error::BackendError {
576 message: format!("Failed to get EVENT_LOOPS map: {e}"),
577 })?
578 } else {
579 let dict = pyo3::types::PyDict::new(py).into_any();
580 globals_mod
581 .setattr("EVENT_LOOPS", &dict)
582 .map_err(|e| Error::BackendError {
583 message: format!("Failed to create EVENT_LOOPS map: {e}"),
584 })?;
585 dict
586 };
587
588 loops
589 .set_item(thread_id, event_loop)
590 .map_err(|e| Error::BackendError {
591 message: format!("Failed to register runtime event loop by thread id: {e}"),
592 })?;
593 Ok(())
594}
595
596fn unregister_thread_event_loop(py: Python<'_>, globals_mod: &Bound<'_, PyAny>) {
599 let unregister_res = (|| -> PyResult<()> {
600 let threading = py.import("threading")?;
601 let thread_id = threading.call_method0("get_ident")?;
602 if globals_mod.hasattr("EVENT_LOOPS")? {
603 let loops = globals_mod.getattr("EVENT_LOOPS")?;
604 let dict = loops.cast::<pyo3::types::PyDict>()?;
605 dict.del_item(thread_id)?;
606 }
607 Ok(())
608 })();
609 if let Err(e) = unregister_res {
610 tracing::debug!(error = %e, "Failed to unregister thread event loop on teardown");
611 }
612}
613
614fn compute_active_builtins(
621 config: &crate::config::AgentConfig,
622) -> Vec<crate::config::BuiltinTools> {
623 let Some(caps) = config.capabilities.as_ref() else {
624 return crate::config::BuiltinTools::all_tools().to_vec();
625 };
626
627 if let Some(enabled) = caps.enabled_tools.as_ref() {
630 return enabled.clone();
631 }
632
633 if let Some(disabled) = caps.disabled_tools.as_ref() {
635 return crate::config::BuiltinTools::all_tools()
636 .iter()
637 .filter(|t| !disabled.contains(t))
638 .cloned()
639 .collect();
640 }
641
642 crate::config::BuiltinTools::all_tools().to_vec()
644}
645
646impl crate::agent::Runtime for PythonRuntime {
647 async fn create_agent(
648 &self,
649 agent_id: u64,
650 config: crate::config::AgentConfig,
651 ) -> Result<(crate::agent::AgentId, Vec<crate::tools::AvailableTool>), Error> {
652 if let Some(save_dir) = config.save_dir.as_ref()
661 && let Err(e) = std::fs::create_dir_all(save_dir)
662 {
663 tracing::warn!(
664 save_dir = %save_dir.display(),
665 error = ?e,
666 "Failed to create save_dir; conversation state may not persist \
667 and resume may fail with \"conversation not found\""
668 );
669 }
670
671 let config_json = {
675 let mut val = serde_json::to_value(&config).map_err(|e| Error::BackendError {
676 message: format!("Failed to serialize AgentConfig: {e}"),
677 })?;
678 if let serde_json::Value::Object(ref mut map) = val {
679 map.insert(
680 "_backend_log_level".to_owned(),
681 serde_json::Value::String(self.config.backend_log_level.as_str().to_owned()),
682 );
683 }
684 serde_json::to_string(&val).map_err(|e| Error::BackendError {
685 message: format!("Failed to re-serialize config JSON: {e}"),
686 })?
687 };
688
689 let custom_tool_names: std::collections::HashSet<String> =
691 config.tools.iter().map(|t| t.name.clone()).collect();
692
693 let (raw_id, raw_tools) = self
694 .send_command("create_agent", |reply| PyCommand::CreateAgent {
695 agent_id,
696 config_json,
697 reply,
698 })
699 .await?;
700
701 let active_builtins = compute_active_builtins(&config);
703 let builtin_names: std::collections::HashSet<&str> = active_builtins
704 .iter()
705 .map(crate::config::BuiltinTools::as_sdk_name)
706 .collect();
707
708 let mut available_tools: Vec<crate::tools::AvailableTool> = raw_tools
712 .into_iter()
713 .filter(|raw| !builtin_names.contains(raw.name.as_str()))
714 .map(|raw| {
715 let source = if custom_tool_names.contains(&raw.name) {
716 crate::tools::ToolSource::Custom
717 } else {
718 crate::tools::ToolSource::Mcp
719 };
720 crate::tools::AvailableTool {
721 name: raw.name,
722 description: raw.description,
723 parameter_schema: raw.parameter_schema,
724 source,
725 }
726 })
727 .collect();
728
729 for builtin in active_builtins {
731 available_tools.push(crate::tools::AvailableTool {
732 name: builtin.as_sdk_name().to_owned(),
733 description: builtin.description().to_owned(),
734 parameter_schema: serde_json::Value::Null,
735 source: crate::tools::ToolSource::Builtin,
736 });
737 }
738
739 tracing::info!(
740 agent_id = raw_id.0,
741 tool_count = available_tools.len(),
742 tools = ?available_tools.iter().map(|t| format!("{t}")).collect::<Vec<_>>(),
743 "Agent created with available tools"
744 );
745
746 Ok((raw_id.0, available_tools))
747 }
748
749 async fn chat(
750 &self,
751 agent_id: crate::agent::AgentId,
752 content: &crate::content::Content,
753 ) -> Result<crate::streaming::ChatResponseHandle, Error> {
754 let prompt = match content {
755 crate::content::Content::Text { text } => text.clone(),
756 other => crate::content::content_to_json(other)?,
757 };
758 self.send_command("chat", |reply| PyCommand::Chat {
759 agent_id: AgentId(agent_id),
760 prompt,
761 reply,
762 })
763 .await
764 }
765
766 async fn shutdown_agent(&self, agent_id: crate::agent::AgentId) -> Result<(), Error> {
767 self.send_command("shutdown_agent", |reply| PyCommand::ShutdownAgent {
768 agent_id: AgentId(agent_id),
769 reply,
770 })
771 .await
772 }
773
774 fn try_shutdown_agent(&self, agent_id: crate::agent::AgentId) {
775 if let Some(ref tx) = self.cmd_tx {
779 let (reply, _) = oneshot::channel();
780 if let Err(e) = tx.try_send(PyCommand::ShutdownAgent {
781 agent_id: AgentId(agent_id),
782 reply,
783 }) {
784 tracing::debug!(
785 agent_id = agent_id,
786 error = %e,
787 "try_shutdown_agent: channel send failed (runtime may already be gone)"
788 );
789 }
790 }
791 }
792
793 async fn cancel(&self, agent_id: crate::agent::AgentId) -> Result<(), Error> {
794 self.send_command("cancel", |reply| PyCommand::Cancel {
795 agent_id: AgentId(agent_id),
796 reply,
797 })
798 .await
799 }
800
801 async fn wait_for_idle(&self, agent_id: crate::agent::AgentId) -> Result<(), Error> {
802 self.send_command("wait_for_idle", |reply| PyCommand::WaitForIdle {
803 agent_id: AgentId(agent_id),
804 reply,
805 })
806 .await
807 }
808
809 async fn send(
810 &self,
811 agent_id: crate::agent::AgentId,
812 content: &crate::content::Content,
813 ) -> Result<(), Error> {
814 let prompt = match content {
815 crate::content::Content::Text { text } => text.clone(),
816 other => crate::content::content_to_json(other)?,
817 };
818 self.send_command("send", |reply| PyCommand::Send {
819 agent_id: AgentId(agent_id),
820 prompt,
821 reply,
822 })
823 .await
824 }
825
826 async fn signal_idle(&self, agent_id: crate::agent::AgentId) -> Result<(), Error> {
827 self.send_command("signal_idle", |reply| PyCommand::SignalIdle {
828 agent_id: AgentId(agent_id),
829 reply,
830 })
831 .await
832 }
833
834 async fn wait_for_wakeup(
835 &self,
836 agent_id: crate::agent::AgentId,
837 timeout: std::time::Duration,
838 ) -> Result<bool, Error> {
839 self.send_command("wait_for_wakeup", |reply| PyCommand::WaitForWakeup {
840 agent_id: AgentId(agent_id),
841 timeout_secs: timeout.as_secs_f64(),
842 reply,
843 })
844 .await
845 }
846
847 async fn history(
848 &self,
849 agent_id: crate::agent::AgentId,
850 ) -> Result<Vec<crate::types::ConversationMessage>, Error> {
851 self.send_command("get_history", |reply| PyCommand::GetHistory {
852 agent_id: AgentId(agent_id),
853 reply,
854 })
855 .await
856 }
857
858 async fn turn_count(&self, agent_id: crate::agent::AgentId) -> Result<u32, Error> {
859 self.send_command("get_turn_count", |reply| PyCommand::GetTurnCount {
860 agent_id: AgentId(agent_id),
861 reply,
862 })
863 .await
864 }
865
866 async fn total_usage(
867 &self,
868 agent_id: crate::agent::AgentId,
869 ) -> Result<crate::types::UsageMetadata, Error> {
870 self.send_command("get_total_usage", |reply| PyCommand::GetTotalUsage {
871 agent_id: AgentId(agent_id),
872 reply,
873 })
874 .await
875 }
876
877 async fn last_turn_usage(
878 &self,
879 agent_id: crate::agent::AgentId,
880 ) -> Result<crate::types::UsageMetadata, Error> {
881 self.send_command("get_last_turn_usage", |reply| PyCommand::GetLastTurnUsage {
882 agent_id: AgentId(agent_id),
883 reply,
884 })
885 .await
886 }
887
888 async fn clear_history(&self, agent_id: crate::agent::AgentId) -> Result<(), Error> {
889 self.send_command("clear_history", |reply| PyCommand::ClearHistory {
890 agent_id: AgentId(agent_id),
891 reply,
892 })
893 .await
894 }
895
896 async fn compaction_indices(&self, agent_id: crate::agent::AgentId) -> Result<Vec<u32>, Error> {
897 self.send_command("compaction_indices", |reply| {
898 PyCommand::GetCompactionIndices {
899 agent_id: AgentId(agent_id),
900 reply,
901 }
902 })
903 .await
904 }
905
906 async fn last_response(
907 &self,
908 agent_id: crate::agent::AgentId,
909 ) -> Result<Option<String>, Error> {
910 self.send_command("last_response", |reply| PyCommand::GetLastResponse {
911 agent_id: AgentId(agent_id),
912 reply,
913 })
914 .await
915 }
916
917 async fn delete(&self, agent_id: crate::agent::AgentId) -> Result<(), Error> {
918 self.send_command("delete", |reply| PyCommand::Delete {
919 agent_id: AgentId(agent_id),
920 reply,
921 })
922 .await
923 }
924
925 async fn disconnect(&self, agent_id: crate::agent::AgentId) -> Result<(), Error> {
926 self.send_command("disconnect", |reply| PyCommand::Disconnect {
927 agent_id: AgentId(agent_id),
928 reply,
929 })
930 .await
931 }
932
933 async fn is_idle(&self, agent_id: crate::agent::AgentId) -> Result<bool, Error> {
934 self.send_command("is_idle", |reply| PyCommand::IsIdle {
935 agent_id: AgentId(agent_id),
936 reply,
937 })
938 .await
939 }
940}