agentmux_srv/server/agent_handlers/
container_turn.rs1use std::sync::Arc;
14
15use serde_json::{json, Value};
16
17use crate::backend::cli_notice::{self, InstallState};
18use crate::backend::container::{CliProvision, ContainerError, ContainerManager, ContainerMountSpec};
19use crate::backend::container_cli::CliInstall;
20use crate::backend::container_image::resolve_container_image;
21
22pub(crate) struct ContainerTurnPrep<'a> {
24 pub cm: &'a ContainerManager,
25 pub block_id: &'a str,
26 pub container_name: &'a str,
27 pub image: &'a str,
29 pub volumes: &'a [String],
30 pub mount_spec: &'a ContainerMountSpec,
31 pub provider_id: &'a str,
32 pub container_command: &'a str,
33 pub broker: &'a Arc<crate::backend::mps::Broker>,
34 pub filestore: &'a Arc<crate::backend::storage::filestore::FileStore>,
35 pub mstore: &'a Arc<crate::backend::storage::store::Store>,
36}
37
38pub(crate) fn error_result_frame(message: &str) -> Value {
40 json!({
41 "type": "result",
42 "is_error": true,
43 "subtype": "error_during_execution",
44 "error": {"message": format!("[AgentMux] {message}")}
45 })
46}
47
48pub(crate) async fn prepare(p: ContainerTurnPrep<'_>) -> Result<(), String> {
52 let image = resolve_container_image(p.image);
53 if let Err(e) = p
54 .cm
55 .ensure_running(p.container_name, &image, p.volumes, &[], p.mount_spec)
56 .await
57 {
58 return Err(fail(&p, &e));
59 }
60
61 let Some(install) = CliInstall::for_provider(p.provider_id, p.container_command) else {
62 return Ok(());
63 };
64 let install_id = uuid::Uuid::new_v4().to_string();
65 let notice = |state: InstallState, seconds: Option<f64>, error: Option<&str>| {
66 let frame = cli_notice::install_frame(&install_id, &install.provider_id, &install.version, state, seconds, error);
67 cli_notice::append_to_pane(p.broker, p.filestore, p.mstore, p.block_id, &frame);
68 };
69
70 let mut started_install = false;
71 let outcome = p
72 .cm
73 .provision_cli(p.container_name, &install, || {
74 started_install = true;
75 notice(InstallState::Installing, None, None);
76 })
77 .await;
78 match outcome {
79 Ok(CliProvision::Present) => Ok(()),
80 Ok(CliProvision::Installed { seconds }) => {
81 notice(InstallState::Installed, Some(seconds), None);
82 Ok(())
83 }
84 Err(e) => {
85 if started_install {
86 let detail = match &e {
87 ContainerError::CliInstall { detail, .. } => detail.clone(),
88 other => other.to_string(),
89 };
90 notice(InstallState::Failed, None, Some(&detail));
91 }
92 Err(fail(&p, &e))
93 }
94 }
95}
96
97fn fail(p: &ContainerTurnPrep<'_>, e: &ContainerError) -> String {
99 tracing::warn!(block_id = %p.block_id, container = %p.container_name, error = %e, "container agent could not start");
100 let message = e.user_message();
101 let line = format!("{}\n", error_result_frame(&message));
102 let zone = crate::backend::blockcontroller::shell::resolve_global_output_zone(
103 &Some(p.mstore.clone()),
104 p.block_id,
105 );
106 crate::backend::blockcontroller::shell::handle_append_block_file(
107 p.broker,
108 p.block_id,
109 crate::backend::blockcontroller::subprocess::SUBPROCESS_OUTPUT_SUBJECT,
110 line.as_bytes(),
111 Some(p.filestore),
112 zone.as_deref(),
113 );
114 message
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn the_error_frame_is_a_terminal_error_result_with_the_agentmux_prefix() {
123 let frame = error_result_frame("Couldn't start the container for this agent: x");
124 assert_eq!(frame["type"], "result");
125 assert_eq!(frame["is_error"], true);
126 assert_eq!(
127 frame["error"]["message"],
128 "[AgentMux] Couldn't start the container for this agent: x"
129 );
130 }
131}