feat(agent): conversation par paire + entrée médiée + pivot terminal/MCP
Coeur inter-agents consolidé et surface front réalignée sur la décision "terminal natif PTY, pas d'UI chat" (Option 1). Domaine - nouveaux modules conversation, mailbox, input, fileguard (ports + types) - orchestrator/profile/events étendus (conversation par paire, FIFO) Application / Infrastructure - orchestrator/service + context_guard : sérialisation FIFO par agent, garde RW mémoire/contexte, dispatch ask/reply - adapters in-memory conversation / mailbox / input / fileguard - registry session + lifecycle agent durcis (1 agent = 1 session vivante) - outils MCP idea_* alignés sur le nouveau dispatch Frontend - MediatedInput + useAgentBusy : entrée utilisateur médiée par IdeA, terminal = vue sortie inchangée - suppression de la vue chat structurée (AgentChatView) — abandonnée - adapter input + ports mis à jour Divers - .ideai/ : mémoire projet + briefs de cadrage versionnés ; requests/ runtime ignoré ; agents projet réels (DevBackend/DevFrontend/QA) Tests : Rust (domain/application/infrastructure/app-tauri) + front (346) verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -283,6 +283,30 @@ pub struct AgentProfile {
|
||||
/// sérialisation : un profil sans MCP sérialise exactement comme avant.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mcp: Option<McpCapability>,
|
||||
/// Motif de **retour-de-prompt** (cadrage « conversation par paire » §6, lot C5).
|
||||
///
|
||||
/// Donnée **déclarative** (pas de code par CLI — Open/Closed) : un motif **littéral**
|
||||
/// recherché par sous-chaîne dans le flux de sortie PTY du tour courant. Quand il
|
||||
/// apparaît, IdeA considère l'agent **revenu au prompt** et le marque `Idle`
|
||||
/// (`InputMediator::mark_idle`), ce qui fait avancer sa file. C'est l'un des deux
|
||||
/// signaux du « double signal OR » (l'autre étant un `idea_reply` explicite) ; le
|
||||
/// **premier** des deux qui arrive libère le tour.
|
||||
///
|
||||
/// **Choix tranché : littéral, pas regex.** Une sous-chaîne littérale est
|
||||
/// déterministe, sans dépendance (`regex`), suffisante pour un sigil de prompt
|
||||
/// stable (ex. `"\n> "`), et ne risque pas le faux-positif d'un motif regex mal
|
||||
/// échappé présent dans la sortie. Un moteur regex pourra être ajouté plus tard
|
||||
/// comme variante déclarative (Open/Closed) si le besoin se confirme.
|
||||
///
|
||||
/// `None` (défaut, et valeur des profils existants) ⇒ **aucune** détection par
|
||||
/// motif : l'agent ne repasse `Idle` que sur signal explicite (`idea_reply`) ou via
|
||||
/// le garde-fou du timeout par tour. Conforme au fallback « en cas de doute → reste
|
||||
/// `Busy` mais la file continue d'accepter » (jamais de faux `Idle`).
|
||||
///
|
||||
/// `skip_serializing_if = Option::is_none` ⇒ **zéro régression** de sérialisation :
|
||||
/// un profil sans motif sérialise exactement comme avant.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prompt_ready_pattern: Option<String>,
|
||||
}
|
||||
|
||||
/// Embedding strategy of an [`EmbedderProfile`] (LOT C, étage 2 vectoriel).
|
||||
@ -418,6 +442,7 @@ impl AgentProfile {
|
||||
session,
|
||||
structured_adapter: None,
|
||||
mcp: None,
|
||||
prompt_ready_pattern: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -439,6 +464,15 @@ impl AgentProfile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder : fixe le motif de **retour-de-prompt** (§6, lot C5) et renvoie le
|
||||
/// profil. Laisse [`AgentProfile::new`] stable (zéro régression d'appel) : les
|
||||
/// profils sans détection de prompt ne l'appellent simplement pas.
|
||||
#[must_use]
|
||||
pub fn with_prompt_ready_pattern(mut self, pattern: impl Into<String>) -> Self {
|
||||
self.prompt_ready_pattern = Some(pattern.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Indique si ce profil peut être **proposé à la sélection/création** d'un
|
||||
/// agent (§17.3, lot D7). Source **unique** de vérité : un profil n'est
|
||||
/// sélectionnable que s'il porte un [`StructuredAdapter`], c'est-à-dire s'il
|
||||
@ -625,4 +659,48 @@ mod mcp_tests {
|
||||
let s = McpConfigStrategy::env("IDEA_MCP_SERVER").expect("valid");
|
||||
assert_eq!(s, McpConfigStrategy::Env { var: "IDEA_MCP_SERVER".to_owned() });
|
||||
}
|
||||
|
||||
// -- Lot C5 : prompt_ready_pattern (détection retour-de-prompt) --------------
|
||||
|
||||
#[test]
|
||||
fn profile_default_has_no_prompt_ready_pattern() {
|
||||
// Existing profiles (built via `new`) carry no pattern ⇒ no false Idle.
|
||||
assert!(profile_without_mcp().prompt_ready_pattern.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_without_prompt_pattern_omits_key_in_json() {
|
||||
let json = serde_json::to_string(&profile_without_mcp()).expect("serialise");
|
||||
assert!(
|
||||
!json.contains("promptReadyPattern"),
|
||||
"a profile without a prompt pattern must NOT serialise the key (zero regression); got: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_json_without_prompt_pattern_deserialises_to_none() {
|
||||
let legacy = r#"{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "Dev",
|
||||
"command": "claude",
|
||||
"args": [],
|
||||
"contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" },
|
||||
"detect": null,
|
||||
"cwdTemplate": "{agentRunDir}"
|
||||
}"#;
|
||||
let profile: AgentProfile = serde_json::from_str(legacy).expect("legacy deserialise");
|
||||
assert!(profile.prompt_ready_pattern.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_prompt_ready_pattern_sets_and_round_trips() {
|
||||
let profile = profile_without_mcp().with_prompt_ready_pattern("\n> ");
|
||||
assert_eq!(profile.prompt_ready_pattern.as_deref(), Some("\n> "));
|
||||
|
||||
let json = serde_json::to_string(&profile).expect("serialise");
|
||||
assert!(json.contains("promptReadyPattern"), "key present: {json}");
|
||||
let back: AgentProfile = serde_json::from_str(&json).expect("deserialise");
|
||||
assert_eq!(profile, back);
|
||||
assert_eq!(back.prompt_ready_pattern.as_deref(), Some("\n> "));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user