swarm: cancel an agent's turn from the swarm queue
A separate $SWARM.control.<agent> subject carries commands, apart from
the text subject. swarm-controller exposes POST /api/agents/{name}/cancel
as a NATS request; the harness answers whether a turn was actually
stopped, through the same cancel path as the dashboard button. The agent
credential gets subscribe on its control subject and allow_responses to
answer; only the controller may publish there.
Refs #3803
This commit is contained in:
parent
e7ed28b382
commit
fefc565794
11 changed files with 558 additions and 69 deletions
|
|
@ -299,6 +299,26 @@ pub const AGENT_INPUT_SUBJECT_WILDCARD: &str = "$SWARM.input.*";
|
|||
/// Shared by [`agent_input_subject`] and [`AGENT_INPUT_SUBJECT_WILDCARD`].
|
||||
const AGENT_INPUT_SUBJECT_PREFIX: &str = "$SWARM.input";
|
||||
|
||||
/// The subject the swarm controller sends an operator's control command for
|
||||
/// `agent` on (see [`AgentControl`]).
|
||||
///
|
||||
/// A family of its own, apart from [`agent_input_subject`]: a grant to send
|
||||
/// text never covers a command that stops a turn, and the other way round.
|
||||
/// Like the input subject it runs controller to agent over the swarm queue
|
||||
/// with no hive in the path.
|
||||
#[must_use]
|
||||
pub fn agent_control_subject(agent: &str) -> String {
|
||||
format!("{AGENT_CONTROL_SUBJECT_PREFIX}.{agent}")
|
||||
}
|
||||
|
||||
/// The publish grant covering every [`agent_control_subject`], for the one
|
||||
/// client that may send them. A wildcard for the same no-roster reason as
|
||||
/// [`AGENT_INPUT_SUBJECT_WILDCARD`].
|
||||
pub const AGENT_CONTROL_SUBJECT_WILDCARD: &str = "$SWARM.control.*";
|
||||
|
||||
/// Shared by [`agent_control_subject`] and [`AGENT_CONTROL_SUBJECT_WILDCARD`].
|
||||
const AGENT_CONTROL_SUBJECT_PREFIX: &str = "$SWARM.control";
|
||||
|
||||
/// The subject `agent` publishes its terminal rows on. One leaf per agent, so
|
||||
/// a subscriber can follow one agent without filtering the swarm's whole
|
||||
/// terminal traffic.
|
||||
|
|
@ -376,6 +396,37 @@ pub fn client_inbox_prefix(client_id: &str) -> String {
|
|||
format!("_INBOX.client.{client_id}")
|
||||
}
|
||||
|
||||
/// What an [`agent_control_subject`] message carries: one command for the
|
||||
/// agent's harness.
|
||||
///
|
||||
/// The harness acts on a command only because the controller is the one
|
||||
/// client granted publish on the subject, so there is no sender field to
|
||||
/// trust or forge here, unlike [`AgentInput::from`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentControl {
|
||||
/// Interrupt the turn in flight, as the agent's own cancel button does.
|
||||
Cancel,
|
||||
}
|
||||
|
||||
/// What an agent answers an [`AgentControl`] request with.
|
||||
///
|
||||
/// The controller sends a command as a request, so a reply is the only
|
||||
/// confirmation the operator gets that the harness took it. No reply within
|
||||
/// [`AGENT_CONTROL_REPLY_DEADLINE`] means "not confirmed", which is not the
|
||||
/// same as "refused".
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AgentControlReply {
|
||||
/// Whether the command changed anything: `false` for a cancel that
|
||||
/// found no turn in flight.
|
||||
pub acted: bool,
|
||||
}
|
||||
|
||||
/// How long the queue lets an agent answer an [`AgentControl`] request, and
|
||||
/// the controller waits for it. The harness answers as soon as it has acted,
|
||||
/// so this only bounds a harness that is down or wedged.
|
||||
pub const AGENT_CONTROL_REPLY_DEADLINE: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
/// What an [`agent_input_subject`] message carries.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AgentInput {
|
||||
|
|
|
|||
Loading…
Reference in a new issue