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

@ -23,7 +23,7 @@ use std::time::Duration;
use async_trait::async_trait;
use application::drain_with_readiness;
use domain::ids::AgentId;
use domain::ids::{AgentId, ProjectId, RuntimeAgentKey};
use domain::input::{AgentBusyState, InputMediator};
use domain::mailbox::{PendingReply, Ticket};
use domain::ports::{AgentSession, AgentSessionError, ReplyEvent, ReplyStream};
@ -38,6 +38,10 @@ fn aid(n: u128) -> AgentId {
AgentId::from_uuid(Uuid::from_u128(n))
}
fn key(agent: AgentId) -> RuntimeAgentKey {
RuntimeAgentKey::new(ProjectId::from_uuid(Uuid::nil()), agent)
}
// ---------------------------------------------------------------------------
// Fake AgentSession scriptable (mono-usage), repris de send_blocking_d1.rs
// ---------------------------------------------------------------------------
@ -121,17 +125,20 @@ impl RecordingMediator {
}
impl InputMediator for RecordingMediator {
fn enqueue(&self, _agent: AgentId, _ticket: Ticket) -> PendingReply {
fn enqueue(&self, _agent: RuntimeAgentKey, _ticket: Ticket) -> PendingReply {
// Jamais utilisé par drain_with_readiness ; un future qui ne résout pas.
PendingReply::new(Box::pin(std::future::pending()))
}
fn preempt(&self, agent: AgentId) {
self.calls.lock().unwrap().push((agent, "preempt"));
fn preempt(&self, agent: RuntimeAgentKey) {
self.calls.lock().unwrap().push((agent.agent_id, "preempt"));
}
fn mark_idle(&self, agent: AgentId) {
self.calls.lock().unwrap().push((agent, "mark_idle"));
fn mark_idle(&self, agent: RuntimeAgentKey) {
self.calls
.lock()
.unwrap()
.push((agent.agent_id, "mark_idle"));
}
fn busy_state(&self, _agent: AgentId) -> AgentBusyState {
fn busy_state(&self, _agent: RuntimeAgentKey) -> AgentBusyState {
AgentBusyState::Idle
}
}
@ -169,7 +176,7 @@ async fn final_only_stream_unblocks_queue_via_mark_idle() {
let session = ScriptedSession::new(Script::Stream(vec![final_("done")]));
let mediator = RecordingMediator::new();
let out = drain_with_readiness(&session, "tâche", None, &mediator, agent).await;
let out = drain_with_readiness(&session, "tâche", None, &mediator, key(agent)).await;
assert_eq!(out, Ok("done".to_owned()), "le Final rend bien son contenu");
assert_eq!(
@ -200,7 +207,7 @@ async fn intermediate_events_do_not_mark_idle_only_final_does() {
]));
let mediator = RecordingMediator::new();
let out = drain_with_readiness(&session, "x", None, &mediator, agent).await;
let out = drain_with_readiness(&session, "x", None, &mediator, key(agent)).await;
assert_eq!(out, Ok("hello".to_owned()));
assert_eq!(
@ -227,7 +234,7 @@ async fn heartbeats_alone_never_mark_idle_before_final() {
]));
let mediator = RecordingMediator::new();
let out = drain_with_readiness(&session, "x", None, &mediator, agent).await;
let out = drain_with_readiness(&session, "x", None, &mediator, key(agent)).await;
assert_eq!(out, Ok("fini".to_owned()));
assert_eq!(mediator.mark_idle_count(agent), 1);
}
@ -242,7 +249,7 @@ async fn stream_without_final_does_not_mark_idle_and_is_io_error() {
let session = ScriptedSession::new(Script::Stream(vec![heartbeat(), delta("a"), tool("b")]));
let mediator = RecordingMediator::new();
let out = drain_with_readiness(&session, "x", None, &mediator, agent).await;
let out = drain_with_readiness(&session, "x", None, &mediator, key(agent)).await;
assert!(
matches!(out, Err(AgentSessionError::Io(_))),
"flux épuisé sans Final ⇒ Io, obtenu {out:?}"
@ -263,7 +270,7 @@ async fn send_error_is_propagated_and_no_mark_idle() {
)));
let mediator = RecordingMediator::new();
let out = drain_with_readiness(&session, "x", None, &mediator, agent).await;
let out = drain_with_readiness(&session, "x", None, &mediator, key(agent)).await;
assert_eq!(out, Err(AgentSessionError::Decode("bad json".to_owned())));
assert_eq!(mediator.mark_idle_count(agent), 0);
}
@ -276,7 +283,7 @@ async fn mark_idle_targets_the_drained_agent_only() {
let session = ScriptedSession::new(Script::Stream(vec![final_("ok")]));
let mediator = RecordingMediator::new();
let _ = drain_with_readiness(&session, "x", None, &mediator, drained).await;
let _ = drain_with_readiness(&session, "x", None, &mediator, key(drained)).await;
assert_eq!(mediator.mark_idle_count(drained), 1);
assert_eq!(
mediator.mark_idle_count(other),
@ -328,7 +335,7 @@ async fn timeout_returns_timeout_no_mark_idle_session_alive() {
"x",
Some(Duration::from_millis(20)),
&mediator,
agent,
key(agent),
)
.await;
assert_eq!(out, Err(AgentSessionError::Timeout));