feat(agent): fondation exécution structurée des agents IA (D0+D1) — §17
Pivot orchestration : agents IA pilotés via leur mode programmatique/JSON
(capture déterministe), au lieu du TUI brut + self-report. §16 (idea/MCP)
marquée remplacée comme voie principale.
- D0 (domaine) : port AgentSession + AgentSessionFactory, types ReplyEvent
/ReplyStream/AgentSessionError, champ AgentProfile.structured_adapter
(Option<StructuredAdapter{Claude,Codex}>, skip si None ⇒ zéro régression),
catalogue Claude/Codex annotés.
- D1 (application) : registre StructuredSessions (jumeau de TerminalSessions),
agrégateur LiveSessions{pty,structured} derrière LiveAgentRegistry (vivant si
PTY OU structuré, surface du trait inchangée), helper send_blocking (draine le
ReplyStream jusqu'au Final, Timeout sans tuer la session).
Tests : domaine 16+2 ; application registre 11 + send_blocking 9 ; workspace 0 échec.
A/B intacts. Aucun adapter concret (D2), pas de Tauri/front.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -189,6 +189,41 @@ pub type OutputStream = Box<dyn Iterator<Item = Vec<u8>> + Send>;
|
||||
/// A boxed stream of domain events, returned by [`EventBus::subscribe`].
|
||||
pub type EventStream = Box<dyn Iterator<Item = DomainEvent> + Send>;
|
||||
|
||||
/// Un événement incrémental d'un tour de réponse d'un agent IA (ARCHITECTURE §17.1).
|
||||
///
|
||||
/// Universel : l'adapter (Claude/Codex) traduit SON format structuré documenté
|
||||
/// vers ces variantes ; **aucun** détail propre à une CLI (pas de `stream-json`,
|
||||
/// pas de `--output-format`, pas de chemin de transcript) ne franchit cette
|
||||
/// frontière domaine.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ReplyEvent {
|
||||
/// Un fragment de texte assistant (rendu incrémental côté UI chat).
|
||||
TextDelta {
|
||||
/// Le fragment de texte.
|
||||
text: String,
|
||||
},
|
||||
/// Une activité d'outil de l'agent (best-effort, pour l'observabilité chat :
|
||||
/// « lit un fichier », « lance une commande »). Le `label` est déjà
|
||||
/// humain-lisible ; le détail brut reste dans l'adapter.
|
||||
ToolActivity {
|
||||
/// Libellé humain-lisible de l'activité.
|
||||
label: String,
|
||||
},
|
||||
/// **Événement terminal déterministe** d'un tour : l'adapter l'émet quand il a
|
||||
/// lu le message `result` documenté de la CLI. Porte le contenu final agrégé.
|
||||
/// Après `Final`, le flux se termine (plus aucun événement).
|
||||
Final {
|
||||
/// Le contenu final agrégé du tour.
|
||||
content: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Flux borné d'événements de réponse d'UN tour (ARCHITECTURE §17.1). Se termine
|
||||
/// après le [`ReplyEvent::Final`] (ou sur erreur). Calqué sur [`OutputStream`],
|
||||
/// mais **typé** : deltas de texte → activités d'outil → un `Final` déterministe,
|
||||
/// plutôt que des octets bruts.
|
||||
pub type ReplyStream = Box<dyn Iterator<Item = ReplyEvent> + Send>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-port error types
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -218,6 +253,29 @@ pub enum PtyError {
|
||||
NotFound,
|
||||
}
|
||||
|
||||
/// Errors from an [`AgentSession`] / [`AgentSessionFactory`] (ARCHITECTURE §17.1).
|
||||
///
|
||||
/// Frontière nette : on ne propage **jamais** le JSON brut d'une CLI à travers
|
||||
/// ces erreurs (cf. [`AgentSessionError::Decode`]).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum AgentSessionError {
|
||||
/// La session programmatique n'a pas pu démarrer (CLI introuvable, mode
|
||||
/// structuré indisponible, handshake invalide).
|
||||
#[error("agent session start failed: {0}")]
|
||||
Start(String),
|
||||
/// Échec d'envoi/de communication avec la session vivante.
|
||||
#[error("agent session io failed: {0}")]
|
||||
Io(String),
|
||||
/// La sortie structurée de la CLI n'a pas pu être décodée (JSON cassé, schéma
|
||||
/// inattendu). On ne propage jamais le JSON brut.
|
||||
#[error("agent session decode failed: {0}")]
|
||||
Decode(String),
|
||||
/// `send_blocking` n'a pas observé de [`ReplyEvent::Final`] dans le temps
|
||||
/// imparti. La session **reste vivante** (on ne tue rien) ; l'appelant décide.
|
||||
#[error("agent session reply timed out")]
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Errors from [`ProcessSpawner`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum ProcessError {
|
||||
@ -402,6 +460,71 @@ pub trait AgentRuntime: Send + Sync {
|
||||
) -> Result<SpawnSpec, RuntimeError>;
|
||||
}
|
||||
|
||||
/// Une **session programmatique persistante** avec un agent IA (ARCHITECTURE §17.1) :
|
||||
/// une conversation vivante que l'on pilote en mode structuré et dont on lit la
|
||||
/// réponse de façon déterministe. Une instance ⇔ un agent IA (invariant « 1 session
|
||||
/// vivante/agent », porté au niveau *type*).
|
||||
///
|
||||
/// Hexagonal : ce trait est **domaine** ; les adapters Claude/Codex (infra) ne
|
||||
/// fuient aucun détail de CLI à travers lui. Substituable (Liskov) : Claude et
|
||||
/// Codex offrent les mêmes garanties (flux d'événements → [`ReplyEvent::Final`]
|
||||
/// déterministe), seul le moteur diffère.
|
||||
///
|
||||
/// Calqué sur [`PtyPort`] (consommé comme trait-objet `Arc<dyn AgentSession>`,
|
||||
/// d'où `#[async_trait]` pour rester object-safe — cf. note d'en-tête du module).
|
||||
#[async_trait]
|
||||
pub trait AgentSession: Send + Sync {
|
||||
/// L'id de session IdeA (mappe la cellule/agent, comme un [`PtyHandle::session_id`]).
|
||||
fn id(&self) -> SessionId;
|
||||
|
||||
/// L'id de conversation **du moteur** (opaque), persisté sur la cellule pour la
|
||||
/// reprise (§15.2). `None` tant que le moteur n'en a pas attribué. Permet à
|
||||
/// `LeafCell.conversation_id` de rester le pivot de reprise, model-agnostic.
|
||||
fn conversation_id(&self) -> Option<String>;
|
||||
|
||||
/// Transmet `prompt` à la session vivante et retourne le **flux** d'événements
|
||||
/// du tour (deltas → [`ReplyEvent::Final`]). Rendu incrémental (UI chat) ET
|
||||
/// base du rendez-vous synchrone (helper applicatif `send_blocking`).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AgentSessionError::Io`]/[`AgentSessionError::Decode`] sur échec de
|
||||
/// communication/décodage.
|
||||
async fn send(&self, prompt: &str) -> Result<ReplyStream, AgentSessionError>;
|
||||
|
||||
/// Termine proprement la session (tue le process/SDK sous-jacent). Idempotent.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AgentSessionError::Io`] si l'arrêt échoue.
|
||||
async fn shutdown(&self) -> Result<(), AgentSessionError>;
|
||||
}
|
||||
|
||||
/// **Factory** sélectionnée par le profil (ARCHITECTURE §17.1) : crée/reprend une
|
||||
/// [`AgentSession`] pour un agent IA. C'est elle qui sait *quel adapter* instancier
|
||||
/// (Claude/Codex) selon `profile.structured_adapter` (§17.3). Open/Closed : ajouter
|
||||
/// un moteur structuré = ajouter un adapter + une variante de registre, sans
|
||||
/// toucher au cœur.
|
||||
#[async_trait]
|
||||
pub trait AgentSessionFactory: Send + Sync {
|
||||
/// Vrai si cette factory sait piloter `profile` en mode structuré (sert au menu
|
||||
/// de sélection §17.6 : ne proposer que les profils supportés).
|
||||
fn supports(&self, profile: &AgentProfile) -> bool;
|
||||
|
||||
/// Démarre une session structurée pour `profile` dans `cwd` (run dir isolé
|
||||
/// §14.1), avec le contexte déjà préparé ([`PreparedContext`]) et l'intention de
|
||||
/// session ([`SessionPlan`] : neuf / assign / resume — réutilise §15).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AgentSessionError::Start`] si la CLI/SDK est indisponible ou le mode
|
||||
/// structuré ne peut s'initialiser.
|
||||
async fn start(
|
||||
&self,
|
||||
profile: &AgentProfile,
|
||||
ctx: &PreparedContext,
|
||||
cwd: &ProjectPath,
|
||||
session: &SessionPlan,
|
||||
) -> Result<Arc<dyn AgentSession>, AgentSessionError>;
|
||||
}
|
||||
|
||||
/// Open and drive pseudo-terminals.
|
||||
#[async_trait]
|
||||
pub trait PtyPort: Send + Sync {
|
||||
|
||||
Reference in New Issue
Block a user