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
|
|
@ -1,5 +1,5 @@
|
|||
//! An agent's subagents: the stream that keeps their rows, which ones the
|
||||
//! swarm has rows for, and a live SSE relay of one subagent's terminal.
|
||||
//! swarm has rows for, and an SSE relay of one subagent's terminal.
|
||||
//!
|
||||
//! The agent's subagent daemon publishes each subagent's classified rows on
|
||||
//! `$SWARM.term.{agent}.sub.{subagent}` under the agent's own queue credential,
|
||||
|
|
@ -15,9 +15,12 @@
|
|||
//! **No agent-status lookup.** Unlike [`crate::term_stream`], an agent that
|
||||
//! has never reported gets an empty stream, not a 404.
|
||||
//!
|
||||
//! **Live tail only, output only.** The relay is a core subscription, so a
|
||||
//! reader sees rows published while it is attached; unlike the agent's own
|
||||
//! terminal route, it replays nothing from the stream. Nothing is sent toward a subagent: subagents take no
|
||||
//! **Replay, then the live tail, output only.** The relay is
|
||||
//! [`crate::term_stream::replay_or_live`], the agent's own terminal route's,
|
||||
//! on the subagent's subject in `term-sub-{agent}`: the subagent's newest
|
||||
//! `agent_term::REPLAY_ROWS` rows, then each new one, with the same
|
||||
//! `Last-Event-ID` resume and the same live-only fallback while the stream
|
||||
//! cannot be read. Nothing is sent toward a subagent: subagents take no
|
||||
//! input from the swarm.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
|
@ -27,7 +30,7 @@ use std::time::Duration;
|
|||
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||
use futures_util::{Stream, StreamExt as _, TryStreamExt as _};
|
||||
use serde::Serialize;
|
||||
|
|
@ -211,7 +214,9 @@ async fn last_row_unix(
|
|||
responses(
|
||||
(status = 200, description = "server-sent event stream; each event's `data` is \
|
||||
one already-classified TermMsg row, JSON as the agent's subagent daemon \
|
||||
published it (opaque to this daemon) — live only, no replay",
|
||||
published it (opaque to this daemon); the subagent's newest retained rows \
|
||||
first, then live rows. Each event's `id` is the row's stream sequence, and a \
|
||||
`Last-Event-ID` request header resumes right after that row",
|
||||
body = String, content_type = "text/event-stream"),
|
||||
(status = 400, description = "the agent or subagent name is not shaped like an \
|
||||
identifier (problem+json)", body = String),
|
||||
|
|
@ -225,18 +230,19 @@ async fn last_row_unix(
|
|||
pub(crate) async fn stream_subagent_term(
|
||||
State(state): State<AppState>,
|
||||
Path((agent, subagent)): Path<(String, String)>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, problem_details::ProblemDetails> {
|
||||
let agent = ident(&agent).map_err(bad_request)?;
|
||||
let subagent = ident(&subagent).map_err(bad_request)?;
|
||||
let client = connected_client(&state).map_err(|e| unavailable(&e))?;
|
||||
let subscriber = crate::term_stream::subscribe(
|
||||
let stream = crate::term_stream::replay_or_live(
|
||||
&client,
|
||||
&subagent_term::stream_name(&agent),
|
||||
subagent_term::subject(&agent, &subagent),
|
||||
crate::term_stream::last_event_id(&headers),
|
||||
"subagent term",
|
||||
)
|
||||
.await?;
|
||||
let stream =
|
||||
subscriber.map(|msg| Ok(Event::default().data(String::from_utf8_lossy(&msg.payload))));
|
||||
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue