swarm-controller: create every agent's subagent stream
swarm-controller now creates `term-sub-<agent>` for every agent a hive is declared to run, at start and every minute after, with the config `swarm_queue_client::subagent_term::open_or_create` spells (subjects `$SWARM.term.<agent>.sub.>`, max_age 24h). An existing stream is opened as it is, as the controller does for its other streams and buckets, under the `$JS.API.STREAM.CREATE.*` grant it already holds. The agent no longer creates the stream: its token is granted publish on `$SWARM.term.<agent>.sub.>` and no `$JS.API.STREAM.CREATE|INFO` subject, and the subagent daemon only publishes. A `CREATE` carries the stream's config in its payload, which no subject grant narrows, so the agent could otherwise pick the stream's subjects and limits.
This commit is contained in:
parent
d6f94e5247
commit
318f67cda9
13 changed files with 135 additions and 102 deletions
|
|
@ -28,8 +28,8 @@ rmcp.workspace = true
|
|||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
# `subagent_term`: the subject, the stream, and opening it as the agent.
|
||||
swarm-queue-client = { workspace = true, features = ["subagent-term"] }
|
||||
# `subagent_term`: the subject each subagent's rows are published on.
|
||||
swarm-queue-client.workspace = true
|
||||
# The agent's own queue credential, read under its store identity.
|
||||
swarm-secret-client.workspace = true
|
||||
tokio.workspace = true
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@
|
|||
//! both daemons run as the agent's user. The hive's shared client is never
|
||||
//! tried, because the queue grants it nothing under these subjects.
|
||||
//!
|
||||
//! **The agent creates its stream.** Before publishing, the task opens
|
||||
//! `term-sub-<agent>`, creating it when it is missing. The stream's subjects
|
||||
//! are how the swarm lists this agent's subagents; a row published while the
|
||||
//! stream cannot be opened still reaches a live subscriber.
|
||||
//! **Publish only.** The rows land in `term-sub-<agent>`, the stream
|
||||
//! `swarm-controller` creates for this agent; this daemon never opens it. A
|
||||
//! row published while that stream does not exist yet still reaches a live
|
||||
//! subscriber, but the swarm does not list the subagent from it.
|
||||
//!
|
||||
//! **Best-effort.** A subagent's run never waits on the queue: no store, a
|
||||
//! refused credential, a full queue, a failed stream create and a failed
|
||||
//! publish are each a log line, and the run goes on.
|
||||
//! refused credential, a full queue and a failed publish are each a log
|
||||
//! line, and the run goes on.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -56,8 +56,8 @@ const QUEUE_DEPTH: usize = 4096;
|
|||
/// connection attempt.
|
||||
const STORE_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// How long after a failed connect, or a failed stream open, the task tries
|
||||
/// again. Rows that arrive before a first connect succeeds are dropped.
|
||||
/// How long after a failed connect the task tries again. Rows that arrive
|
||||
/// before a first connect succeeds are dropped.
|
||||
const RETRY_AFTER: Duration = Duration::from_mins(1);
|
||||
|
||||
/// Room left under the server's payload limit for subject, headers and
|
||||
|
|
@ -243,19 +243,15 @@ async fn connect(target: &Arc<Target>) -> anyhow::Result<async_nats::Client> {
|
|||
.map_err(|e| anyhow!(swarm_queue_client::chain(&e)))
|
||||
}
|
||||
|
||||
/// The connection and the stream, each retried at most once per
|
||||
/// [`RETRY_AFTER`].
|
||||
/// The connection, retried at most once per [`RETRY_AFTER`].
|
||||
#[derive(Default)]
|
||||
struct Queue {
|
||||
client: Option<async_nats::Client>,
|
||||
connect_after: Option<Instant>,
|
||||
stream_open: bool,
|
||||
stream_after: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Queue {
|
||||
/// A client to publish on, connecting and opening the stream first when
|
||||
/// they are due.
|
||||
/// A client to publish on, connecting first when a connect is due.
|
||||
async fn ready(&mut self, target: &Arc<Target>) -> Option<async_nats::Client> {
|
||||
let now = Instant::now();
|
||||
if self.client.is_none() && self.connect_after.is_none_or(|t| now >= t) {
|
||||
|
|
@ -275,25 +271,7 @@ impl Queue {
|
|||
}
|
||||
}
|
||||
}
|
||||
let client = self.client.clone()?;
|
||||
if !self.stream_open
|
||||
&& self.stream_after.is_none_or(|t| now >= t)
|
||||
&& swarm_queue_client::ensure_connected(&client).is_ok()
|
||||
{
|
||||
match subagent_term::open_or_create(&client, &target.agent).await {
|
||||
Ok(_) => self.stream_open = true,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = %swarm_queue_client::chain(&e),
|
||||
retry_in = ?RETRY_AFTER,
|
||||
"subagent terminal: opening this agent's stream failed; rows still \
|
||||
reach live subscribers but the swarm cannot list the subagent"
|
||||
);
|
||||
self.stream_after = Some(now + RETRY_AFTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(client)
|
||||
self.client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue