feat(agent): adapters structurés Claude/Codex + fake CLI + conformité (D2) — §17
infrastructure/src/session/ : machinerie de process générique (paramétrable par la commande = seam d'injection du fake CLI), adapters ClaudeSdkSession/ CodexExecSession avec parsing ISOLÉ par adapter (parse_event), factory StructuredSessionFactory (routage par structured_adapter), FakeCli scriptable + harnais de conformité Liskov assert_agent_session_contract. Incarnation « un run par tour » (send relance claude -p / --resume <id>, continuité via conversation_id — colle au pivot reprise B). Tests : 41 contre le FAKE CLI (jamais le vrai claude/codex), workspace vert. Points en attente des spikes S1/S2 (format réel) — n'impactent que parse_event : - mapping JSON→ReplyEvent Claude (S1) et Codex (S2) sur schémas SUPPOSÉS ; - Claude multi-blocs : parse_event ne garde que le 1er bloc (à corriger si Claude émet plusieurs blocs/message — confirmer S1) ; - flux sans Final : permissif côté adapter, l'erreur est gérée par send_blocking (consommateur). À reconfirmer côté UI streaming (D4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
86
crates/infrastructure/src/session/factory.rs
Normal file
86
crates/infrastructure/src/session/factory.rs
Normal file
@ -0,0 +1,86 @@
|
||||
//! [`StructuredSessionFactory`] — la fabrique [`AgentSessionFactory`] qui **route un
|
||||
//! profil vers le bon adapter** structuré (ARCHITECTURE §17.2) selon
|
||||
//! `profile.structured_adapter` (§17.3). Agrège Claude + Codex derrière une seule
|
||||
//! surface ; aucun type concret ne franchit la frontière domaine (seuls
|
||||
//! `Arc<dyn AgentSession>` sortent).
|
||||
//!
|
||||
//! Open/Closed : ajouter un moteur structuré = ajouter un adapter + une variante
|
||||
//! [`StructuredAdapter`] + un bras de `match` ici. Le cœur ne bouge pas.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use domain::ports::{
|
||||
AgentSession, AgentSessionError, AgentSessionFactory, PreparedContext, SessionPlan,
|
||||
};
|
||||
use domain::profile::{AgentProfile, StructuredAdapter};
|
||||
use domain::project::ProjectPath;
|
||||
use domain::SessionId;
|
||||
|
||||
use super::claude::ClaudeSdkSession;
|
||||
use super::codex::CodexExecSession;
|
||||
|
||||
/// Fabrique infra des sessions structurées, sélectionnée par le profil.
|
||||
///
|
||||
/// Sans état : elle instancie l'adapter au vol depuis le profil (le binaire à
|
||||
/// lancer = `profile.command`), de sorte qu'un seul exemplaire injecté au
|
||||
/// composition root sert tous les agents (jumeau de `CliAgentRuntime`).
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct StructuredSessionFactory;
|
||||
|
||||
impl StructuredSessionFactory {
|
||||
/// Construit la fabrique.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
/// Dérive l'[`SessionPlan`] le `seed` de reprise : seul [`SessionPlan::Resume`]
|
||||
/// amorce l'adapter avec un id de conversation existant ; `Assign`/`None` partent
|
||||
/// d'une conversation neuve (l'id sera capté au premier tour).
|
||||
fn seed_conversation_id(session: &SessionPlan) -> Option<String> {
|
||||
match session {
|
||||
SessionPlan::Resume { conversation_id } => Some(conversation_id.clone()),
|
||||
SessionPlan::None | SessionPlan::Assign { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AgentSessionFactory for StructuredSessionFactory {
|
||||
fn supports(&self, profile: &AgentProfile) -> bool {
|
||||
profile.structured_adapter.is_some()
|
||||
}
|
||||
|
||||
async fn start(
|
||||
&self,
|
||||
profile: &AgentProfile,
|
||||
_ctx: &PreparedContext,
|
||||
cwd: &ProjectPath,
|
||||
session: &SessionPlan,
|
||||
) -> Result<Arc<dyn AgentSession>, AgentSessionError> {
|
||||
let adapter = profile.structured_adapter.ok_or_else(|| {
|
||||
AgentSessionError::Start(format!(
|
||||
"le profil « {} » n'a pas d'adapter structuré",
|
||||
profile.name
|
||||
))
|
||||
})?;
|
||||
|
||||
let id = SessionId::new_random();
|
||||
let command = profile.command.clone();
|
||||
let cwd = cwd.as_str().to_owned();
|
||||
let seed = seed_conversation_id(session);
|
||||
|
||||
// NOTE : le contexte (`_ctx`) est injecté par `LaunchAgent` (D3) via le
|
||||
// convention file dans le run dir *avant* l'appel à la factory (le `.md` est
|
||||
// déjà écrit) ; l'adapter n'a donc qu'à lancer la CLI dans ce cwd. Aucune
|
||||
// injection supplémentaire n'incombe ici en mode structuré (la CLI lit son
|
||||
// fichier conventionnel — CLAUDE.md / AGENTS.md — depuis le cwd).
|
||||
let session: Arc<dyn AgentSession> = match adapter {
|
||||
StructuredAdapter::Claude => Arc::new(ClaudeSdkSession::new(id, command, cwd, seed)),
|
||||
StructuredAdapter::Codex => Arc::new(CodexExecSession::new(id, command, cwd, seed)),
|
||||
};
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user