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

@ -13,7 +13,7 @@ use std::sync::Mutex;
use async_trait::async_trait;
use application::{drain_with_readiness, drain_with_readiness_outcome, send_blocking, TurnOutcome};
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};
@ -24,6 +24,10 @@ fn aid(n: u128) -> AgentId {
AgentId::from_uuid(Uuid::from_u128(n))
}
fn key(n: u128) -> RuntimeAgentKey {
RuntimeAgentKey::new(ProjectId::from_uuid(Uuid::nil()), aid(n))
}
// ---------------------------------------------------------------------------
// Fake AgentSession : `send` rejoue une liste fixe d'événements.
// ---------------------------------------------------------------------------
@ -57,17 +61,17 @@ struct RecordingMediator {
calls: Mutex<Vec<&'static str>>,
}
impl InputMediator for RecordingMediator {
fn enqueue(&self, _agent: AgentId, _ticket: Ticket) -> PendingReply {
fn enqueue(&self, _agent: RuntimeAgentKey, _ticket: Ticket) -> PendingReply {
PendingReply::new(Box::pin(std::future::pending()))
}
fn preempt(&self, _agent: AgentId) {}
fn mark_idle(&self, _agent: AgentId) {
fn preempt(&self, _agent: RuntimeAgentKey) {}
fn mark_idle(&self, _agent: RuntimeAgentKey) {
self.calls.lock().unwrap().push("idle");
}
fn mark_alive(&self, _agent: AgentId) {
fn mark_alive(&self, _agent: RuntimeAgentKey) {
self.calls.lock().unwrap().push("alive");
}
fn busy_state(&self, _agent: AgentId) -> AgentBusyState {
fn busy_state(&self, _agent: RuntimeAgentKey) -> AgentBusyState {
AgentBusyState::Idle
}
}
@ -86,7 +90,7 @@ async fn outcome_rate_limited_some_without_final_is_graceful() {
}],
};
let mediator = RecordingMediator::default();
let out = drain_with_readiness_outcome(&session, "go", None, &mediator, aid(1))
let out = drain_with_readiness_outcome(&session, "go", None, &mediator, key(1))
.await
.expect("un tour limité est une fin gracieuse, pas une erreur");
assert_eq!(
@ -109,7 +113,7 @@ async fn outcome_rate_limited_none_without_final_is_graceful() {
events: vec![ReplyEvent::RateLimited { resets_at_ms: None }],
};
let mediator = RecordingMediator::default();
let out = drain_with_readiness_outcome(&session, "go", None, &mediator, aid(1))
let out = drain_with_readiness_outcome(&session, "go", None, &mediator, key(1))
.await
.expect("fin gracieuse");
assert_eq!(out, TurnOutcome::RateLimited { resets_at_ms: None });
@ -131,7 +135,7 @@ async fn outcome_rate_limited_then_final_is_completed() {
],
};
let mediator = RecordingMediator::default();
let out = drain_with_readiness_outcome(&session, "go", None, &mediator, aid(1))
let out = drain_with_readiness_outcome(&session, "go", None, &mediator, key(1))
.await
.expect("ok");
assert_eq!(out, TurnOutcome::Completed("fini".to_owned()));
@ -147,7 +151,7 @@ async fn outcome_truncated_stream_without_final_or_ratelimit_is_io_error() {
events: vec![ReplyEvent::TextDelta { text: "a".into() }],
};
let mediator = RecordingMediator::default();
let err = drain_with_readiness_outcome(&session, "go", None, &mediator, aid(1))
let err = drain_with_readiness_outcome(&session, "go", None, &mediator, key(1))
.await
.expect_err("flux tronqué sans limite ⇒ erreur");
assert!(matches!(err, AgentSessionError::Io(_)), "vu: {err:?}");
@ -167,7 +171,7 @@ async fn drain_with_readiness_rate_limited_is_io_error() {
}],
};
let mediator = RecordingMediator::default();
let err = drain_with_readiness(&session, "go", None, &mediator, aid(1))
let err = drain_with_readiness(&session, "go", None, &mediator, key(1))
.await
.expect_err("limite ⇒ Io sur la signature historique");
assert!(matches!(err, AgentSessionError::Io(_)), "vu: {err:?}");
@ -197,7 +201,7 @@ async fn drain_with_readiness_nominal_still_completes() {
],
};
let mediator = RecordingMediator::default();
let content = drain_with_readiness(&session, "go", None, &mediator, aid(1))
let content = drain_with_readiness(&session, "go", None, &mediator, key(1))
.await
.expect("ok");
assert_eq!(content, "fini");