swarm-controller: replay a subagent's newest rows before its live tail
A subagent's terminal opened directly showed nothing until the subagent published again: the relay was a core subscription, though its rows sit in term-sub-<agent> for 24 hours. It now goes through the agent route's replay-then-tail, so it opens with the subagent's newest 100 rows and resumes after Last-Event-ID. - term_stream.rs: the replay takes a stream name and a filter subject instead of an agent, and `replay_or_live` (replay, else the live core subscription) is the one fn both routes call. When the filter is one of several subjects in the stream, a fresh reader's consumer starts at the oldest row and drops all but the newest REPLAY_ROWS it counts as pending, since the stream's newest sequences may belong to sibling subagents. A single-subject stream (term-main-<agent>) picks its start with replay_start exactly as before. Tests for both subagent cases. - subagent_term.rs: the relay calls replay_or_live on the subagent's subject in term-sub-<agent> and reads Last-Event-ID; module doc and OpenAPI text no longer say "live only". - main.rs: the handler test passes the new headers argument. - swarm-queue-client agent_term.rs: replay_consumer_config takes the filter subject instead of the agent. - swarm-queue-client subagent_term.rs: consumer_create_grant, the extended CONSUMER.CREATE form with a subagent subject as filter. - swarm-nats-auth policy.rs: the controller holds that grant; the agent grant's `$SWARM.term.*` filter is two tokens short of a subagent subject, and the test that refused it now admits it. - docs/swarm/ui.md, README.md: a subagent terminal opens with its newest rows. Fixes #5527
This commit is contained in:
parent
f65364759f
commit
d9c586ef94
8 changed files with 272 additions and 71 deletions
|
|
@ -135,17 +135,21 @@ pub async fn open_or_create(
|
|||
}
|
||||
}
|
||||
|
||||
/// The consumer that replays `agent`'s rows from `start_sequence` and then
|
||||
/// follows new ones, delivered to `deliver_subject` in stream order.
|
||||
/// The consumer that replays the rows on `filter_subject` from
|
||||
/// `start_sequence` and then follows new ones, delivered to `deliver_subject`
|
||||
/// in stream order. The filter is an agent's [`stream_subject`] or one
|
||||
/// subagent's [`crate::subagent_term::subject`].
|
||||
///
|
||||
/// Ephemeral and unacknowledged: the server drops it once nothing listens
|
||||
/// on `deliver_subject`, so a reader that goes away needs no delete grant.
|
||||
/// `name` must be set, because the server only takes the filter from the
|
||||
/// subject [`consumer_create_grant`] covers when the create names a consumer.
|
||||
/// subject [`consumer_create_grant`] (or
|
||||
/// [`crate::subagent_term::consumer_create_grant`]) covers when the create
|
||||
/// names a consumer.
|
||||
#[cfg(feature = "agent-term")]
|
||||
#[must_use]
|
||||
pub fn replay_consumer_config(
|
||||
agent: &str,
|
||||
filter_subject: String,
|
||||
name: String,
|
||||
deliver_subject: String,
|
||||
start_sequence: u64,
|
||||
|
|
@ -153,7 +157,7 @@ pub fn replay_consumer_config(
|
|||
async_nats::jetstream::consumer::push::Config {
|
||||
name: Some(name),
|
||||
deliver_subject,
|
||||
filter_subject: stream_subject(agent),
|
||||
filter_subject,
|
||||
deliver_policy: async_nats::jetstream::consumer::DeliverPolicy::ByStartSequence {
|
||||
start_sequence,
|
||||
},
|
||||
|
|
@ -269,7 +273,12 @@ mod tests {
|
|||
#[cfg(feature = "agent-term")]
|
||||
#[test]
|
||||
fn the_consumer_replays_before_it_follows() {
|
||||
let cfg = replay_consumer_config("iris", "r1".to_owned(), "_INBOX.x".to_owned(), 901);
|
||||
let cfg = replay_consumer_config(
|
||||
stream_subject("iris"),
|
||||
"r1".to_owned(),
|
||||
"_INBOX.x".to_owned(),
|
||||
901,
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.deliver_policy,
|
||||
async_nats::jetstream::consumer::DeliverPolicy::ByStartSequence {
|
||||
|
|
|
|||
|
|
@ -6,17 +6,18 @@
|
|||
//! credential. The queue grants that credential publish on this subject
|
||||
//! family and no `JetStream` subject.
|
||||
//!
|
||||
//! The stream exists so the swarm can list an agent's subagents: subagents
|
||||
//! The stream lets the swarm list an agent's subagents: subagents
|
||||
//! are spawned on demand under caller-chosen names, and the set of subjects
|
||||
//! the stream holds is the list. `swarm-controller` creates one stream per
|
||||
//! agent with the fixed config [`crate::subagent_term::open_or_create`]
|
||||
//! spells, and takes the list from its subject counts
|
||||
//! ([`crate::subagent_term::subagent_names`]).
|
||||
//! ([`crate::subagent_term::subagent_names`]). A reader attaching to one
|
||||
//! subagent's terminal replays that subagent's newest rows from it.
|
||||
//!
|
||||
//! The name and the subject live here because three crates must agree on
|
||||
//! them: the publisher in `hive-subagent-mcp`, `swarm-controller`, and the
|
||||
//! queue grant in `swarm-nats.nix`, whose `{agent}` template spells the same
|
||||
//! subjects.
|
||||
//! The name and the subject live here because they must agree across the
|
||||
//! publisher in `hive-subagent-mcp`, `swarm-controller`, the queue grant in
|
||||
//! `swarm-nats.nix`, whose `{agent}` template spells the same subjects, and
|
||||
//! the reader's consumer grant in `swarm-nats-auth`.
|
||||
|
||||
use crate::TERM_SUBJECT_PREFIX;
|
||||
|
||||
|
|
@ -56,6 +57,17 @@ pub fn subject_wildcard() -> String {
|
|||
subject("*", "*")
|
||||
}
|
||||
|
||||
/// The reader's grant to create a replay consumer on one subagent's rows:
|
||||
/// the extended create form of [`crate::agent_term::consumer_create_grant`],
|
||||
/// on any stream and under any consumer name, with a [`subject`] as the
|
||||
/// filter tokens the server checks the payload against. The last `*` also
|
||||
/// matches a literal `*` or `>`, so it admits `$SWARM.term.<agent>.sub.>`
|
||||
/// too, and nothing outside [`subject_wildcard`]'s family.
|
||||
#[must_use]
|
||||
pub fn consumer_create_grant() -> String {
|
||||
format!("$JS.API.CONSUMER.CREATE.*.*.{}", subject_wildcard())
|
||||
}
|
||||
|
||||
/// Every subject [`stream_name`]'s stream captures, which is also the publish
|
||||
/// grant's shape.
|
||||
#[must_use]
|
||||
|
|
@ -149,6 +161,10 @@ mod tests {
|
|||
assert_eq!(stream_name("iris"), "term-sub-iris");
|
||||
assert_eq!(subject("iris", "scout"), "$SWARM.term.iris.sub.scout");
|
||||
assert_eq!(stream_subjects("iris"), "$SWARM.term.iris.sub.>");
|
||||
assert_eq!(
|
||||
consumer_create_grant(),
|
||||
"$JS.API.CONSUMER.CREATE.*.*.$SWARM.term.*.sub.*"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue