fix(runtime): isolate agent state by project (#101)

This commit is contained in:
2026-07-25 22:14:02 +02:00
parent 6a87c4635f
commit 6e98fd89f7
52 changed files with 1894 additions and 805 deletions

View File

@ -14,7 +14,7 @@
use serde::{Deserialize, Serialize};
use crate::ids::AgentId;
use crate::ids::{AgentId, RuntimeAgentKey};
use crate::mailbox::{PendingReply, Ticket, TicketId};
use crate::ports::PtyHandle;
@ -162,7 +162,7 @@ pub trait InputMediator: Send + Sync {
/// cell (sole owner of the terminal) runs the write-portal handshake and writes the
/// text + submit sequence through the single PTY writer. The mediator stays the
/// authority of the FIFO/busy state and correlation only.
fn enqueue(&self, agent: AgentId, ticket: Ticket) -> PendingReply;
fn enqueue(&self, agent: RuntimeAgentKey, ticket: Ticket) -> PendingReply;
/// Headless/system enqueue: appends `ticket` to the same FIFO and marks the agent
/// busy, but does **not** deliver any text to the human terminal surface.
@ -175,7 +175,7 @@ pub trait InputMediator: Send + Sync {
///
/// Default keeps compatibility for simple mediators; production overrides it to
/// suppress delivery.
fn enqueue_silent(&self, agent: AgentId, ticket: Ticket) -> PendingReply {
fn enqueue_silent(&self, agent: RuntimeAgentKey, ticket: Ticket) -> PendingReply {
self.enqueue(agent, ticket)
}
@ -186,7 +186,7 @@ pub trait InputMediator: Send + Sync {
/// the orchestrator calls this once it has resolved/launched the agent's live
/// session for the target conversation. Default: no-op (a mediator that does not
/// own the delivery write).
fn bind_handle(&self, _agent: AgentId, _handle: PtyHandle) {}
fn bind_handle(&self, _agent: RuntimeAgentKey, _handle: PtyHandle) {}
/// Like [`InputMediator::bind_handle`], but also records the target's
/// [`SubmitConfig`] (ARCHITECTURE §20.3) so the adapter can echo
@ -203,7 +203,12 @@ pub trait InputMediator: Send + Sync {
///
/// Default: delegates to [`InputMediator::bind_handle`], ignoring the submit config.
/// The infra adapter overrides it to stash the submit config.
fn bind_handle_with_submit(&self, agent: AgentId, handle: PtyHandle, _submit: SubmitConfig) {
fn bind_handle_with_submit(
&self,
agent: RuntimeAgentKey,
handle: PtyHandle,
_submit: SubmitConfig,
) {
self.bind_handle(agent, handle);
}
@ -212,7 +217,7 @@ pub trait InputMediator: Send + Sync {
/// for source compatibility; it now always returns `false`. Callers should stop
/// branching on it (the orchestrator no longer falls back to its own PTY write).
#[must_use]
fn delivers_turn(&self, _agent: AgentId) -> bool {
fn delivers_turn(&self, _agent: RuntimeAgentKey) -> bool {
false
}
@ -228,7 +233,7 @@ pub trait InputMediator: Send + Sync {
/// (chemin chaud, fallback sûr, zéro régression).
///
/// Default: no-op (a mediator that does not gate cold starts).
fn mark_starting(&self, _agent: AgentId) {}
fn mark_starting(&self, _agent: RuntimeAgentKey) {}
/// Signal de **readiness de démarrage** : le pont MCP de `agent` vient de se
/// connecter (son CLI est up et a chargé les outils `idea_*`). Si un premier tour
@ -240,7 +245,7 @@ pub trait InputMediator: Send + Sync {
/// watcher prompt-ready PTY ayant été supprimé.
///
/// Default: no-op (médiateur qui ne gate pas les démarrages à froid).
fn release_cold_start(&self, _agent: AgentId) {}
fn release_cold_start(&self, _agent: RuntimeAgentKey) {}
/// Déclare si une **cellule terminal du frontend** est montée pour `agent`
/// (`true` au montage du write-portal, `false` au démontage). C'est le frontend
@ -253,14 +258,14 @@ pub trait InputMediator: Send + Sync {
///
/// Default: no-op (médiateur sans notion de cellule front — tout passe par
/// l'événement, comportement historique).
fn set_front_attached(&self, _agent: AgentId, _attached: bool) {}
fn set_front_attached(&self, _agent: RuntimeAgentKey, _attached: bool) {}
/// Interrompre = preempt: signals the running turn to stop (Échap/stop). This
/// is **not** an enqueue and correlates **no** ticket.
fn preempt(&self, agent: AgentId);
fn preempt(&self, agent: RuntimeAgentKey);
/// Marks `agent` free (explicit signal) so its FIFO advances.
fn mark_idle(&self, agent: AgentId);
fn mark_idle(&self, agent: RuntimeAgentKey);
/// **End-of-turn** signal: the agent's transcript just recorded a completed turn
/// (the [`crate::ports::TurnWatcher`] fired), and **no** `idea_reply` carried a
@ -273,7 +278,7 @@ pub trait InputMediator: Send + Sync {
/// Replaces the former prompt-ready watcher branch verbatim; only the **trigger**
/// changed (transcript `turn_duration` instead of a PTY prompt sigil). Default:
/// [`InputMediator::mark_idle`] (a mediator with no mailbox/grace just advances).
fn turn_ended(&self, agent: AgentId) {
fn turn_ended(&self, agent: RuntimeAgentKey) {
self.mark_idle(agent);
}
@ -287,7 +292,7 @@ pub trait InputMediator: Send + Sync {
/// Default: no-op (a mediator that does not track liveness). The infra adapter
/// `MediatedInbox` overrides it to refresh `last_seen` and publish an
/// `AgentLivenessChanged{Stalled→Alive}` recovery on the first late battement.
fn mark_alive(&self, _agent: AgentId) {}
fn mark_alive(&self, _agent: RuntimeAgentKey) {}
/// Declares the agent's **stall threshold** (its profile's
/// [`crate::profile::LivenessStrategy::stall_after_ms`]) so the stall detector knows
@ -298,10 +303,10 @@ pub trait InputMediator: Send + Sync {
/// zero regression).
///
/// Default: no-op (a mediator that does not track liveness).
fn set_stall_threshold(&self, _agent: AgentId, _stall_after_ms: Option<u32>) {}
fn set_stall_threshold(&self, _agent: RuntimeAgentKey, _stall_after_ms: Option<u32>) {}
/// The current [`AgentBusyState`] of `agent`.
fn busy_state(&self, agent: AgentId) -> AgentBusyState;
fn busy_state(&self, agent: RuntimeAgentKey) -> AgentBusyState;
}
#[cfg(test)]