a3s 0.10.5

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
//! Agent Island control dispatch and live-context validation.

use super::*;

impl App {
    pub(in crate::tui) fn apply_agent_island_control(
        &mut self,
        request: AgentControlRequest,
    ) -> Option<Cmd<Msg>> {
        let parent_id = self.agent_presence.publisher.instance_id().to_string();
        match request.action {
            AgentControlActionKind::ApproveOnce
            | AgentControlActionKind::ApproveAlways
            | AgentControlActionKind::Deny
                if request.activity_id == parent_id
                    && request.message.is_none()
                    && self.state == State::Awaiting
                    && self.pending_tools.front().is_some_and(|pending| {
                        request.context == self.agent_island_approval_context(&pending.tool_id)
                    }) =>
            {
                match request.action {
                    AgentControlActionKind::ApproveOnce => self.apply_approval(0).map(cmd::msg),
                    AgentControlActionKind::ApproveAlways => self.apply_approval(1).map(cmd::msg),
                    AgentControlActionKind::Deny => self
                        .deny_current_approval("Denied from Agent Island.")
                        .map(cmd::msg),
                    _ => None,
                }
            }
            AgentControlActionKind::Stop
                if request.activity_id == parent_id
                    && request.message.is_none()
                    && request.context == self.agent_island_parent_context()
                    && self.agent_island_stop_available() =>
            {
                self.begin_stream_interrupt("interrupted from Agent Island")
            }
            AgentControlActionKind::Cancel => {
                if request.message.is_some() {
                    return None;
                }
                let task_id = request
                    .activity_id
                    .strip_prefix(&format!("{parent_id}:"))
                    .map(str::to_string)?;
                let expected_activity = format!("{parent_id}:{task_id}");
                let live = self
                    .runtime
                    .subagent_ids()
                    .into_iter()
                    .zip(self.runtime.subagents())
                    .any(|(id, run)| id == task_id && !run.done);
                if request.activity_id != expected_activity
                    || request.context != self.agent_island_child_context(&task_id)
                    || !live
                {
                    return None;
                }
                if !self.agent_presence.cancel_requested.insert(task_id.clone()) {
                    return None;
                }
                let session = self.session.clone();
                Some(cmd::cmd(move || async move {
                    let cancelled = session.cancel_subagent_task(&task_id).await;
                    Msg::AgentIslandSubagentCancelFinished { task_id, cancelled }
                }))
            }
            AgentControlActionKind::Reply if request.activity_id == parent_id => {
                let message = request.message?.trim().to_string();
                if message.is_empty() {
                    return None;
                }
                let context_matches = match self.state {
                    State::Streaming => {
                        !self.interrupting && request.context == self.agent_island_parent_context()
                    }
                    State::Awaiting => self.pending_tools.front().is_some_and(|pending| {
                        request.context == self.agent_island_approval_context(&pending.tool_id)
                    }),
                    State::Idle | State::Rebuilding => false,
                };
                context_matches.then(|| cmd::msg(Msg::Submit(message)))
            }
            _ => None,
        }
    }

    pub(in crate::tui) fn apply_agent_island_subagent_cancel_result(
        &mut self,
        task_id: String,
        cancelled: bool,
    ) {
        if !cancelled {
            self.agent_presence.cancel_requested.remove(&task_id);
            tracing::debug!(%task_id, "agent island child cancellation was already stale");
        }
    }

    pub(in crate::tui) fn begin_stream_interrupt(&mut self, reason: &str) -> Option<Cmd<Msg>> {
        self.begin_stream_interrupt_with_goal_policy(reason, true)
    }

    pub(in crate::tui) fn begin_send_now_interrupt(&mut self) -> Option<Cmd<Msg>> {
        self.begin_stream_interrupt_with_goal_policy("superseded by Send now", false)
    }

    fn begin_stream_interrupt_with_goal_policy(
        &mut self,
        reason: &str,
        cancel_goal: bool,
    ) -> Option<Cmd<Msg>> {
        if !self.agent_island_stop_available() {
            return None;
        }
        let goal_cancelled = cancel_goal && self.cancel_goal_state(reason);
        self.interrupting = true;
        self.agent_presence
            .publisher
            .reconcile_control_grants(Vec::new(), epoch_ms());
        if self.stream_join.is_none() && self.rx.is_none() && !self.host_progress_inflight {
            self.interrupted_stream_start_token = Some(self.stream_start_token);
        }
        self.stream_start_token = self.stream_start_token.wrapping_add(1);
        self.deep_research_stream_timeout_token =
            self.deep_research_stream_timeout_token.wrapping_add(1);
        let status_entry =
            self.push_tracked_line(&Style::new().fg(TN_YELLOW).render("  ⎋ interrupting…"));
        let session = self.session.clone();
        let join = self.stream_join.take();
        let host_abort = self.host_tool_abort.take();
        Some(cmd::cmd(move || async move {
            if let Some(host_abort) = host_abort {
                host_abort.abort();
            }
            let _ = session
                .cancel_and_settle(
                    Duration::from_millis(DEEP_RESEARCH_ABORT_GRACE_MS),
                    Duration::from_millis(GRACEFUL_QUIT_ABORT_SETTLE_MS),
                )
                .await;
            if let Some(join) = join {
                let _ = settle_stream_join_for_quit(
                    join,
                    Duration::from_millis(GRACEFUL_QUIT_ABORT_SETTLE_MS),
                )
                .await;
            }
            Msg::Interrupted {
                goal_cancelled,
                status_entry,
            }
        }))
    }

    pub(in crate::tui) fn agent_island_stop_available(&self) -> bool {
        self.state == State::Streaming
            && !self.interrupting
            && !self.stream_join_settling
            && !self.deep_research_subagent_settlement_inflight
    }

    pub(super) fn agent_island_parent_context(&self) -> String {
        format!("{}:{}", self.session_id, self.stream_start_token)
    }

    pub(super) fn agent_island_approval_context(&self, tool_id: &str) -> String {
        format!("{}:approval:{tool_id}", self.session_id)
    }

    pub(super) fn agent_island_child_context(&self, task_id: &str) -> String {
        format!("{}:{task_id}", self.session_id)
    }

    pub(super) fn local_agent_vendor(&self) -> AgentVendor {
        match self.model_source {
            ModelSelectionSource::Claude => AgentVendor::Anthropic,
            ModelSelectionSource::Codex => AgentVendor::OpenAi,
            ModelSelectionSource::Kimi => AgentVendor::Moonshot,
            ModelSelectionSource::CodeBuddy => AgentVendor::Tencent,
            ModelSelectionSource::OsGateway => AgentVendor::A3s,
            ModelSelectionSource::Config => self
                .model
                .as_deref()
                .and_then(AgentVendor::from_hint)
                .unwrap_or_default(),
        }
    }
}