ticket117: synchronie métier idea_ask_agent + corrélation task ↔ rendezvous
This commit is contained in:
@ -3698,6 +3698,7 @@ pub async fn spawn_background_command(
|
|||||||
label: request.label,
|
label: request.label,
|
||||||
command,
|
command,
|
||||||
wake_policy,
|
wake_policy,
|
||||||
|
rendezvous: None,
|
||||||
deadline_ms: request.deadline_ms,
|
deadline_ms: request.deadline_ms,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -20,8 +20,8 @@ use domain::ports::{
|
|||||||
IdGenerator, SpawnSpec,
|
IdGenerator, SpawnSpec,
|
||||||
};
|
};
|
||||||
use domain::{
|
use domain::{
|
||||||
AgentId, BackgroundTask, BackgroundTaskKind, BackgroundTaskResult, BackgroundTaskState,
|
AgentId, BackgroundTask, BackgroundTaskKind, BackgroundTaskRendezvousLink,
|
||||||
BackgroundTaskWakePolicy, ProjectId, TaskId,
|
BackgroundTaskResult, BackgroundTaskState, BackgroundTaskWakePolicy, ProjectId, TaskId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
@ -52,6 +52,8 @@ pub struct SpawnBackgroundCommandInput {
|
|||||||
pub command: SpawnSpec,
|
pub command: SpawnSpec,
|
||||||
/// Completion wake policy.
|
/// Completion wake policy.
|
||||||
pub wake_policy: BackgroundTaskWakePolicy,
|
pub wake_policy: BackgroundTaskWakePolicy,
|
||||||
|
/// Optional composite rendezvous this task participates in.
|
||||||
|
pub rendezvous: Option<BackgroundTaskRendezvousLink>,
|
||||||
/// Optional absolute deadline, epoch milliseconds.
|
/// Optional absolute deadline, epoch milliseconds.
|
||||||
pub deadline_ms: Option<u64>,
|
pub deadline_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
@ -103,6 +105,7 @@ impl SpawnBackgroundCommand {
|
|||||||
input.label,
|
input.label,
|
||||||
input.command,
|
input.command,
|
||||||
input.wake_policy,
|
input.wake_policy,
|
||||||
|
input.rendezvous,
|
||||||
input.deadline_ms,
|
input.deadline_ms,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -117,12 +120,13 @@ impl SpawnBackgroundCommand {
|
|||||||
label: String,
|
label: String,
|
||||||
command: SpawnSpec,
|
command: SpawnSpec,
|
||||||
wake_policy: BackgroundTaskWakePolicy,
|
wake_policy: BackgroundTaskWakePolicy,
|
||||||
|
rendezvous: Option<BackgroundTaskRendezvousLink>,
|
||||||
deadline_ms: Option<u64>,
|
deadline_ms: Option<u64>,
|
||||||
) -> Result<BackgroundTask, AppError> {
|
) -> Result<BackgroundTask, AppError> {
|
||||||
let task_id = TaskId::from_uuid(self.ids.new_uuid());
|
let task_id = TaskId::from_uuid(self.ids.new_uuid());
|
||||||
let now = u64::try_from(self.clock.now_millis().max(0)).unwrap_or(0);
|
let now = u64::try_from(self.clock.now_millis().max(0)).unwrap_or(0);
|
||||||
|
|
||||||
let task = BackgroundTask::new(
|
let mut task = BackgroundTask::new(
|
||||||
task_id,
|
task_id,
|
||||||
project_id,
|
project_id,
|
||||||
owner_agent_id,
|
owner_agent_id,
|
||||||
@ -132,6 +136,9 @@ impl SpawnBackgroundCommand {
|
|||||||
deadline_ms,
|
deadline_ms,
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||||
|
if let Some(link) = rendezvous {
|
||||||
|
task = task.with_rendezvous(link);
|
||||||
|
}
|
||||||
self.store.create(&task).await.map_err(map_port_err)?;
|
self.store.create(&task).await.map_err(map_port_err)?;
|
||||||
|
|
||||||
let running = task
|
let running = task
|
||||||
@ -287,6 +294,7 @@ impl RetryBackgroundTask {
|
|||||||
command,
|
command,
|
||||||
old.wake_policy,
|
old.wake_policy,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(SpawnBackgroundCommandOutput { task })
|
Ok(SpawnBackgroundCommandOutput { task })
|
||||||
|
|||||||
@ -27,9 +27,10 @@ use domain::mailbox::{Ticket, TicketId};
|
|||||||
use domain::ports::{BackgroundTaskStore, Clock, EventBus, ProfileStore, PtyHandle};
|
use domain::ports::{BackgroundTaskStore, Clock, EventBus, ProfileStore, PtyHandle};
|
||||||
use domain::project::ProjectPath;
|
use domain::project::ProjectPath;
|
||||||
use domain::{
|
use domain::{
|
||||||
AgentId, AgentProfile, BackgroundTask, BackgroundTaskKind, BackgroundTaskResult,
|
AgentId, AgentProfile, BackgroundTask, BackgroundTaskKind, BackgroundTaskRendezvousLink,
|
||||||
BackgroundTaskState, BackgroundTaskWakePolicy, DomainEvent, OrchestratorCommand,
|
BackgroundTaskResult, BackgroundTaskState, BackgroundTaskWakePolicy, DomainEvent,
|
||||||
OrchestratorVisibility, ProfileId, Project, RuntimeAgentKey, TaskId,
|
OrchestratorCommand, OrchestratorVisibility, ProfileId, Project, RendezvousId, RuntimeAgentKey,
|
||||||
|
TaskId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::conversation::RecordTurn;
|
use crate::conversation::RecordTurn;
|
||||||
@ -128,6 +129,13 @@ fn resolve_background_cwd(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn rendezvous_context_for_task(task: &BackgroundTask) -> Option<domain::RendezvousContext> {
|
fn rendezvous_context_for_task(task: &BackgroundTask) -> Option<domain::RendezvousContext> {
|
||||||
|
if let Some(link) = &task.rendezvous {
|
||||||
|
return Some(domain::RendezvousContext {
|
||||||
|
requester_agent_id: link.requester_agent_id,
|
||||||
|
target_agent_id: link.target_agent_id,
|
||||||
|
conversation_id: link.conversation_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
match &task.kind {
|
match &task.kind {
|
||||||
BackgroundTaskKind::HeadlessRendezvous {
|
BackgroundTaskKind::HeadlessRendezvous {
|
||||||
requester_agent_id,
|
requester_agent_id,
|
||||||
@ -413,6 +421,10 @@ pub struct OrchestratorService {
|
|||||||
/// Séparé de [`WaitForGraph`] qui reste un objet domaine minimal de détection de
|
/// Séparé de [`WaitForGraph`] qui reste un objet domaine minimal de détection de
|
||||||
/// cycle, sans API de traversal.
|
/// cycle, sans API de traversal.
|
||||||
active_waits: StdMutex<Vec<(AgentId, AgentId)>>,
|
active_waits: StdMutex<Vec<(AgentId, AgentId)>>,
|
||||||
|
/// Composite business rendezvous currently driven by a target agent. Used to
|
||||||
|
/// structurally attach `idea_run_in_background` tasks launched during
|
||||||
|
/// `idea_ask_agent` without parsing the model's textual `Final`.
|
||||||
|
active_rendezvous: StdMutex<HashMap<RuntimeAgentKey, ActiveRendezvous>>,
|
||||||
/// Bus d'événements pour publier [`DomainEvent::AgentReplied`] à l'issue d'un
|
/// Bus d'événements pour publier [`DomainEvent::AgentReplied`] à l'issue d'un
|
||||||
/// `ask` réussi (§17.4). Injecté via [`Self::with_events`] ; `None` ⇒ pas de
|
/// `ask` réussi (§17.4). Injecté via [`Self::with_events`] ; `None` ⇒ pas de
|
||||||
/// publication (l'`ask` fonctionne quand même).
|
/// publication (l'`ask` fonctionne quand même).
|
||||||
@ -541,6 +553,12 @@ pub struct OrchestratorOutcome {
|
|||||||
pub reply: Option<String>,
|
pub reply: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ActiveRendezvous {
|
||||||
|
link: BackgroundTaskRendezvousLink,
|
||||||
|
background_tasks: Vec<TaskId>,
|
||||||
|
}
|
||||||
|
|
||||||
impl OrchestratorService {
|
impl OrchestratorService {
|
||||||
/// Builds the service from the use cases and ports it dispatches to.
|
/// Builds the service from the use cases and ports it dispatches to.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@ -569,6 +587,7 @@ impl OrchestratorService {
|
|||||||
conversations: None,
|
conversations: None,
|
||||||
wait_for: StdMutex::new(WaitForGraph::new()),
|
wait_for: StdMutex::new(WaitForGraph::new()),
|
||||||
active_waits: StdMutex::new(Vec::new()),
|
active_waits: StdMutex::new(Vec::new()),
|
||||||
|
active_rendezvous: StdMutex::new(HashMap::new()),
|
||||||
events: None,
|
events: None,
|
||||||
ask_locks: StdMutex::new(HashMap::new()),
|
ask_locks: StdMutex::new(HashMap::new()),
|
||||||
mcp_runtime_provider: None,
|
mcp_runtime_provider: None,
|
||||||
@ -681,6 +700,47 @@ impl OrchestratorService {
|
|||||||
Arc::clone(locks.entry(*agent_id).or_default())
|
Arc::clone(locks.entry(*agent_id).or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn active_rendezvous_link(
|
||||||
|
&self,
|
||||||
|
agent: RuntimeAgentKey,
|
||||||
|
) -> Option<BackgroundTaskRendezvousLink> {
|
||||||
|
self.active_rendezvous
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.get(&agent)
|
||||||
|
.map(|r| r.link.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attach_background_to_active_rendezvous(
|
||||||
|
&self,
|
||||||
|
agent: RuntimeAgentKey,
|
||||||
|
rendezvous_id: RendezvousId,
|
||||||
|
task_id: TaskId,
|
||||||
|
) {
|
||||||
|
if let Some(active) = self
|
||||||
|
.active_rendezvous
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.get_mut(&agent)
|
||||||
|
.filter(|active| active.link.rendezvous_id == rendezvous_id)
|
||||||
|
{
|
||||||
|
active.background_tasks.push(task_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_rendezvous_has_background_tasks(
|
||||||
|
&self,
|
||||||
|
agent: RuntimeAgentKey,
|
||||||
|
rendezvous_id: RendezvousId,
|
||||||
|
) -> bool {
|
||||||
|
self.active_rendezvous
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.get(&agent)
|
||||||
|
.filter(|active| active.link.rendezvous_id == rendezvous_id)
|
||||||
|
.is_some_and(|active| !active.background_tasks.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the transitive set of agents currently waited on by `agent`.
|
/// Returns the transitive set of agents currently waited on by `agent`.
|
||||||
///
|
///
|
||||||
/// Used by user-driven cancellation: stopping A while A waits on B should also
|
/// Used by user-driven cancellation: stopping A while A waits on B should also
|
||||||
@ -750,10 +810,7 @@ impl OrchestratorService {
|
|||||||
&self,
|
&self,
|
||||||
project: &Project,
|
project: &Project,
|
||||||
owner_agent_id: AgentId,
|
owner_agent_id: AgentId,
|
||||||
requester_agent_id: Option<AgentId>,
|
link: BackgroundTaskRendezvousLink,
|
||||||
target_agent_id: AgentId,
|
|
||||||
ticket_id: TicketId,
|
|
||||||
conversation_id: domain::conversation::ConversationId,
|
|
||||||
) -> Result<Option<TaskId>, AppError> {
|
) -> Result<Option<TaskId>, AppError> {
|
||||||
let Some(store) = &self.background_tasks else {
|
let Some(store) = &self.background_tasks else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@ -765,16 +822,17 @@ impl OrchestratorService {
|
|||||||
project.id,
|
project.id,
|
||||||
owner_agent_id,
|
owner_agent_id,
|
||||||
BackgroundTaskKind::HeadlessRendezvous {
|
BackgroundTaskKind::HeadlessRendezvous {
|
||||||
requester_agent_id,
|
requester_agent_id: link.requester_agent_id,
|
||||||
target_agent_id,
|
target_agent_id: link.target_agent_id,
|
||||||
ticket_id,
|
ticket_id: link.ticket_id,
|
||||||
conversation_id,
|
conversation_id: link.conversation_id,
|
||||||
},
|
},
|
||||||
BackgroundTaskWakePolicy::RecordOnly,
|
BackgroundTaskWakePolicy::RecordOnly,
|
||||||
now,
|
now,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.map_err(|err| AppError::Internal(err.to_string()))?
|
.map_err(|err| AppError::Internal(err.to_string()))?
|
||||||
|
.with_rendezvous(link)
|
||||||
.transition(BackgroundTaskState::Running, now)
|
.transition(BackgroundTaskState::Running, now)
|
||||||
.map_err(|err| AppError::Internal(err.to_string()))?;
|
.map_err(|err| AppError::Internal(err.to_string()))?;
|
||||||
store
|
store
|
||||||
@ -812,6 +870,27 @@ impl OrchestratorService {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn mark_rendezvous_task_waiting(&self, task_id: Option<TaskId>) -> Result<(), AppError> {
|
||||||
|
let (Some(store), Some(task_id)) = (&self.background_tasks, task_id) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let task = store
|
||||||
|
.get(task_id)
|
||||||
|
.await
|
||||||
|
.map_err(|err| AppError::Store(err.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::Store(format!("background task {task_id} not found")))?;
|
||||||
|
if task.is_terminal() || task.state == BackgroundTaskState::Waiting {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let waiting = task
|
||||||
|
.transition(BackgroundTaskState::Waiting, self.now_ms())
|
||||||
|
.map_err(|err| AppError::Internal(err.to_string()))?;
|
||||||
|
store
|
||||||
|
.save(&waiting)
|
||||||
|
.await
|
||||||
|
.map_err(|err| AppError::Store(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
async fn complete_rendezvous_task_failure(
|
async fn complete_rendezvous_task_failure(
|
||||||
&self,
|
&self,
|
||||||
project: &Project,
|
project: &Project,
|
||||||
@ -1223,6 +1302,8 @@ impl OrchestratorService {
|
|||||||
AppError::Invalid("background command runner is not configured".to_owned())
|
AppError::Invalid("background command runner is not configured".to_owned())
|
||||||
})?;
|
})?;
|
||||||
let cwd = resolve_background_cwd(&project.root, cwd)?;
|
let cwd = resolve_background_cwd(&project.root, cwd)?;
|
||||||
|
let agent_key = runtime_key(project, owner);
|
||||||
|
let rendezvous = self.active_rendezvous_link(agent_key);
|
||||||
let output = spawn
|
let output = spawn
|
||||||
.execute(SpawnBackgroundCommandInput {
|
.execute(SpawnBackgroundCommandInput {
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@ -1237,9 +1318,17 @@ impl OrchestratorService {
|
|||||||
sandbox: None,
|
sandbox: None,
|
||||||
},
|
},
|
||||||
wake_policy: BackgroundTaskWakePolicy::WakeOwner,
|
wake_policy: BackgroundTaskWakePolicy::WakeOwner,
|
||||||
|
rendezvous: rendezvous.clone(),
|
||||||
deadline_ms,
|
deadline_ms,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
if let Some(link) = rendezvous {
|
||||||
|
self.attach_background_to_active_rendezvous(
|
||||||
|
agent_key,
|
||||||
|
link.rendezvous_id,
|
||||||
|
output.task.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
let state = match output.task.state {
|
let state = match output.task.state {
|
||||||
BackgroundTaskState::Queued => "Queued",
|
BackgroundTaskState::Queued => "Queued",
|
||||||
BackgroundTaskState::Running => "Running",
|
BackgroundTaskState::Running => "Running",
|
||||||
@ -1812,17 +1901,19 @@ impl OrchestratorService {
|
|||||||
// Timeout de tour piloté par profil (lot 2) + armement du seuil de stall, AVANT
|
// Timeout de tour piloté par profil (lot 2) + armement du seuil de stall, AVANT
|
||||||
// l'enqueue qui démarre le tour (le médiateur arme alors sa fenêtre de vivacité).
|
// l'enqueue qui démarre le tour (le médiateur arme alors sa fenêtre de vivacité).
|
||||||
let turn_timeout = self.turn_timeout_for(project, agent_id).await;
|
let turn_timeout = self.turn_timeout_for(project, agent_id).await;
|
||||||
let _pending = input.enqueue_silent(agent_key, ticket);
|
let pending = input.enqueue_silent(agent_key, ticket);
|
||||||
|
let rendezvous_link = BackgroundTaskRendezvousLink {
|
||||||
|
rendezvous_id: RendezvousId::new_random(),
|
||||||
|
ticket_id,
|
||||||
|
requester_agent_id: requester,
|
||||||
|
target_agent_id: agent_id,
|
||||||
|
conversation_id,
|
||||||
|
};
|
||||||
let rendezvous_task = self
|
let rendezvous_task = self
|
||||||
.start_rendezvous_task(
|
.start_rendezvous_task(project, agent_id, rendezvous_link.clone())
|
||||||
project,
|
|
||||||
agent_id,
|
|
||||||
requester,
|
|
||||||
agent_id,
|
|
||||||
ticket_id,
|
|
||||||
conversation_id,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
let _active_rendezvous =
|
||||||
|
ActiveRendezvousGuard::new(self, agent_key, rendezvous_link.clone());
|
||||||
// Auto-update live-state (lot LS3), best-effort : la cible passe `Working` sur
|
// Auto-update live-state (lot LS3), best-effort : la cible passe `Working` sur
|
||||||
// cette transition d'`ask` acceptée (chemin structuré). `task` est encore vivant
|
// cette transition d'`ask` acceptée (chemin structuré). `task` est encore vivant
|
||||||
// ici (utilisé par le drain plus bas), on le distille directement.
|
// ici (utilisé par le drain plus bas), on le distille directement.
|
||||||
@ -1918,15 +2009,13 @@ impl OrchestratorService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Borne par la fenêtre d'inactivité (réarmée sur signe de vie) sous plafond absolu.
|
// Borne par la fenêtre d'inactivité (réarmée sur signe de vie) sous plafond absolu.
|
||||||
let result = match self
|
let initial_result = match self
|
||||||
.run_ask_with_watchdog(wait, turn_timeout, &project.root, agent_id, target, started)
|
.run_ask_with_watchdog(wait, turn_timeout, &project.root, agent_id, target, started)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
WatchdogOutcome::Resolved(Ok(content)) => {
|
WatchdogOutcome::Resolved(Ok(content)) => {
|
||||||
self.complete_rendezvous_task_success(project, agent_id, rendezvous_task, &content)
|
|
||||||
.await?;
|
|
||||||
crate::diag!(
|
crate::diag!(
|
||||||
"[rendezvous] ask resolved (structured): target={target} \
|
"[rendezvous] ask turn finalized (structured): target={target} \
|
||||||
(agent {agent_id}) ticket={ticket_id} after_ms={} reply_len={}",
|
(agent {agent_id}) ticket={ticket_id} after_ms={} reply_len={}",
|
||||||
started.elapsed().as_millis(),
|
started.elapsed().as_millis(),
|
||||||
content.len(),
|
content.len(),
|
||||||
@ -2012,6 +2101,67 @@ impl OrchestratorService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let result = if self
|
||||||
|
.active_rendezvous_has_background_tasks(agent_key, rendezvous_link.rendezvous_id)
|
||||||
|
{
|
||||||
|
self.mark_rendezvous_task_waiting(rendezvous_task).await?;
|
||||||
|
crate::diag!(
|
||||||
|
"[rendezvous] ask waiting on background: target={target} \
|
||||||
|
(agent {agent_id}) ticket={ticket_id} rendezvous={} first_final_len={}",
|
||||||
|
rendezvous_link.rendezvous_id,
|
||||||
|
initial_result.len(),
|
||||||
|
);
|
||||||
|
let remaining = self.ask_ceiling.saturating_sub(started.elapsed());
|
||||||
|
match tokio::time::timeout(remaining, pending).await {
|
||||||
|
Ok(Ok(domain::mailbox::TurnResolution::Replied(content))) => content,
|
||||||
|
Ok(Ok(domain::mailbox::TurnResolution::ReturnedToPromptNoReply)) => {
|
||||||
|
let err = AppError::TargetReturnedNoReply(target.to_owned());
|
||||||
|
self.complete_rendezvous_task_failure(
|
||||||
|
project,
|
||||||
|
agent_id,
|
||||||
|
rendezvous_task,
|
||||||
|
format!("NoReply: {err}"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
self.mark_target_done_best_effort(&project.root, agent_id, ticket_id)
|
||||||
|
.await;
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
let err = AppError::Process(err.to_string());
|
||||||
|
self.complete_rendezvous_task_failure(
|
||||||
|
project,
|
||||||
|
agent_id,
|
||||||
|
rendezvous_task,
|
||||||
|
format!("NoReply: {err}"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
self.mark_target_done_best_effort(&project.root, agent_id, ticket_id)
|
||||||
|
.await;
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
Err(_elapsed) => {
|
||||||
|
self.complete_rendezvous_task_failure(
|
||||||
|
project,
|
||||||
|
agent_id,
|
||||||
|
rendezvous_task,
|
||||||
|
format!(
|
||||||
|
"Timeout: composite rendezvous ceiling reached for target {target}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
self.mark_target_done_best_effort(&project.root, agent_id, ticket_id)
|
||||||
|
.await;
|
||||||
|
return Err(AppError::TargetCeilingActive(target.to_owned()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initial_result
|
||||||
|
};
|
||||||
|
|
||||||
|
self.complete_rendezvous_task_success(project, agent_id, rendezvous_task, &result)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Succès : le `Final` a rendu la réponse. On retire explicitement le ticket de
|
// Succès : le `Final` a rendu la réponse. On retire explicitement le ticket de
|
||||||
// comptabilité (aucun `idea_reply` ne le fera), puis on désarme le garde RAII.
|
// comptabilité (aucun `idea_reply` ne le fera), puis on désarme le garde RAII.
|
||||||
mailbox.cancel_head(agent_key, ticket_id);
|
mailbox.cancel_head(agent_key, ticket_id);
|
||||||
@ -2915,6 +3065,53 @@ impl Drop for WaitEdgeGuard<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ActiveRendezvousGuard<'a> {
|
||||||
|
active: &'a StdMutex<HashMap<RuntimeAgentKey, ActiveRendezvous>>,
|
||||||
|
agent: RuntimeAgentKey,
|
||||||
|
rendezvous_id: RendezvousId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ActiveRendezvousGuard<'a> {
|
||||||
|
fn new(
|
||||||
|
service: &'a OrchestratorService,
|
||||||
|
agent: RuntimeAgentKey,
|
||||||
|
link: BackgroundTaskRendezvousLink,
|
||||||
|
) -> Self {
|
||||||
|
let rendezvous_id = link.rendezvous_id;
|
||||||
|
service
|
||||||
|
.active_rendezvous
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.insert(
|
||||||
|
agent,
|
||||||
|
ActiveRendezvous {
|
||||||
|
link,
|
||||||
|
background_tasks: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
active: &service.active_rendezvous,
|
||||||
|
agent,
|
||||||
|
rendezvous_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ActiveRendezvousGuard<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut active = self
|
||||||
|
.active
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
if active
|
||||||
|
.get(&self.agent)
|
||||||
|
.is_some_and(|entry| entry.link.rendezvous_id == self.rendezvous_id)
|
||||||
|
{
|
||||||
|
active.remove(&self.agent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Normalises a profile reference for tolerant matching: lowercased, with spaces,
|
/// Normalises a profile reference for tolerant matching: lowercased, with spaces,
|
||||||
/// dashes and underscores stripped (`"Claude Code"`, `"claude-code"`, `"claude"`
|
/// dashes and underscores stripped (`"Claude Code"`, `"claude-code"`, `"claude"`
|
||||||
/// → comparable forms; `claude` ⊂ ... handled by the command match above).
|
/// → comparable forms; `claude` ⊂ ... handled by the command match above).
|
||||||
|
|||||||
@ -134,10 +134,33 @@ impl AgentWakeService {
|
|||||||
owner_agent_id: agent,
|
owner_agent_id: agent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
drain_reply_stream_with_readiness(stream, self.input.as_ref(), key)
|
let final_content = drain_reply_stream_with_readiness(stream, self.input.as_ref(), key)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| WakeError::Session(err.to_string()))?;
|
.map_err(|err| WakeError::Session(err.to_string()))?;
|
||||||
|
|
||||||
|
if let Some(task_id) = delivery.delivered_task_id {
|
||||||
|
let task = self.load_task(task_id).await?;
|
||||||
|
if let Some(link) = task.rendezvous.as_ref() {
|
||||||
|
let still_waiting = self
|
||||||
|
.tasks
|
||||||
|
.list_open_for_agent(agent)
|
||||||
|
.await
|
||||||
|
.map_err(|err| WakeError::Store(err.to_string()))?
|
||||||
|
.into_iter()
|
||||||
|
.any(|open| {
|
||||||
|
open.id != task_id
|
||||||
|
&& open.rendezvous.as_ref().is_some_and(|open_link| {
|
||||||
|
open_link.rendezvous_id == link.rendezvous_id
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !still_waiting {
|
||||||
|
self.mailbox
|
||||||
|
.resolve_ticket(key, link.ticket_id, final_content)
|
||||||
|
.map_err(|err| WakeError::Task(err.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.mailbox.cancel_head(key, delivery.ticket_id);
|
self.mailbox.cancel_head(key, delivery.ticket_id);
|
||||||
self.input.mark_idle(key);
|
self.input.mark_idle(key);
|
||||||
guard.disarm();
|
guard.disarm();
|
||||||
|
|||||||
@ -4,10 +4,10 @@ use std::sync::{Arc, Mutex};
|
|||||||
use application::{AgentWakeService, WakeSessionProvider};
|
use application::{AgentWakeService, WakeSessionProvider};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::background_task::{
|
use domain::background_task::{
|
||||||
BackgroundTask, BackgroundTaskKind, BackgroundTaskResult, BackgroundTaskState,
|
BackgroundTask, BackgroundTaskKind, BackgroundTaskRendezvousLink, BackgroundTaskResult,
|
||||||
BackgroundTaskWakePolicy,
|
BackgroundTaskState, BackgroundTaskWakePolicy,
|
||||||
};
|
};
|
||||||
use domain::ids::{AgentId, ProjectId, RuntimeAgentKey, SessionId, TaskId};
|
use domain::ids::{AgentId, ProjectId, RendezvousId, RuntimeAgentKey, SessionId, TaskId};
|
||||||
use domain::inbox::{
|
use domain::inbox::{
|
||||||
AgentInbox, AgentInboxSnapshot, InboxError, InboxItem, InboxItemKind, InboxReceipt,
|
AgentInbox, AgentInboxSnapshot, InboxError, InboxItem, InboxItemKind, InboxReceipt,
|
||||||
InboxReceiptStatus, InboxSource,
|
InboxReceiptStatus, InboxSource,
|
||||||
@ -20,6 +20,7 @@ use domain::ports::{
|
|||||||
};
|
};
|
||||||
use domain::project::{Project, ProjectPath};
|
use domain::project::{Project, ProjectPath};
|
||||||
use domain::remote::RemoteRef;
|
use domain::remote::RemoteRef;
|
||||||
|
use domain::ConversationId;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
fn id(n: u128) -> Uuid {
|
fn id(n: u128) -> Uuid {
|
||||||
@ -95,6 +96,37 @@ fn completed_task(
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn completed_rendezvous_task(
|
||||||
|
project: &Project,
|
||||||
|
owner: AgentId,
|
||||||
|
task_id: TaskId,
|
||||||
|
link: BackgroundTaskRendezvousLink,
|
||||||
|
) -> BackgroundTask {
|
||||||
|
BackgroundTask::new(
|
||||||
|
task_id,
|
||||||
|
project.id,
|
||||||
|
owner,
|
||||||
|
BackgroundTaskKind::Command {
|
||||||
|
label: "build".to_owned(),
|
||||||
|
},
|
||||||
|
BackgroundTaskWakePolicy::WakeOwner,
|
||||||
|
1,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.with_rendezvous(link)
|
||||||
|
.transition(BackgroundTaskState::Running, 10)
|
||||||
|
.unwrap()
|
||||||
|
.complete(BackgroundTaskResult::Success {
|
||||||
|
finished_at_ms: 20,
|
||||||
|
exit_code: Some(0),
|
||||||
|
summary: "done".to_owned(),
|
||||||
|
stdout_tail: None,
|
||||||
|
stderr_tail: None,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FakeInbox {
|
struct FakeInbox {
|
||||||
queues: Mutex<HashMap<RuntimeAgentKey, VecDeque<InboxItem>>>,
|
queues: Mutex<HashMap<RuntimeAgentKey, VecDeque<InboxItem>>>,
|
||||||
@ -147,6 +179,7 @@ impl AgentInbox for FakeInbox {
|
|||||||
struct SharedTurnState {
|
struct SharedTurnState {
|
||||||
busy: Mutex<HashMap<RuntimeAgentKey, AgentBusyState>>,
|
busy: Mutex<HashMap<RuntimeAgentKey, AgentBusyState>>,
|
||||||
tickets: Mutex<HashMap<RuntimeAgentKey, VecDeque<Ticket>>>,
|
tickets: Mutex<HashMap<RuntimeAgentKey, VecDeque<Ticket>>>,
|
||||||
|
replies: Mutex<Vec<(TicketId, String)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SharedTurnState {
|
impl SharedTurnState {
|
||||||
@ -168,6 +201,10 @@ impl SharedTurnState {
|
|||||||
.map(VecDeque::len)
|
.map(VecDeque::len)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replies(&self) -> Vec<(TicketId, String)> {
|
||||||
|
self.replies.lock().unwrap().clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputMediator for SharedTurnState {
|
impl InputMediator for SharedTurnState {
|
||||||
@ -220,6 +257,25 @@ impl AgentMailbox for SharedTurnState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_ticket(
|
||||||
|
&self,
|
||||||
|
agent: RuntimeAgentKey,
|
||||||
|
ticket_id: TicketId,
|
||||||
|
result: String,
|
||||||
|
) -> Result<(), MailboxError> {
|
||||||
|
let mut tickets = self.tickets.lock().unwrap();
|
||||||
|
let queue = tickets
|
||||||
|
.get_mut(&agent)
|
||||||
|
.ok_or(MailboxError::NoPendingRequest(agent.agent_id))?;
|
||||||
|
let pos = queue
|
||||||
|
.iter()
|
||||||
|
.position(|ticket| ticket.id == ticket_id)
|
||||||
|
.ok_or(MailboxError::NoPendingRequest(agent.agent_id))?;
|
||||||
|
queue.remove(pos);
|
||||||
|
self.replies.lock().unwrap().push((ticket_id, result));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn cancel_head(&self, agent: RuntimeAgentKey, ticket_id: TicketId) {
|
fn cancel_head(&self, agent: RuntimeAgentKey, ticket_id: TicketId) {
|
||||||
let mut tickets = self.tickets.lock().unwrap();
|
let mut tickets = self.tickets.lock().unwrap();
|
||||||
if let Some(queue) = tickets.get_mut(&agent) {
|
if let Some(queue) = tickets.get_mut(&agent) {
|
||||||
@ -230,6 +286,65 @@ impl AgentMailbox for SharedTurnState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rendezvous_background_completion_resolves_original_ticket_with_business_final() {
|
||||||
|
let project = project();
|
||||||
|
let owner = agent(1);
|
||||||
|
let requester = agent(2);
|
||||||
|
let task_id = task_id(10);
|
||||||
|
let rendezvous_ticket = ticket(99);
|
||||||
|
let conversation_id = ConversationId::from_uuid(id(300));
|
||||||
|
let rendezvous_id = RendezvousId::from_uuid(id(400));
|
||||||
|
let link = BackgroundTaskRendezvousLink {
|
||||||
|
rendezvous_id,
|
||||||
|
ticket_id: rendezvous_ticket,
|
||||||
|
requester_agent_id: Some(requester),
|
||||||
|
target_agent_id: owner,
|
||||||
|
conversation_id,
|
||||||
|
};
|
||||||
|
let inbox = Arc::new(FakeInbox::default());
|
||||||
|
let turns = Arc::new(SharedTurnState::default());
|
||||||
|
let tasks = Arc::new(FakeTaskStore::default());
|
||||||
|
let session = Arc::new(FakeSession::with_events(vec![ReplyEvent::Final {
|
||||||
|
content: "real business answer".to_owned(),
|
||||||
|
}]));
|
||||||
|
let sessions = Arc::new(FakeSessionProvider::with_session(session));
|
||||||
|
|
||||||
|
turns.enqueue_silent(
|
||||||
|
runtime_key(owner),
|
||||||
|
Ticket::from_agent(
|
||||||
|
rendezvous_ticket,
|
||||||
|
requester,
|
||||||
|
conversation_id,
|
||||||
|
"Requester",
|
||||||
|
"original ask",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
turns.mark_idle(runtime_key(owner));
|
||||||
|
tasks.insert(completed_rendezvous_task(&project, owner, task_id, link));
|
||||||
|
inbox
|
||||||
|
.enqueue_message(
|
||||||
|
runtime_key(owner),
|
||||||
|
completion_item(owner, task_id, ticket(20)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
service(inbox, turns.clone(), tasks, sessions)
|
||||||
|
.wake_agent(
|
||||||
|
&project,
|
||||||
|
owner,
|
||||||
|
WakeReason::BackgroundCompletion { task_id },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
turns.replies(),
|
||||||
|
vec![(rendezvous_ticket, "real business answer".to_owned())]
|
||||||
|
);
|
||||||
|
assert_eq!(turns.ticket_depth(owner), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FakeTaskStore {
|
struct FakeTaskStore {
|
||||||
tasks: Mutex<HashMap<TaskId, BackgroundTask>>,
|
tasks: Mutex<HashMap<TaskId, BackgroundTask>>,
|
||||||
|
|||||||
@ -9,7 +9,8 @@ use async_trait::async_trait;
|
|||||||
use application::{
|
use application::{
|
||||||
DeleteModelArtifact, DeleteModelArtifactInput, DeleteModelServer, DeleteModelServerInput,
|
DeleteModelArtifact, DeleteModelArtifactInput, DeleteModelServer, DeleteModelServerInput,
|
||||||
EnsureLocalModelServer, EnsureLocalModelServerInput, LiveAgentRegistry, LiveSessionKind,
|
EnsureLocalModelServer, EnsureLocalModelServerInput, LiveAgentRegistry, LiveSessionKind,
|
||||||
LiveSessionSnapshot, ModelArtifactDownloadTracker, ModelServerReadinessPolicy, TerminalSessions,
|
LiveSessionSnapshot, ModelArtifactDownloadTracker, ModelServerReadinessPolicy,
|
||||||
|
TerminalSessions,
|
||||||
};
|
};
|
||||||
use domain::events::DomainEvent;
|
use domain::events::DomainEvent;
|
||||||
use domain::layout::Workspace;
|
use domain::layout::Workspace;
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::conversation::ConversationId;
|
use crate::conversation::ConversationId;
|
||||||
use crate::ids::{AgentId, ProjectId, ScheduleId, TaskId};
|
use crate::ids::{AgentId, ProjectId, RendezvousId, ScheduleId, TaskId};
|
||||||
use crate::mailbox::TicketId;
|
use crate::mailbox::TicketId;
|
||||||
|
|
||||||
/// Maximum length for human-facing task labels.
|
/// Maximum length for human-facing task labels.
|
||||||
@ -59,6 +59,22 @@ pub enum BackgroundTaskWakePolicy {
|
|||||||
RecordOnly,
|
RecordOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Structural link from a background task to a composite inter-agent rendezvous.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BackgroundTaskRendezvousLink {
|
||||||
|
/// Stable business rendezvous id, independent from any task id.
|
||||||
|
pub rendezvous_id: RendezvousId,
|
||||||
|
/// Mailbox ticket that must receive the eventual business `Final`.
|
||||||
|
pub ticket_id: TicketId,
|
||||||
|
/// Agent that requested the rendezvous, when known.
|
||||||
|
pub requester_agent_id: Option<AgentId>,
|
||||||
|
/// Target agent doing the work.
|
||||||
|
pub target_agent_id: AgentId,
|
||||||
|
/// Conversation entered by the target turn.
|
||||||
|
pub conversation_id: ConversationId,
|
||||||
|
}
|
||||||
|
|
||||||
/// Kind-specific payload of a background task.
|
/// Kind-specific payload of a background task.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
@ -107,6 +123,9 @@ pub struct BackgroundTask {
|
|||||||
pub owner_agent_id: AgentId,
|
pub owner_agent_id: AgentId,
|
||||||
/// Kind-specific payload.
|
/// Kind-specific payload.
|
||||||
pub kind: BackgroundTaskKind,
|
pub kind: BackgroundTaskKind,
|
||||||
|
/// Optional composite rendezvous this task participates in.
|
||||||
|
#[serde(default)]
|
||||||
|
pub rendezvous: Option<BackgroundTaskRendezvousLink>,
|
||||||
/// Current lifecycle state.
|
/// Current lifecycle state.
|
||||||
pub state: BackgroundTaskState,
|
pub state: BackgroundTaskState,
|
||||||
/// Completion wake policy.
|
/// Completion wake policy.
|
||||||
@ -143,6 +162,7 @@ impl BackgroundTask {
|
|||||||
project_id,
|
project_id,
|
||||||
owner_agent_id,
|
owner_agent_id,
|
||||||
kind,
|
kind,
|
||||||
|
rendezvous: None,
|
||||||
state: BackgroundTaskState::Queued,
|
state: BackgroundTaskState::Queued,
|
||||||
wake_policy,
|
wake_policy,
|
||||||
created_at_ms: now_ms,
|
created_at_ms: now_ms,
|
||||||
@ -155,6 +175,13 @@ impl BackgroundTask {
|
|||||||
Ok(task)
|
Ok(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a copy linked to a composite inter-agent rendezvous.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_rendezvous(mut self, link: BackgroundTaskRendezvousLink) -> Self {
|
||||||
|
self.rendezvous = Some(link);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a copy moved to a non-terminal state.
|
/// Returns a copy moved to a non-terminal state.
|
||||||
///
|
///
|
||||||
/// Terminal transitions must use [`Self::complete`] so the result/state
|
/// Terminal transitions must use [`Self::complete`] so the result/state
|
||||||
|
|||||||
@ -115,6 +115,10 @@ typed_id!(
|
|||||||
/// Identifies a first-class background task.
|
/// Identifies a first-class background task.
|
||||||
TaskId
|
TaskId
|
||||||
);
|
);
|
||||||
|
typed_id!(
|
||||||
|
/// Identifies one composite inter-agent business rendezvous.
|
||||||
|
RendezvousId
|
||||||
|
);
|
||||||
|
|
||||||
/// Runtime-only key for an agent scoped by its project.
|
/// Runtime-only key for an agent scoped by its project.
|
||||||
///
|
///
|
||||||
|
|||||||
@ -79,8 +79,8 @@ pub use error::DomainError;
|
|||||||
pub use events::RendezvousContext;
|
pub use events::RendezvousContext;
|
||||||
|
|
||||||
pub use ids::{
|
pub use ids::{
|
||||||
AgentId, IssueId, LayoutId, LocalModelServerId, NodeId, ProfileId, ProjectId, RuntimeAgentKey,
|
AgentId, IssueId, LayoutId, LocalModelServerId, NodeId, ProfileId, ProjectId, RendezvousId,
|
||||||
ScheduleId, SessionId, SkillId, SprintId, TabId, TaskId, TemplateId, WindowId,
|
RuntimeAgentKey, ScheduleId, SessionId, SkillId, SprintId, TabId, TaskId, TemplateId, WindowId,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use project::{Project, ProjectPath};
|
pub use project::{Project, ProjectPath};
|
||||||
@ -95,9 +95,10 @@ pub use mcp_tool_permissions::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub use background_task::{
|
pub use background_task::{
|
||||||
BackgroundTask, BackgroundTaskError, BackgroundTaskKind, BackgroundTaskResult,
|
BackgroundTask, BackgroundTaskError, BackgroundTaskKind, BackgroundTaskRendezvousLink,
|
||||||
BackgroundTaskState, BackgroundTaskWakePolicy, BACKGROUND_TASK_LABEL_MAX_CHARS,
|
BackgroundTaskResult, BackgroundTaskState, BackgroundTaskWakePolicy,
|
||||||
BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES, BACKGROUND_TASK_TEXT_MAX_BYTES,
|
BACKGROUND_TASK_LABEL_MAX_CHARS, BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES,
|
||||||
|
BACKGROUND_TASK_TEXT_MAX_BYTES,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use skill::{Skill, SkillRef, SkillScope};
|
pub use skill::{Skill, SkillRef, SkillScope};
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
//! Pure invariants for first-class background tasks.
|
//! Pure invariants for first-class background tasks.
|
||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
BackgroundTask, BackgroundTaskError, BackgroundTaskKind, BackgroundTaskResult,
|
BackgroundTask, BackgroundTaskError, BackgroundTaskKind, BackgroundTaskRendezvousLink,
|
||||||
BackgroundTaskState, BackgroundTaskWakePolicy,
|
BackgroundTaskResult, BackgroundTaskState, BackgroundTaskWakePolicy, ConversationId,
|
||||||
|
RendezvousId, TicketId,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -43,6 +44,23 @@ fn success_result(at: u64) -> BackgroundTaskResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendezvous_link_roundtrips_on_background_task() {
|
||||||
|
let link = BackgroundTaskRendezvousLink {
|
||||||
|
rendezvous_id: RendezvousId::from_uuid(Uuid::from_u128(9)),
|
||||||
|
ticket_id: TicketId::from_uuid(Uuid::from_u128(10)),
|
||||||
|
requester_agent_id: Some(agent_id(4)),
|
||||||
|
target_agent_id: agent_id(3),
|
||||||
|
conversation_id: ConversationId::from_uuid(Uuid::from_u128(11)),
|
||||||
|
};
|
||||||
|
let task = command_task().with_rendezvous(link.clone());
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&task).unwrap();
|
||||||
|
let decoded: BackgroundTask = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoded.rendezvous, Some(link));
|
||||||
|
}
|
||||||
|
|
||||||
fn failure_result(at: u64) -> BackgroundTaskResult {
|
fn failure_result(at: u64) -> BackgroundTaskResult {
|
||||||
BackgroundTaskResult::Failure {
|
BackgroundTaskResult::Failure {
|
||||||
finished_at_ms: at,
|
finished_at_ms: at,
|
||||||
|
|||||||
Reference in New Issue
Block a user