Skip to main content

agentmux_srv/server/agent_handlers/
container_turn.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Getting a container agent's container ready for a turn: the container is
5//! running, and the provider CLI is installed in it.
6//!
7//! Both places that start a container turn (`agentinput` and `agent.send`)
8//! go through [`prepare`], so a failure reads the same in the pane whichever
9//! way the message arrived.
10//!
11//! Spec: docs/specs/SPEC_CONTAINER_AGENTS_WORK_FOR_EVERYONE_2026_10_07.md.
12
13use 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
22/// Everything [`prepare`] needs from the turn that is starting.
23pub(crate) struct ContainerTurnPrep<'a> {
24    pub cm: &'a ContainerManager,
25    pub block_id: &'a str,
26    pub container_name: &'a str,
27    /// The stored `agent:container_image`; empty means the default image.
28    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
38/// The `result` frame that ends a turn with an error and carries `message`.
39pub(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
48/// Start the container, then make sure the provider CLI is in it. On failure
49/// the pane gets a plain-language error frame (persisted, so it survives a
50/// reload) and the same text is returned.
51pub(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
97/// Log `e`, write its plain-language message to the pane, and return it.
98fn 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}