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:
parent
ca51bb9d54
commit
1779fa0568
3 changed files with 388 additions and 1 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
87
swarm-queue-client/src/bash_task.rs
Normal file
87
swarm-queue-client/src/bash_task.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue