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:
@ -127,6 +127,22 @@ fn resolve_background_cwd(
|
||||
ProjectPath::new(path).map_err(|_| AppError::Invalid("invalid background task cwd".to_owned()))
|
||||
}
|
||||
|
||||
fn rendezvous_context_for_task(task: &BackgroundTask) -> Option<domain::RendezvousContext> {
|
||||
match &task.kind {
|
||||
BackgroundTaskKind::HeadlessRendezvous {
|
||||
requester_agent_id,
|
||||
target_agent_id,
|
||||
conversation_id,
|
||||
..
|
||||
} => Some(domain::RendezvousContext {
|
||||
requester_agent_id: *requester_agent_id,
|
||||
target_agent_id: *target_agent_id,
|
||||
conversation_id: *conversation_id,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_path_no_parent(path: &Path) -> Result<PathBuf, AppError> {
|
||||
let mut out = PathBuf::new();
|
||||
for component in path.components() {
|
||||
@ -847,22 +863,26 @@ impl OrchestratorService {
|
||||
if task.is_terminal() {
|
||||
return Ok(());
|
||||
}
|
||||
let rendezvous = rendezvous_context_for_task(&task);
|
||||
let event = match &result {
|
||||
BackgroundTaskResult::Success { .. } => DomainEvent::BackgroundTaskCompleted {
|
||||
project_id: project.id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous,
|
||||
},
|
||||
BackgroundTaskResult::Failure { .. } => DomainEvent::BackgroundTaskFailed {
|
||||
project_id: project.id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous,
|
||||
},
|
||||
BackgroundTaskResult::Cancelled { .. } | BackgroundTaskResult::Expired { .. } => {
|
||||
DomainEvent::BackgroundTaskCancelled {
|
||||
project_id: project.id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous,
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -2967,6 +2987,52 @@ mod tests {
|
||||
assert_eq!(submit.delay_ms, Some(CODEX_SUBMIT_DELAY_MS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendezvous_context_is_extracted_from_headless_background_task_kind() {
|
||||
let requester = aid(1);
|
||||
let target = aid(2);
|
||||
let conversation_id = domain::ConversationId::from_uuid(uuid::Uuid::from_u128(3));
|
||||
let task = BackgroundTask::new(
|
||||
TaskId::from_uuid(uuid::Uuid::from_u128(4)),
|
||||
domain::ProjectId::from_uuid(uuid::Uuid::from_u128(5)),
|
||||
target,
|
||||
BackgroundTaskKind::HeadlessRendezvous {
|
||||
requester_agent_id: Some(requester),
|
||||
target_agent_id: target,
|
||||
ticket_id: TicketId::from_uuid(uuid::Uuid::from_u128(6)),
|
||||
conversation_id,
|
||||
},
|
||||
BackgroundTaskWakePolicy::RecordOnly,
|
||||
100,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let context = rendezvous_context_for_task(&task).expect("rendezvous context");
|
||||
|
||||
assert_eq!(context.requester_agent_id, Some(requester));
|
||||
assert_eq!(context.target_agent_id, target);
|
||||
assert_eq!(context.conversation_id, conversation_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendezvous_context_is_absent_for_command_background_task() {
|
||||
let task = BackgroundTask::new(
|
||||
TaskId::from_uuid(uuid::Uuid::from_u128(7)),
|
||||
domain::ProjectId::from_uuid(uuid::Uuid::from_u128(8)),
|
||||
aid(9),
|
||||
BackgroundTaskKind::Command {
|
||||
label: "cargo test".to_owned(),
|
||||
},
|
||||
BackgroundTaskWakePolicy::RecordOnly,
|
||||
100,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rendezvous_context_for_task(&task), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_profile_submit_delay_is_preserved() {
|
||||
let p = profile(3, "OpenAI Codex CLI", "codex")
|
||||
|
||||
@ -231,6 +231,12 @@ pub struct AgentBackgroundTaskState {
|
||||
pub stdout_tail: Option<String>,
|
||||
/// Bounded stderr tail.
|
||||
pub stderr_tail: Option<String>,
|
||||
/// Agent that requested a headless rendezvous, when this task is one.
|
||||
pub requester_agent_id: Option<AgentId>,
|
||||
/// Target agent for a headless rendezvous, when this task is one.
|
||||
pub target_agent_id: Option<AgentId>,
|
||||
/// Conversation opened by a headless rendezvous, when this task is one.
|
||||
pub conversation_id: Option<domain::ConversationId>,
|
||||
/// Creation timestamp, epoch milliseconds.
|
||||
pub created_at_ms: u64,
|
||||
/// Last update timestamp, epoch milliseconds.
|
||||
@ -592,6 +598,8 @@ impl GetProjectWorkState {
|
||||
impl From<BackgroundTask> for AgentBackgroundTaskState {
|
||||
fn from(task: BackgroundTask) -> Self {
|
||||
let (exit_code, summary, stdout_tail, stderr_tail) = flatten_background_result(&task);
|
||||
let (requester_agent_id, target_agent_id, conversation_id) =
|
||||
flatten_background_rendezvous_context(&task.kind);
|
||||
Self {
|
||||
task_id: task.id,
|
||||
kind: BackgroundTaskKindLabel::from(&task.kind),
|
||||
@ -600,12 +608,37 @@ impl From<BackgroundTask> for AgentBackgroundTaskState {
|
||||
summary,
|
||||
stdout_tail,
|
||||
stderr_tail,
|
||||
requester_agent_id,
|
||||
target_agent_id,
|
||||
conversation_id,
|
||||
created_at_ms: task.created_at_ms,
|
||||
updated_at_ms: task.updated_at_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_background_rendezvous_context(
|
||||
kind: &BackgroundTaskKind,
|
||||
) -> (
|
||||
Option<AgentId>,
|
||||
Option<AgentId>,
|
||||
Option<domain::ConversationId>,
|
||||
) {
|
||||
match kind {
|
||||
BackgroundTaskKind::HeadlessRendezvous {
|
||||
requester_agent_id,
|
||||
target_agent_id,
|
||||
conversation_id,
|
||||
..
|
||||
} => (
|
||||
*requester_agent_id,
|
||||
Some(*target_agent_id),
|
||||
Some(*conversation_id),
|
||||
),
|
||||
_ => (None, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&BackgroundTaskKind> for BackgroundTaskKindLabel {
|
||||
fn from(kind: &BackgroundTaskKind) -> Self {
|
||||
match kind {
|
||||
|
||||
@ -2611,6 +2611,15 @@ pub struct AgentBackgroundTaskStateDto {
|
||||
/// Bounded stderr tail.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stderr_tail: Option<String>,
|
||||
/// Agent that requested a headless rendezvous, when known.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub requester_agent_id: Option<String>,
|
||||
/// Target agent for a headless rendezvous.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target_agent_id: Option<String>,
|
||||
/// Conversation opened by a headless rendezvous.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation_id: Option<String>,
|
||||
/// Creation timestamp, epoch milliseconds.
|
||||
pub created_at_ms: u64,
|
||||
/// Last update timestamp, epoch milliseconds.
|
||||
@ -2627,6 +2636,9 @@ impl From<AgentBackgroundTaskState> for AgentBackgroundTaskStateDto {
|
||||
summary: task.summary,
|
||||
stdout_tail: task.stdout_tail,
|
||||
stderr_tail: task.stderr_tail,
|
||||
requester_agent_id: task.requester_agent_id.map(|id| id.to_string()),
|
||||
target_agent_id: task.target_agent_id.map(|id| id.to_string()),
|
||||
conversation_id: task.conversation_id.map(|id| id.to_string()),
|
||||
created_at_ms: task.created_at_ms,
|
||||
updated_at_ms: task.updated_at_ms,
|
||||
}
|
||||
@ -4052,6 +4064,15 @@ pub struct BackgroundTaskDto {
|
||||
/// Bounded stderr tail (unset for PTY-backed commands, which merge streams).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stderr_tail: Option<String>,
|
||||
/// Agent that requested a headless rendezvous, when known.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub requester_agent_id: Option<String>,
|
||||
/// Target agent for a headless rendezvous.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target_agent_id: Option<String>,
|
||||
/// Conversation opened by a headless rendezvous.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation_id: Option<String>,
|
||||
/// Creation timestamp, epoch milliseconds.
|
||||
pub created_at_ms: u64,
|
||||
/// Last update timestamp, epoch milliseconds.
|
||||
@ -4093,6 +4114,8 @@ fn background_state_label(state: BackgroundTaskState) -> &'static str {
|
||||
|
||||
impl From<BackgroundTask> for BackgroundTaskDto {
|
||||
fn from(task: BackgroundTask) -> Self {
|
||||
let (requester_agent_id, target_agent_id, conversation_id) =
|
||||
background_rendezvous_context_labels(&task.kind);
|
||||
let (exit_code, summary, stdout_tail, stderr_tail) = match &task.result {
|
||||
Some(BackgroundTaskResult::Success {
|
||||
exit_code,
|
||||
@ -4134,12 +4157,33 @@ impl From<BackgroundTask> for BackgroundTaskDto {
|
||||
summary,
|
||||
stdout_tail,
|
||||
stderr_tail,
|
||||
requester_agent_id,
|
||||
target_agent_id,
|
||||
conversation_id,
|
||||
created_at_ms: task.created_at_ms,
|
||||
updated_at_ms: task.updated_at_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn background_rendezvous_context_labels(
|
||||
kind: &BackgroundTaskKind,
|
||||
) -> (Option<String>, Option<String>, Option<String>) {
|
||||
match kind {
|
||||
BackgroundTaskKind::HeadlessRendezvous {
|
||||
requester_agent_id,
|
||||
target_agent_id,
|
||||
conversation_id,
|
||||
..
|
||||
} => (
|
||||
requester_agent_id.map(|id| id.to_string()),
|
||||
Some(target_agent_id.to_string()),
|
||||
Some(conversation_id.to_string()),
|
||||
),
|
||||
_ => (None, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a task-id string (UUID) coming from the frontend.
|
||||
///
|
||||
/// # Errors
|
||||
@ -4188,7 +4232,8 @@ pub struct SpawnBackgroundCommandRequestDto {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use application::McpToolPermissionCatalogue;
|
||||
use domain::{AgentId, ProjectMcpToolPermissions};
|
||||
use domain::mailbox::TicketId;
|
||||
use domain::{AgentId, ConversationId, ProjectMcpToolPermissions};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -4239,4 +4284,78 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_task_dto_exposes_rendezvous_context_only_for_headless_rendezvous() {
|
||||
let project_id = ProjectId::from_uuid(Uuid::from_u128(1));
|
||||
let owner = AgentId::from_uuid(Uuid::from_u128(2));
|
||||
let requester = AgentId::from_uuid(Uuid::from_u128(3));
|
||||
let conversation_id = ConversationId::from_uuid(Uuid::from_u128(4));
|
||||
let task = BackgroundTask::new(
|
||||
TaskId::from_uuid(Uuid::from_u128(5)),
|
||||
project_id,
|
||||
owner,
|
||||
BackgroundTaskKind::HeadlessRendezvous {
|
||||
requester_agent_id: Some(requester),
|
||||
target_agent_id: owner,
|
||||
ticket_id: TicketId::from_uuid(Uuid::from_u128(6)),
|
||||
conversation_id,
|
||||
},
|
||||
domain::BackgroundTaskWakePolicy::RecordOnly,
|
||||
100,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let json = serde_json::to_value(BackgroundTaskDto::from(task)).unwrap();
|
||||
|
||||
assert_eq!(json["kind"], "headlessRendezvous");
|
||||
assert_eq!(json["requesterAgentId"], requester.to_string());
|
||||
assert_eq!(json["targetAgentId"], owner.to_string());
|
||||
assert_eq!(json["conversationId"], conversation_id.to_string());
|
||||
|
||||
let command = BackgroundTask::new(
|
||||
TaskId::from_uuid(Uuid::from_u128(7)),
|
||||
project_id,
|
||||
owner,
|
||||
BackgroundTaskKind::Command {
|
||||
label: "cargo test".to_owned(),
|
||||
},
|
||||
domain::BackgroundTaskWakePolicy::RecordOnly,
|
||||
100,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let json = serde_json::to_value(BackgroundTaskDto::from(command)).unwrap();
|
||||
assert!(json.get("requesterAgentId").is_none());
|
||||
assert!(json.get("targetAgentId").is_none());
|
||||
assert!(json.get("conversationId").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_background_task_state_dto_exposes_rendezvous_context() {
|
||||
let requester = AgentId::from_uuid(Uuid::from_u128(11));
|
||||
let target = AgentId::from_uuid(Uuid::from_u128(12));
|
||||
let conversation_id = ConversationId::from_uuid(Uuid::from_u128(13));
|
||||
let state = AgentBackgroundTaskState {
|
||||
task_id: TaskId::from_uuid(Uuid::from_u128(14)),
|
||||
kind: BackgroundTaskKindLabel::HeadlessRendezvous,
|
||||
state: BackgroundTaskState::Completed,
|
||||
exit_code: None,
|
||||
summary: Some("ok".to_owned()),
|
||||
stdout_tail: None,
|
||||
stderr_tail: None,
|
||||
requester_agent_id: Some(requester),
|
||||
target_agent_id: Some(target),
|
||||
conversation_id: Some(conversation_id),
|
||||
created_at_ms: 100,
|
||||
updated_at_ms: 200,
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(AgentBackgroundTaskStateDto::from(state)).unwrap();
|
||||
|
||||
assert_eq!(json["requesterAgentId"], requester.to_string());
|
||||
assert_eq!(json["targetAgentId"], target.to_string());
|
||||
assert_eq!(json["conversationId"], conversation_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 ».
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//! Domain events published on the [`crate::ports::EventBus`] and relayed to the
|
||||
//! presentation layer (ARCHITECTURE §3.2).
|
||||
|
||||
use crate::conversation::ConversationParty;
|
||||
use crate::conversation::{ConversationId, ConversationParty};
|
||||
use crate::device::DeviceId;
|
||||
use crate::ids::{
|
||||
AgentId, IssueId, LocalModelServerId, ProfileId, ProjectId, SessionId, SkillId, SprintId,
|
||||
@ -14,6 +14,18 @@ use crate::plugin::{PluginId, PluginMcpServerId, PluginVersion};
|
||||
use crate::sprint::{SprintOrder, SprintVersion};
|
||||
use crate::template::TemplateVersion;
|
||||
|
||||
/// Context carried by terminal background-task events when they come from a
|
||||
/// headless inter-agent rendezvous.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RendezvousContext {
|
||||
/// Agent that requested the rendezvous, when known.
|
||||
pub requester_agent_id: Option<AgentId>,
|
||||
/// Target agent that owns the rendezvous conversation.
|
||||
pub target_agent_id: AgentId,
|
||||
/// Conversation opened by the target turn.
|
||||
pub conversation_id: ConversationId,
|
||||
}
|
||||
|
||||
/// Which entry door a processed orchestration request arrived through.
|
||||
///
|
||||
/// IdeA exposes the *same* [`crate::OrchestratorService::dispatch`] behind two
|
||||
@ -142,6 +154,8 @@ pub enum DomainEvent {
|
||||
task_id: TaskId,
|
||||
/// The agent that owns completion delivery.
|
||||
owner_agent_id: AgentId,
|
||||
/// Rendezvous context when this terminal task is a headless ask.
|
||||
rendezvous: Option<RendezvousContext>,
|
||||
},
|
||||
/// A first-class background task failed.
|
||||
BackgroundTaskFailed {
|
||||
@ -151,6 +165,8 @@ pub enum DomainEvent {
|
||||
task_id: TaskId,
|
||||
/// The agent that owns completion delivery.
|
||||
owner_agent_id: AgentId,
|
||||
/// Rendezvous context when this terminal task is a headless ask.
|
||||
rendezvous: Option<RendezvousContext>,
|
||||
},
|
||||
/// A first-class background task was cancelled.
|
||||
BackgroundTaskCancelled {
|
||||
@ -160,6 +176,8 @@ pub enum DomainEvent {
|
||||
task_id: TaskId,
|
||||
/// The agent that owns completion delivery.
|
||||
owner_agent_id: AgentId,
|
||||
/// Rendezvous context when this terminal task is a headless ask.
|
||||
rendezvous: Option<RendezvousContext>,
|
||||
},
|
||||
/// A first-class background task has a terminal result not yet delivered.
|
||||
BackgroundTaskCompletionDeliveryPending {
|
||||
|
||||
@ -76,6 +76,7 @@ mod validation;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub use error::DomainError;
|
||||
pub use events::RendezvousContext;
|
||||
|
||||
pub use ids::{
|
||||
AgentId, IssueId, LayoutId, LocalModelServerId, NodeId, ProfileId, ProjectId, RuntimeAgentKey,
|
||||
|
||||
@ -5874,6 +5874,7 @@ mod tests {
|
||||
project_id,
|
||||
task_id,
|
||||
owner_agent_id: owner,
|
||||
rendezvous: None,
|
||||
});
|
||||
let frame = tokio::time::timeout(Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user