feat: livrable ticket #91 — notification fin BackgroundTask enrichie
- Backend: enrichissement HeadlessRendezvous avec requester/target/conversationId - Frontend: toast 'Requester -> Target completed/...' explicite - Clic ouvrant viewer de conversation pour visualiser l'échange
This commit is contained in:
@ -6,7 +6,7 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use domain::conversation::ConversationParty;
|
||||
use domain::events::{DomainEvent, OrchestrationSource};
|
||||
use domain::events::{DomainEvent, OrchestrationSource, RendezvousContext};
|
||||
use domain::input::AgentLiveness;
|
||||
use domain::model_server::ModelServerLifecycleStatus;
|
||||
use domain::{IssueLinkKind, IssuePriority, IssueStatus};
|
||||
@ -298,6 +298,15 @@ pub enum DomainEventDto {
|
||||
agent_id: String,
|
||||
/// Lightweight event/state label.
|
||||
state: String,
|
||||
/// Agent that requested a headless rendezvous, when known.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
requester_agent_id: Option<String>,
|
||||
/// Target agent for a headless rendezvous.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
target_agent_id: Option<String>,
|
||||
/// Conversation opened by a headless rendezvous.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
conversation_id: Option<String>,
|
||||
},
|
||||
/// An agent inbox queue depth changed.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -725,6 +734,24 @@ fn conversation_party_wire(party: ConversationParty) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn rendezvous_requester_agent_id(rendezvous: &Option<RendezvousContext>) -> Option<String> {
|
||||
rendezvous
|
||||
.as_ref()
|
||||
.and_then(|ctx| ctx.requester_agent_id.map(|id| id.to_string()))
|
||||
}
|
||||
|
||||
fn rendezvous_target_agent_id(rendezvous: &Option<RendezvousContext>) -> Option<String> {
|
||||
rendezvous
|
||||
.as_ref()
|
||||
.map(|ctx| ctx.target_agent_id.to_string())
|
||||
}
|
||||
|
||||
fn rendezvous_conversation_id(rendezvous: &Option<RendezvousContext>) -> Option<String> {
|
||||
rendezvous
|
||||
.as_ref()
|
||||
.map(|ctx| ctx.conversation_id.to_string())
|
||||
}
|
||||
|
||||
impl From<&DomainEvent> for DomainEventDto {
|
||||
fn from(e: &DomainEvent) -> Self {
|
||||
match e {
|
||||
@ -862,6 +889,9 @@ impl From<&DomainEvent> for DomainEventDto {
|
||||
task_id: task_id.to_string(),
|
||||
agent_id: owner_agent_id.to_string(),
|
||||
state: "started".to_owned(),
|
||||
requester_agent_id: None,
|
||||
target_agent_id: None,
|
||||
conversation_id: None,
|
||||
},
|
||||
DomainEvent::BackgroundTaskStateChanged {
|
||||
project_id,
|
||||
@ -873,36 +903,51 @@ impl From<&DomainEvent> for DomainEventDto {
|
||||
task_id: task_id.to_string(),
|
||||
agent_id: owner_agent_id.to_string(),
|
||||
state: format!("{state:?}"),
|
||||
requester_agent_id: None,
|
||||
target_agent_id: None,
|
||||
conversation_id: None,
|
||||
},
|
||||
DomainEvent::BackgroundTaskCompleted {
|
||||
project_id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous,
|
||||
} => Self::BackgroundTaskChanged {
|
||||
project_id: project_id.to_string(),
|
||||
task_id: task_id.to_string(),
|
||||
agent_id: owner_agent_id.to_string(),
|
||||
state: "completed".to_owned(),
|
||||
requester_agent_id: rendezvous_requester_agent_id(rendezvous),
|
||||
target_agent_id: rendezvous_target_agent_id(rendezvous),
|
||||
conversation_id: rendezvous_conversation_id(rendezvous),
|
||||
},
|
||||
DomainEvent::BackgroundTaskFailed {
|
||||
project_id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous,
|
||||
} => Self::BackgroundTaskChanged {
|
||||
project_id: project_id.to_string(),
|
||||
task_id: task_id.to_string(),
|
||||
agent_id: owner_agent_id.to_string(),
|
||||
state: "failed".to_owned(),
|
||||
requester_agent_id: rendezvous_requester_agent_id(rendezvous),
|
||||
target_agent_id: rendezvous_target_agent_id(rendezvous),
|
||||
conversation_id: rendezvous_conversation_id(rendezvous),
|
||||
},
|
||||
DomainEvent::BackgroundTaskCancelled {
|
||||
project_id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous,
|
||||
} => Self::BackgroundTaskChanged {
|
||||
project_id: project_id.to_string(),
|
||||
task_id: task_id.to_string(),
|
||||
agent_id: owner_agent_id.to_string(),
|
||||
state: "cancelled".to_owned(),
|
||||
requester_agent_id: rendezvous_requester_agent_id(rendezvous),
|
||||
target_agent_id: rendezvous_target_agent_id(rendezvous),
|
||||
conversation_id: rendezvous_conversation_id(rendezvous),
|
||||
},
|
||||
DomainEvent::BackgroundTaskCompletionDeliveryPending {
|
||||
project_id,
|
||||
@ -913,6 +958,9 @@ impl From<&DomainEvent> for DomainEventDto {
|
||||
task_id: task_id.to_string(),
|
||||
agent_id: owner_agent_id.to_string(),
|
||||
state: "deliveryPending".to_owned(),
|
||||
requester_agent_id: None,
|
||||
target_agent_id: None,
|
||||
conversation_id: None,
|
||||
},
|
||||
DomainEvent::BackgroundTaskCompletionDelivered {
|
||||
project_id,
|
||||
@ -923,6 +971,9 @@ impl From<&DomainEvent> for DomainEventDto {
|
||||
task_id: task_id.to_string(),
|
||||
agent_id: owner_agent_id.to_string(),
|
||||
state: "delivered".to_owned(),
|
||||
requester_agent_id: None,
|
||||
target_agent_id: None,
|
||||
conversation_id: None,
|
||||
},
|
||||
DomainEvent::AgentInboxQueued { agent_id, depth } => Self::AgentInboxChanged {
|
||||
agent_id: agent_id.to_string(),
|
||||
@ -1205,7 +1256,7 @@ mod tests {
|
||||
use super::*;
|
||||
use domain::ids::AgentId;
|
||||
use domain::mailbox::TicketId;
|
||||
use domain::{LocalModelServerId, ProjectId};
|
||||
use domain::{ConversationId, LocalModelServerId, ProjectId, TaskId};
|
||||
use serde_json::json;
|
||||
|
||||
fn agent(n: u128) -> AgentId {
|
||||
@ -1216,6 +1267,14 @@ mod tests {
|
||||
LocalModelServerId::from_uuid(uuid::Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
fn task(n: u128) -> TaskId {
|
||||
TaskId::from_uuid(uuid::Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
fn conversation(n: u128) -> ConversationId {
|
||||
ConversationId::from_uuid(uuid::Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_server_status_changed_relays_ready_to_dto_and_wire() {
|
||||
let dto = DomainEventDto::from(&DomainEvent::ModelServerStatusChanged {
|
||||
@ -1348,6 +1407,56 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_completion_relays_rendezvous_context_to_wire() {
|
||||
let project_id = ProjectId::from_uuid(uuid::Uuid::from_u128(1));
|
||||
let task_id = task(2);
|
||||
let requester = agent(3);
|
||||
let target = agent(4);
|
||||
let conversation_id = conversation(5);
|
||||
|
||||
let dto = DomainEventDto::from(&DomainEvent::BackgroundTaskCompleted {
|
||||
project_id,
|
||||
task_id,
|
||||
owner_agent_id: target,
|
||||
rendezvous: Some(RendezvousContext {
|
||||
requester_agent_id: Some(requester),
|
||||
target_agent_id: target,
|
||||
conversation_id,
|
||||
}),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(&dto).unwrap(),
|
||||
json!({
|
||||
"type": "backgroundTaskChanged",
|
||||
"projectId": project_id.to_string(),
|
||||
"taskId": task_id.to_string(),
|
||||
"agentId": target.to_string(),
|
||||
"state": "completed",
|
||||
"requesterAgentId": requester.to_string(),
|
||||
"targetAgentId": target.to_string(),
|
||||
"conversationId": conversation_id.to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_failure_without_rendezvous_omits_context_fields() {
|
||||
let dto = DomainEventDto::from(&DomainEvent::BackgroundTaskFailed {
|
||||
project_id: ProjectId::from_uuid(uuid::Uuid::from_u128(1)),
|
||||
task_id: task(2),
|
||||
owner_agent_id: agent(3),
|
||||
rendezvous: None,
|
||||
});
|
||||
|
||||
let json = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(json["type"], "backgroundTaskChanged");
|
||||
assert!(json.get("requesterAgentId").is_none());
|
||||
assert!(json.get("targetAgentId").is_none());
|
||||
assert!(json.get("conversationId").is_none());
|
||||
}
|
||||
|
||||
/// LS6 : un `AgentRateLimited` du domaine se relaie en DTO portant le même agent
|
||||
/// et l'heure de reset (époche-ms), et se sérialise en `"agentRateLimited"` avec
|
||||
/// `resetsAtMs` — le fait neutre que le front badge « limité jusqu'à HH:MM ».
|
||||
|
||||
Reference in New Issue
Block a user