fix(runtime): isolate agent state by project (#101)
This commit is contained in:
@ -29,7 +29,7 @@ use domain::project::ProjectPath;
|
||||
use domain::{
|
||||
AgentId, AgentProfile, BackgroundTask, BackgroundTaskKind, BackgroundTaskResult,
|
||||
BackgroundTaskState, BackgroundTaskWakePolicy, DomainEvent, OrchestratorCommand,
|
||||
OrchestratorVisibility, ProfileId, Project, TaskId,
|
||||
OrchestratorVisibility, ProfileId, Project, RuntimeAgentKey, TaskId,
|
||||
};
|
||||
|
||||
use crate::conversation::RecordTurn;
|
||||
@ -59,6 +59,10 @@ const DEFAULT_ROWS: u16 = 24;
|
||||
/// See [`DEFAULT_ROWS`].
|
||||
const DEFAULT_COLS: u16 = 80;
|
||||
|
||||
fn runtime_key(project: &Project, agent_id: AgentId) -> RuntimeAgentKey {
|
||||
RuntimeAgentKey::new(project.id, agent_id)
|
||||
}
|
||||
|
||||
/// Submit defaults for delegated prompts, after applying profile-specific
|
||||
/// compatibility fallbacks for existing saved profiles.
|
||||
fn submit_config_for_profile(profile: &AgentProfile) -> SubmitConfig {
|
||||
@ -234,7 +238,7 @@ fn resolve_turn_timeout(turn_timeout_ms: Option<u32>) -> Duration {
|
||||
struct BusyTurnGuard {
|
||||
input: Arc<dyn InputMediator>,
|
||||
mailbox: Arc<dyn domain::mailbox::AgentMailbox>,
|
||||
agent: AgentId,
|
||||
agent: RuntimeAgentKey,
|
||||
ticket: TicketId,
|
||||
armed: bool,
|
||||
}
|
||||
@ -244,7 +248,7 @@ impl BusyTurnGuard {
|
||||
fn new(
|
||||
input: Arc<dyn InputMediator>,
|
||||
mailbox: Arc<dyn domain::mailbox::AgentMailbox>,
|
||||
agent: AgentId,
|
||||
agent: RuntimeAgentKey,
|
||||
ticket: TicketId,
|
||||
) -> Self {
|
||||
Self {
|
||||
@ -279,7 +283,7 @@ impl Drop for BusyTurnGuard {
|
||||
// blocage à diagnostiquer.
|
||||
crate::diag!(
|
||||
"[rendezvous] busy-guard freed target agent {} (ticket {})",
|
||||
self.agent,
|
||||
self.agent.agent_id,
|
||||
self.ticket,
|
||||
);
|
||||
}
|
||||
@ -1116,7 +1120,7 @@ impl OrchestratorService {
|
||||
from,
|
||||
ticket,
|
||||
result,
|
||||
} => self.reply(from, ticket, result),
|
||||
} => self.reply(project, from, ticket, result),
|
||||
OrchestratorCommand::ListAgents => self.list_agents(project).await,
|
||||
OrchestratorCommand::StopAgent { name } => self.stop_agent(project, name).await,
|
||||
OrchestratorCommand::UpdateAgentContext { name, context } => {
|
||||
@ -1501,7 +1505,10 @@ impl OrchestratorService {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(session_id) = self.sessions.session_for_agent(&agent_id) {
|
||||
if let Some(session_id) = self
|
||||
.sessions
|
||||
.session_for_agent_in_project(project.id, &agent_id)
|
||||
{
|
||||
match visibility {
|
||||
OrchestratorVisibility::Background => {
|
||||
return Ok(OrchestratorOutcome {
|
||||
@ -1516,12 +1523,14 @@ impl OrchestratorService {
|
||||
// hôte est légitime (rebind de vue), mais viser un **autre** node
|
||||
// pour un agent singleton déjà vivant est un second lancement ⇒
|
||||
// refus `AgentAlreadyRunning`.
|
||||
let host_node = self.sessions.node_for_agent(&agent_id);
|
||||
let host_node = self
|
||||
.sessions
|
||||
.node_for_agent_in_project(project.id, &agent_id);
|
||||
match ReattachDecision::resolve(Some(node_id), host_node, None) {
|
||||
ReattachDecision::Rebind { node_id } => {
|
||||
let session = self
|
||||
.sessions
|
||||
.rebind_agent_node(&agent_id, node_id)
|
||||
.rebind_agent_node_in_project(project.id, &agent_id, node_id)
|
||||
.ok_or_else(|| {
|
||||
AppError::NotFound(format!(
|
||||
"running session {session_id} for agent {name}"
|
||||
@ -1620,7 +1629,6 @@ impl OrchestratorService {
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("agent {target}")))?;
|
||||
let agent_id = agent.id;
|
||||
|
||||
// Détection de cycle (cadrage C3 §6) : si l'ask vient d'un **agent** A vers la
|
||||
// cible B, refuser AVANT tout enqueue si poser l'arête A→B fermerait un cycle
|
||||
// d'attente (B attend déjà …→A). Pur, sans I/O ⇒ jamais de deadlock.
|
||||
@ -1647,7 +1655,7 @@ impl OrchestratorService {
|
||||
// Résoudre paresseusement le **fil** de l'ask : A↔B si un agent demande, sinon
|
||||
// User↔B. La session vivante est désormais keyée par conversation (lève
|
||||
// `session-registry-agent-ambiguity`).
|
||||
let conversation_id = self.resolve_conversation(requester, agent_id);
|
||||
let conversation_id = self.resolve_conversation(project, requester, agent_id);
|
||||
|
||||
// Sérialisation FIFO **par agent** (A0) : verrou de tour de la **cible**, tenu
|
||||
// pour TOUT le tour (enqueue → réponse). RAII : tombe sur chaque early-return.
|
||||
@ -1751,6 +1759,7 @@ impl OrchestratorService {
|
||||
))
|
||||
}
|
||||
};
|
||||
let agent_key = runtime_key(project, agent_id);
|
||||
|
||||
// Checkpoint Prompt (best-effort), AVANT de déplacer `task` dans le ticket.
|
||||
let prompt_source = match requester {
|
||||
@ -1783,7 +1792,7 @@ impl OrchestratorService {
|
||||
// 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é).
|
||||
let turn_timeout = self.turn_timeout_for(project, agent_id).await;
|
||||
let _pending = input.enqueue_silent(agent_id, ticket);
|
||||
let _pending = input.enqueue_silent(agent_key, ticket);
|
||||
let rendezvous_task = self
|
||||
.start_rendezvous_task(
|
||||
project,
|
||||
@ -1809,7 +1818,7 @@ impl OrchestratorService {
|
||||
// (drop)** — en ramenant la cible `Idle` au Drop (cf. [`BusyTurnGuard`]). C'est
|
||||
// le fix de la cause racine du blocage `Busy` à vie.
|
||||
let busy_guard =
|
||||
BusyTurnGuard::new(Arc::clone(input), Arc::clone(mailbox), agent_id, ticket_id);
|
||||
BusyTurnGuard::new(Arc::clone(input), Arc::clone(mailbox), agent_key, ticket_id);
|
||||
|
||||
// Rendezvous beacon (chemin structuré) : équivalent du « ask started » du chemin
|
||||
// PTY. La cible n'a pas de PTY ; le tour se débloque uniquement sur le `Final`
|
||||
@ -1840,7 +1849,7 @@ impl OrchestratorService {
|
||||
&task,
|
||||
None,
|
||||
input.as_ref(),
|
||||
agent_id,
|
||||
agent_key,
|
||||
announcement_publisher,
|
||||
);
|
||||
|
||||
@ -1865,8 +1874,11 @@ impl OrchestratorService {
|
||||
if let (Some(service), Some(structured)) =
|
||||
(&self.session_limits, &self.structured)
|
||||
{
|
||||
if let Some(node_id) = structured.node_for_agent(&agent_id) {
|
||||
if let Some(node_id) =
|
||||
structured.node_for_agent_in_project(project.id, &agent_id)
|
||||
{
|
||||
service.on_rate_limited(
|
||||
project.id,
|
||||
agent_id,
|
||||
node_id,
|
||||
conversation_id,
|
||||
@ -1982,8 +1994,8 @@ impl OrchestratorService {
|
||||
|
||||
// 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.
|
||||
mailbox.cancel_head(agent_id, ticket_id);
|
||||
input.mark_idle(agent_id);
|
||||
mailbox.cancel_head(agent_key, ticket_id);
|
||||
input.mark_idle(agent_key);
|
||||
busy_guard.disarm();
|
||||
|
||||
// Checkpoint Response (best-effort), AVANT de déplacer `result`.
|
||||
@ -2046,7 +2058,8 @@ impl OrchestratorService {
|
||||
let target = target.as_str();
|
||||
|
||||
// User↔Agent thread (no requester ⇒ left = User). Same lazy resolution as ask.
|
||||
let conversation_id = self.resolve_conversation(None, agent_id);
|
||||
let agent_key = runtime_key(project, agent_id);
|
||||
let conversation_id = self.resolve_conversation(project, None, agent_id);
|
||||
|
||||
// Ensure the target is live for this thread and bind its input handle on the
|
||||
// mediator (delivery path). Same call the ask path uses.
|
||||
@ -2061,15 +2074,15 @@ impl OrchestratorService {
|
||||
// de gate (livraison immédiate, sinon blocage indéfini).
|
||||
let gate_cold_start = cold_launch && has_mcp;
|
||||
if gate_cold_start {
|
||||
input.mark_starting(agent_id);
|
||||
input.mark_starting(agent_key);
|
||||
}
|
||||
input.bind_handle_with_submit(agent_id, handle, submit);
|
||||
input.bind_handle_with_submit(agent_key, handle, submit);
|
||||
|
||||
// Enqueue a human-sourced ticket in the SAME FIFO as delegations. Fire-and-
|
||||
// forget: we drop the PendingReply (the human reads the terminal). The
|
||||
// mediator emits AgentBusyChanged at the source on a starting turn.
|
||||
let ticket = Ticket::from_human(TicketId::new_random(), conversation_id, "vous", text);
|
||||
let _pending = input.enqueue(agent_id, ticket);
|
||||
let _pending = input.enqueue(agent_key, ticket);
|
||||
|
||||
Ok(OrchestratorOutcome {
|
||||
detail: format!("submitted human input to agent {target}"),
|
||||
@ -2110,7 +2123,7 @@ impl OrchestratorService {
|
||||
return Err(AppError::NotFound(format!("agent {agent_id}")));
|
||||
}
|
||||
|
||||
input.preempt(agent_id);
|
||||
input.preempt(runtime_key(project, agent_id));
|
||||
|
||||
Ok(OrchestratorOutcome {
|
||||
detail: format!("interrupted agent {agent_id}"),
|
||||
@ -2147,9 +2160,9 @@ impl OrchestratorService {
|
||||
/// Libère le premier tour différé d'un agent **lancé à froid** quand son pont MCP se
|
||||
/// connecte (readiness de démarrage). Pont entre l'McpServer (adapter entrant) et le
|
||||
/// médiateur d'entrée. No-op si aucun médiateur n'est câblé ou si rien n'est différé.
|
||||
pub fn release_agent_cold_start(&self, agent: domain::AgentId) {
|
||||
pub fn release_agent_cold_start(&self, project: &Project, agent: domain::AgentId) {
|
||||
if let Some(input) = &self.input {
|
||||
input.release_cold_start(agent);
|
||||
input.release_cold_start(runtime_key(project, agent));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2174,10 +2187,15 @@ impl OrchestratorService {
|
||||
/// cellule reçoit ses tours via l'événement `DelegationReady` (le front écrit) ; un
|
||||
/// agent **headless** (délégué en arrière-plan, sans cellule) voit le médiateur écrire
|
||||
/// lui-même la tâche dans son PTY — sinon le tour est perdu. No-op sans médiateur.
|
||||
pub fn set_agent_front_attached(&self, agent: domain::AgentId, attached: bool) {
|
||||
pub fn set_agent_front_attached(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent: domain::AgentId,
|
||||
attached: bool,
|
||||
) {
|
||||
crate::diag!("[delivery] front attachment changed: agent={agent} attached={attached}");
|
||||
if let Some(input) = &self.input {
|
||||
input.set_front_attached(agent, attached);
|
||||
input.set_front_attached(runtime_key(project, agent), attached);
|
||||
} else {
|
||||
crate::diag!(
|
||||
"[delivery] front attachment ignored because input mediator is not wired: \
|
||||
@ -2191,6 +2209,7 @@ impl OrchestratorService {
|
||||
/// stable per-agent id derived from the target (legacy routing — never panics).
|
||||
fn resolve_conversation(
|
||||
&self,
|
||||
project: &Project,
|
||||
requester: Option<AgentId>,
|
||||
target: AgentId,
|
||||
) -> domain::conversation::ConversationId {
|
||||
@ -2200,11 +2219,11 @@ impl OrchestratorService {
|
||||
};
|
||||
let right = ConversationParty::agent(target);
|
||||
match &self.conversations {
|
||||
Some(reg) => reg.resolve(left, right).id,
|
||||
Some(reg) => reg.resolve(project.id, left, right).id,
|
||||
// Repli pur déterministe partagé avec `LaunchAgent` (ARCHITECTURE §19.7,
|
||||
// lot P8a) : la même paire dérive la même clé de conversation des deux
|
||||
// côtés (sauvegarde du handoff ici, dérivation côté cellule là-bas).
|
||||
None => domain::conversation::ConversationId::for_pair(left, right),
|
||||
None => domain::conversation::ConversationId::for_project_pair(project.id, left, right),
|
||||
}
|
||||
}
|
||||
|
||||
@ -2223,6 +2242,7 @@ impl OrchestratorService {
|
||||
/// matching ask) — typed, never a panic.
|
||||
fn reply(
|
||||
&self,
|
||||
project: &Project,
|
||||
from: AgentId,
|
||||
ticket: Option<TicketId>,
|
||||
result: String,
|
||||
@ -2243,11 +2263,12 @@ impl OrchestratorService {
|
||||
"idea_reply n'est pas disponible : file inter-agents non câblée".to_owned(),
|
||||
)
|
||||
})?;
|
||||
let from_key = runtime_key(project, from);
|
||||
// Corrélation par ticket quand l'agent l'a renvoyé (déterministe, multi-fil) ;
|
||||
// sinon repli sur la tête de file de l'émetteur (compat agents mono-fil).
|
||||
let correlation = match ticket {
|
||||
Some(ticket_id) => mailbox.resolve_ticket(from, ticket_id, result),
|
||||
None => mailbox.resolve(from, result),
|
||||
Some(ticket_id) => mailbox.resolve_ticket(from_key, ticket_id, result),
|
||||
None => mailbox.resolve(from_key, result),
|
||||
};
|
||||
// Rendezvous beacon (diagnostics) : un `idea_reply` est arrivé. Tracer s'il a
|
||||
// corrélé à un ask en vol — un échec ici (« no matching ask ») signe une
|
||||
@ -2269,7 +2290,7 @@ impl OrchestratorService {
|
||||
// pairs with prompt-ready detection; whichever fires first frees the turn. No-op
|
||||
// (and no spurious event) when the mediator is absent or `from` was already idle.
|
||||
if let Some(input) = self.input.as_ref() {
|
||||
input.mark_idle(from);
|
||||
input.mark_idle(from_key);
|
||||
}
|
||||
Ok(OrchestratorOutcome {
|
||||
detail: format!("reply from agent {from} delivered"),
|
||||
@ -2300,10 +2321,10 @@ impl OrchestratorService {
|
||||
// «1 session vivante / conversation» (cadrage C3 §5.2) : on cherche d'abord la
|
||||
// session du **fil**, puis on retombe sur la session de l'agent (compat : un
|
||||
// agent mono-fil dont la session n'a pas encore été liée à sa conversation).
|
||||
let existing = self
|
||||
.sessions
|
||||
.session_for(conversation_id)
|
||||
.or_else(|| self.sessions.session_for_agent(&agent_id));
|
||||
let existing = self.sessions.session_for(conversation_id).or_else(|| {
|
||||
self.sessions
|
||||
.session_for_agent_in_project(project.id, &agent_id)
|
||||
});
|
||||
if let Some(session_id) = existing {
|
||||
if let Some(handle) = self.sessions.handle(&session_id) {
|
||||
// (Re)lier le fil à cette session vivante (idempotent). Réutilisation
|
||||
@ -2334,11 +2355,14 @@ impl OrchestratorService {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let session_id = self.sessions.session_for_agent(&agent_id).ok_or_else(|| {
|
||||
AppError::Process(format!(
|
||||
"agent {target} n'a pas de session terminal vivante après lancement"
|
||||
))
|
||||
})?;
|
||||
let session_id = self
|
||||
.sessions
|
||||
.session_for_agent_in_project(project.id, &agent_id)
|
||||
.ok_or_else(|| {
|
||||
AppError::Process(format!(
|
||||
"agent {target} n'a pas de session terminal vivante après lancement"
|
||||
))
|
||||
})?;
|
||||
// Lier la session fraîchement lancée à CE fil (registre terminal + registre de
|
||||
// conversations) ⇒ un prochain ask sur le même fil la réutilise.
|
||||
self.bind_conversation_session(conversation_id, session_id);
|
||||
@ -2373,7 +2397,7 @@ impl OrchestratorService {
|
||||
structured: &Arc<StructuredSessions>,
|
||||
) -> Result<Option<Arc<dyn domain::ports::AgentSession>>, AppError> {
|
||||
// Cible déjà chaude : route directe (aucun lancement).
|
||||
if let Some(session) = structured.session_for_agent(&agent_id) {
|
||||
if let Some(session) = structured.session_for_agent_in_project(project.id, &agent_id) {
|
||||
return Ok(Some(session));
|
||||
}
|
||||
|
||||
@ -2413,7 +2437,7 @@ impl OrchestratorService {
|
||||
|
||||
// Le launcher a inséré la session dans le registre partagé : la relire.
|
||||
structured
|
||||
.session_for_agent(&agent_id)
|
||||
.session_for_agent_in_project(project.id, &agent_id)
|
||||
.map(Some)
|
||||
.ok_or_else(|| {
|
||||
AppError::Process(format!(
|
||||
@ -2528,7 +2552,7 @@ impl OrchestratorService {
|
||||
|
||||
let session_id = self
|
||||
.sessions
|
||||
.session_for_agent(&agent_id)
|
||||
.session_for_agent_in_project(project.id, &agent_id)
|
||||
.ok_or_else(|| AppError::NotFound(format!("running session for agent {name}")))?;
|
||||
|
||||
self.close_terminal
|
||||
@ -2719,7 +2743,7 @@ impl OrchestratorService {
|
||||
async fn turn_timeout_for(&self, project: &Project, agent_id: AgentId) -> Duration {
|
||||
let (stall_after_ms, turn_timeout_ms) = self.liveness_for_agent(project, agent_id).await;
|
||||
if let Some(input) = &self.input {
|
||||
input.set_stall_threshold(agent_id, stall_after_ms);
|
||||
input.set_stall_threshold(runtime_key(project, agent_id), stall_after_ms);
|
||||
}
|
||||
resolve_turn_timeout(turn_timeout_ms)
|
||||
}
|
||||
@ -2982,18 +3006,18 @@ mod tests {
|
||||
impl domain::input::InputMediator for SpyMediator {
|
||||
fn enqueue(
|
||||
&self,
|
||||
_agent: AgentId,
|
||||
_agent: RuntimeAgentKey,
|
||||
_ticket: domain::mailbox::Ticket,
|
||||
) -> domain::mailbox::PendingReply {
|
||||
domain::mailbox::PendingReply::new(Box::pin(async {
|
||||
Err(domain::mailbox::MailboxError::Cancelled)
|
||||
}))
|
||||
}
|
||||
fn preempt(&self, _agent: AgentId) {}
|
||||
fn mark_idle(&self, agent: AgentId) {
|
||||
self.idled.lock().unwrap().push(agent);
|
||||
fn preempt(&self, _agent: RuntimeAgentKey) {}
|
||||
fn mark_idle(&self, agent: RuntimeAgentKey) {
|
||||
self.idled.lock().unwrap().push(agent.agent_id);
|
||||
}
|
||||
fn busy_state(&self, _agent: AgentId) -> domain::input::AgentBusyState {
|
||||
fn busy_state(&self, _agent: RuntimeAgentKey) -> domain::input::AgentBusyState {
|
||||
domain::input::AgentBusyState::Idle
|
||||
}
|
||||
}
|
||||
@ -3005,7 +3029,7 @@ mod tests {
|
||||
impl domain::mailbox::AgentMailbox for SpyMailbox {
|
||||
fn enqueue(
|
||||
&self,
|
||||
_agent: AgentId,
|
||||
_agent: RuntimeAgentKey,
|
||||
_ticket: domain::mailbox::Ticket,
|
||||
) -> domain::mailbox::PendingReply {
|
||||
domain::mailbox::PendingReply::new(Box::pin(async {
|
||||
@ -3014,13 +3038,16 @@ mod tests {
|
||||
}
|
||||
fn resolve(
|
||||
&self,
|
||||
_agent: AgentId,
|
||||
_agent: RuntimeAgentKey,
|
||||
_result: String,
|
||||
) -> Result<(), domain::mailbox::MailboxError> {
|
||||
Ok(())
|
||||
}
|
||||
fn cancel_head(&self, agent: AgentId, ticket_id: TicketId) {
|
||||
self.cancelled.lock().unwrap().push((agent, ticket_id));
|
||||
fn cancel_head(&self, agent: RuntimeAgentKey, ticket_id: TicketId) {
|
||||
self.cancelled
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((agent.agent_id, ticket_id));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3030,6 +3057,9 @@ mod tests {
|
||||
fn tid(n: u128) -> TicketId {
|
||||
TicketId::from_uuid(uuid::Uuid::from_u128(n))
|
||||
}
|
||||
fn rkey(n: u128) -> RuntimeAgentKey {
|
||||
RuntimeAgentKey::new(domain::ProjectId::from_uuid(uuid::Uuid::nil()), aid(n))
|
||||
}
|
||||
|
||||
/// Drop d'un garde **armé** ⇒ `cancel_head` + `mark_idle` sur la cible (c'est le
|
||||
/// comportement qui débloque un agent resté `Busy` sur un futur abandonné).
|
||||
@ -3041,7 +3071,7 @@ mod tests {
|
||||
let _g = BusyTurnGuard::new(
|
||||
Arc::clone(&med) as Arc<dyn domain::input::InputMediator>,
|
||||
Arc::clone(&mb) as Arc<dyn domain::mailbox::AgentMailbox>,
|
||||
aid(1),
|
||||
rkey(1),
|
||||
tid(7),
|
||||
);
|
||||
} // Drop ici.
|
||||
@ -3066,7 +3096,7 @@ mod tests {
|
||||
let g = BusyTurnGuard::new(
|
||||
Arc::clone(&med) as Arc<dyn domain::input::InputMediator>,
|
||||
Arc::clone(&mb) as Arc<dyn domain::mailbox::AgentMailbox>,
|
||||
aid(1),
|
||||
rkey(1),
|
||||
tid(7),
|
||||
);
|
||||
g.disarm();
|
||||
|
||||
@ -10,7 +10,7 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use domain::background_task::{BackgroundTask, BackgroundTaskResult};
|
||||
use domain::events::DomainEvent;
|
||||
use domain::ids::AgentId;
|
||||
use domain::ids::{AgentId, RuntimeAgentKey};
|
||||
use domain::inbox::{AgentInbox, InboxItem, InboxItemKind, InboxSource};
|
||||
use domain::input::InputMediator;
|
||||
use domain::mailbox::{AgentMailbox, Ticket};
|
||||
@ -90,25 +90,26 @@ impl AgentWakeService {
|
||||
agent: AgentId,
|
||||
reason: WakeReason,
|
||||
) -> Result<(), WakeError> {
|
||||
let key = RuntimeAgentKey::new(project.id, agent);
|
||||
self.publish(DomainEvent::AgentWakeScheduled {
|
||||
project_id: project.id,
|
||||
agent_id: agent,
|
||||
});
|
||||
|
||||
if self.input.busy_state(agent).is_busy() {
|
||||
if self.input.busy_state(key).is_busy() {
|
||||
return Err(WakeError::AgentBusy { agent_id: agent });
|
||||
}
|
||||
|
||||
let Some(item) = self.inbox.dequeue_next(agent) else {
|
||||
let Some(item) = self.inbox.dequeue_next(key) else {
|
||||
return Ok(());
|
||||
};
|
||||
let delivery = self.delivery_from_item(item, &reason).await?;
|
||||
let ticket = Ticket::new(delivery.ticket_id, "IdeA", delivery.prompt.clone());
|
||||
let _pending = self.input.enqueue_silent(agent, ticket);
|
||||
let _pending = self.input.enqueue_silent(key, ticket);
|
||||
let guard = WakeTurnGuard::new(
|
||||
Arc::clone(&self.input),
|
||||
Arc::clone(&self.mailbox),
|
||||
agent,
|
||||
key,
|
||||
delivery.ticket_id,
|
||||
);
|
||||
|
||||
@ -133,12 +134,12 @@ impl AgentWakeService {
|
||||
owner_agent_id: agent,
|
||||
});
|
||||
}
|
||||
drain_reply_stream_with_readiness(stream, self.input.as_ref(), agent)
|
||||
drain_reply_stream_with_readiness(stream, self.input.as_ref(), key)
|
||||
.await
|
||||
.map_err(|err| WakeError::Session(err.to_string()))?;
|
||||
|
||||
self.mailbox.cancel_head(agent, delivery.ticket_id);
|
||||
self.input.mark_idle(agent);
|
||||
self.mailbox.cancel_head(key, delivery.ticket_id);
|
||||
self.input.mark_idle(key);
|
||||
guard.disarm();
|
||||
Ok(())
|
||||
}
|
||||
@ -220,7 +221,7 @@ struct WakeDelivery {
|
||||
struct WakeTurnGuard {
|
||||
input: Arc<dyn InputMediator>,
|
||||
mailbox: Arc<dyn AgentMailbox>,
|
||||
agent: AgentId,
|
||||
agent: RuntimeAgentKey,
|
||||
ticket: domain::mailbox::TicketId,
|
||||
armed: bool,
|
||||
}
|
||||
@ -229,7 +230,7 @@ impl WakeTurnGuard {
|
||||
fn new(
|
||||
input: Arc<dyn InputMediator>,
|
||||
mailbox: Arc<dyn AgentMailbox>,
|
||||
agent: AgentId,
|
||||
agent: RuntimeAgentKey,
|
||||
ticket: domain::mailbox::TicketId,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
||||
Reference in New Issue
Block a user