PicoBot/src/agent/projection.rs
xiaoxixi ac201a3949 feat: durable agent orchestration with run persistence, inbox continuation, and signal/steer
- AgentCatalog/definitions with strict Markdown frontmatter, delegation graph,
  fail-closed tool scoping, and signal contracts
- structured cancellation (AgentError::Cancelled/TimedOut) across provider
  streams, tool batches, and sleep; /stop drives the same terminal state
- schema v6 run/group/inbox persistence with execution-ID conditional
  transitions and completion-slot reservations
- ExecutionGate separating run quota from provider/tool step permits
- background completion inbox with hidden-trigger continuation turns,
  fairness scheduling, lease release, dead-lettering, and activation recovery
- typed TurnMailbox with two-phase steer admission and atomic consumption at
  turn commit; /stop releases admitted steer events back to pending
- emit_signal tool with contract-enforced rate/dedupe/severity/size limits
- WS run/event projection (GetAgentRuns, AgentRunUpdated, AgentEventUpdated),
  /api/agent-runs* management endpoints, /api/tasks union, WebUI run tree
  and signal cards
- ChannelContext.durable_private persisted for continuation delivery reuse

Version 1.7.0
2026-08-11 11:51:20 +08:00

75 lines
2.1 KiB
Rust

//! Broadcast projection for durable Agent run/event updates.
//!
//! The hub follows the WorkManager plan-change broadcast pattern: the
//! Coordinator publishes a bounded view after each commit; the gateway
//! relays it to WebSocket clients. A lost broadcast is never fatal — the
//! client recalibrates with `GetAgentRuns`, whose `revision` comes from
//! `agent_session_state`.
use crate::protocol::{AgentEventView, AgentRunView};
/// One projected change for a root session. Exactly one of `run`/`event` is
/// present.
#[derive(Debug, Clone)]
pub struct AgentProjection {
pub session_id: String,
pub revision: i64,
pub run: Option<AgentRunView>,
pub event: Option<AgentEventView>,
}
#[derive(Debug, Clone)]
pub struct AgentProjectionHub {
tx: tokio::sync::broadcast::Sender<AgentProjection>,
}
impl Default for AgentProjectionHub {
fn default() -> Self {
Self::new()
}
}
impl AgentProjectionHub {
pub fn new() -> Self {
let (tx, _) = tokio::sync::broadcast::channel(256);
Self { tx }
}
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<AgentProjection> {
self.tx.subscribe()
}
/// Publish a run update. Best-effort: relays lag and drop the event,
/// clients recalibrate with a full query.
pub fn publish(&self, projection: AgentProjection) {
let _ = self.tx.send(projection);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn publish_reaches_subscribers_and_drop_is_nonfatal() {
let hub = AgentProjectionHub::new();
let mut rx = hub.subscribe();
hub.publish(AgentProjection {
session_id: "cli:test:d".to_string(),
revision: 1,
run: None,
event: None,
});
let received = rx.try_recv().unwrap();
assert_eq!(received.session_id, "cli:test:d");
// No subscribers after the first is dropped; publish must not panic.
drop(rx);
hub.publish(AgentProjection {
session_id: "cli:test:d".to_string(),
revision: 2,
run: None,
event: None,
});
}
}