feat(workstate): snapshot des délégations en file par agent (Lot B backend)
Ajoute un port de lecture ségrégué `AgentQueueSnapshot` (ISP) distinct du `AgentMailbox` mutant : il expose `queue_for(agent)` qui renvoie des `QueuedTicketSnapshot` clonés, ordonnés FIFO, avec position recalculée (0 = tête). Observer la file ne la mute jamais ; le one-shot reply sender reste dans l'adaptateur. - domain : value object `QueuedTicketSnapshot` + trait `AgentQueueSnapshot` (object-safe, partagé en `Arc<dyn …>`). - infrastructure : `InMemoryMailbox` implémente la vue lecture en plus de la vue mutation ; positions recalculées à chaque appel. - application : le read-model work-state liste les délégations en attente via ce port, troncature de l'aperçu de tâche en conservant la longueur d'origine. - app-tauri : DTO `camelCase` des tickets en file câblé dans l'état. Tests verts : domain mailbox (6), infrastructure mailbox (13), application workstate (12), app-tauri dto_agents (20). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -82,7 +82,10 @@ pub use profile::{
|
||||
McpServerWiring, RateLimitPattern, SessionStrategy,
|
||||
};
|
||||
|
||||
pub use mailbox::{AgentMailbox, MailboxError, PendingReply, Ticket, TicketId};
|
||||
pub use mailbox::{
|
||||
AgentMailbox, AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket,
|
||||
TicketId,
|
||||
};
|
||||
|
||||
pub use conversation::{
|
||||
Conversation, ConversationError, ConversationId, ConversationParty, ConversationRegistry,
|
||||
|
||||
@ -33,6 +33,47 @@ use crate::conversation::ConversationId;
|
||||
use crate::ids::AgentId;
|
||||
use crate::input::InputSource;
|
||||
|
||||
/// A read-only, cloned view of one queued [`Ticket`] in a target agent's FIFO.
|
||||
///
|
||||
/// Carries **only the data** of a ticket (never the one-shot reply sender, which
|
||||
/// stays inside the adapter), plus its `position` in the FIFO at snapshot time
|
||||
/// (`0` = head). Pure value object used by the [`AgentQueueSnapshot`] read port so
|
||||
/// the work-state read model can list pending delegations without touching the
|
||||
/// mutating [`AgentMailbox`] surface (ISP).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct QueuedTicketSnapshot {
|
||||
/// Stable id of the queued ticket.
|
||||
pub id: TicketId,
|
||||
/// The origin of the input (Human or a delegating Agent).
|
||||
pub source: InputSource,
|
||||
/// The conversation thread this task enters.
|
||||
pub conversation: ConversationId,
|
||||
/// Display name (or id) of the requester.
|
||||
pub requester: String,
|
||||
/// The full task/message text (preview/truncation is the caller's concern).
|
||||
pub task: String,
|
||||
/// Position in the FIFO at snapshot time (`0` = head, recomputed each call).
|
||||
pub position: u32,
|
||||
}
|
||||
|
||||
/// Read-only inspection port over the per-agent delegation FIFO.
|
||||
///
|
||||
/// Segregated from the mutating [`AgentMailbox`] (Interface Segregation): the
|
||||
/// work-state read model depends on this port to *observe* the queue, never on
|
||||
/// `enqueue`/`resolve`/`cancel`. Implementations return **cloned** snapshots in
|
||||
/// FIFO order with recomputed positions; observing the queue must never mutate it.
|
||||
///
|
||||
/// Object-safe (`&self`) so the application layer holds it as
|
||||
/// `Arc<dyn AgentQueueSnapshot>`; one concrete `InMemoryMailbox` can be shared as
|
||||
/// both an [`AgentMailbox`] (mutation) and an [`AgentQueueSnapshot`] (read) view.
|
||||
pub trait AgentQueueSnapshot: Send + Sync {
|
||||
/// Returns a cloned, FIFO-ordered snapshot of `agent`'s queued tickets.
|
||||
///
|
||||
/// An agent with no queue yields an empty `Vec`. Positions are recomputed from
|
||||
/// the current order (`0` = head). Pure read: the queue is left untouched.
|
||||
fn queue_for(&self, agent: AgentId) -> Vec<QueuedTicketSnapshot>;
|
||||
}
|
||||
|
||||
/// Identifies one queued [`Ticket`] within a target agent's mailbox.
|
||||
///
|
||||
/// Newtype around [`uuid::Uuid`]; minted by the adapter on `enqueue`. It is **never
|
||||
@ -310,4 +351,22 @@ mod tests {
|
||||
let a = AgentId::from_uuid(uuid::Uuid::from_u128(1));
|
||||
assert_ne!(MailboxError::NoPendingRequest(a), MailboxError::Cancelled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_ticket_snapshot_carries_ticket_data_and_position() {
|
||||
let conv = ConversationId::from_uuid(uuid::Uuid::from_u128(3));
|
||||
let from = AgentId::from_uuid(uuid::Uuid::from_u128(4));
|
||||
let snap = QueuedTicketSnapshot {
|
||||
id: TicketId::from_uuid(uuid::Uuid::from_u128(8)),
|
||||
source: InputSource::agent(from),
|
||||
conversation: conv,
|
||||
requester: "Main".to_owned(),
|
||||
task: "delegate".to_owned(),
|
||||
position: 2,
|
||||
};
|
||||
assert_eq!(snap.source.as_agent(), Some(from));
|
||||
assert_eq!(snap.conversation, conv);
|
||||
assert_eq!(snap.position, 2);
|
||||
assert_eq!(snap.task, "delegate");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user