Watch
0
0
Fork
You've already forked hyperhive
0

swarm: the contract for an agent's bash task status and output

A status key per task in the bash-task-status bucket and the task's output rows
in a per-agent stream, kept apart as for subagents. The controller creates the
bucket and the streams, and the agent credential may publish its own keys and
subjects. Nothing publishes yet; the bash MCP half follows.
This commit is contained in:
iris 2026-11-15 17:23:22 +01:00 • committed by mara
commit 1779fa0568
3 changed files with 388 additions and 1 deletions

View file

@ -33,6 +33,9 @@ notices = ["async-nats/jetstream"]
# `subagent_term::open_or_create`, for swarm-controller, which creates every
# agent's subagent stream. Explicit for the same reason as `notices`.
subagent-term = ["async-nats/jetstream"]
# `bash_task::open_status` and `open_output`, for swarm-controller, which creates
# the status bucket and every agent's task output stream. Needs `kv` for the bucket.
bash-task = ["kv"]
# `agent_term::open_or_create` and the replay consumer, for swarm-controller,
# which creates every agent's terminal stream and replays it to swarm-ui.
# Explicit for the same reason as `notices`.

View file

@ -0,0 +1,87 @@
//! The connection side of an agent's bash tasks: the status bucket and the
//! output stream. The names, keys, subjects and the status value are
//! `swarm_wire::bash_task`.
//!
//! `swarm-controller` creates the bucket once and one output stream per agent
//! with the fixed config [`output_config`] spells.
use swarm_wire::bash_task::{OUTPUT_MAX_AGE, output_stream_name, output_stream_subjects};
/// How large one agent's output stream may grow before the discard policy
/// reclaims space, as for the subagent rows.
const OUTPUT_MAX_BYTES: i64 = 64 * 1024 * 1024;
/// The config [`open_output`] creates `agent`'s output stream with.
///
/// `max_message_size` is the queue's live `max_payload` ([`crate::max_payload`]),
/// so the stream never rejects a row the queue itself accepted.
pub(crate) fn output_config(
agent: &str,
max_message_size: i32,
) -> async_nats::jetstream::stream::Config {
async_nats::jetstream::stream::Config {
name: output_stream_name(agent),
description: Some(format!("Output of {agent}'s bash tasks")),
subjects: vec![output_stream_subjects(agent)],
max_age: OUTPUT_MAX_AGE,
max_bytes: OUTPUT_MAX_BYTES,
discard: async_nats::jetstream::stream::DiscardPolicy::Old,
max_message_size,
..Default::default()
}
}
/// Open the status bucket, creating it if nothing has yet. `history: 1`: a
/// reader wants each task's current state, not every state it has been in.
pub async fn open_status(
client: &async_nats::Client,
) -> Result<async_nats::jetstream::kv::Store, crate::Error> {
let js = async_nats::jetstream::new(client.clone());
crate::kv::open_or_create(
&js,
swarm_wire::bash_task::BUCKET,
"bash-task-status",
"The state of each bash task, as its agent's bash MCP last published it".to_owned(),
)
.await
}
/// Open `agent`'s output stream, creating it if it does not exist yet.
pub async fn open_output(
client: &async_nats::Client,
agent: &str,
) -> Result<async_nats::jetstream::stream::Stream, crate::Error> {
let js = async_nats::jetstream::new(client.clone());
let name = output_stream_name(agent);
match js.get_stream(&name).await {
Ok(stream) => Ok(stream),
Err(e) => {
tracing::info!(stream = %name, reason = %e, "bash task output stream not available, creating it");
let max_message_size = i32::try_from(crate::max_payload(client)).unwrap_or(i32::MAX);
js.create_stream(output_config(agent, max_message_size))
.await
.map_err(|source| crate::Error::CreateAgentStream {
stream: name,
source,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_output_stream_caps_bytes_and_discards_the_oldest() {
let cfg = output_config("iris", 8_388_608);
assert_eq!(cfg.name, "term-bash-iris");
assert_eq!(cfg.subjects, vec!["$SWARM.term.iris.bash.>".to_owned()]);
assert_eq!(cfg.max_bytes, OUTPUT_MAX_BYTES);
assert_eq!(
cfg.discard,
async_nats::jetstream::stream::DiscardPolicy::Old
);
assert_eq!(cfg.max_message_size, 8_388_608);
}
}

View file

@ -138,7 +138,11 @@ pub enum Error {
source: async_nats::jetstream::context::CreateStreamError,
},
#[cfg(any(feature = "subagent-term", feature = "agent-term"))]
#[cfg(any(
feature = "subagent-term",
feature = "agent-term",
feature = "bash-task"
))]
#[error("creating the {stream} stream")]
CreateAgentStream {
// Owned: the name carries the agent's (`subagent_term::stream_name`,
@ -200,6 +204,11 @@ pub mod wanted;
/// merge, are in `swarm_wire::agent_status`.
pub mod agent_status;
/// The status bucket and the output stream of an agent's bash tasks. The names,
/// keys and values are `swarm_wire::bash_task`. Behind the `bash-task` feature.
#[cfg(feature = "bash-task")]
pub mod bash_task;
/// Opening the bucket each agent's subagent daemon publishes the run state of
/// its subagents into: one key per subagent. The name, keys and value are
/// `swarm_wire::subagent_status`; opening the bucket needs `kv`.