fix(orchestrator): délégation inter-agent toujours headless, jamais d'injection PTY
La conversation inter-agent (idea_ask_agent) doit rester strictement headless : la cible répond via une session structured capturée par IdeA, sans jamais écrire dans le PTY d'une cellule visible ni fermer le terminal que l'utilisateur observe. - lifecycle: flag `allow_structured_alongside_pty` — ouvre une session structured pour la délégation sans fermer le PTY visible (coexistence). - orchestrator/service: `ensure_structured_session` ne ferme plus le PTY visible ; mapping typé de l'erreur no-reply ; coexistence PTY/structured. - infrastructure/input: garantit zéro `DelegationReady` et zéro write PTY pour une délégation headless, même quand l'entrée est `front_owned`. - app-tauri (commands/state): câblage du flag de coexistence. - tests: fixtures portées vers le modèle structured/headless, assertions cibles mises à jour (aucun #[ignore] ajouté, aucun test retiré). Validé réel : cargo build OK ; orchestrator_service 60/0, agent_lifecycle 62/0, structured_launch_d3 22/0, infrastructure input 35/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -758,6 +758,7 @@ impl ChangeAgentProfile {
|
||||
// the minimal declaration. A profile-hot-swap that needs the real
|
||||
// endpoint is re-driven through the app-tauri launch path.
|
||||
mcp_runtime: None,
|
||||
allow_structured_alongside_pty: false,
|
||||
})
|
||||
.await?;
|
||||
Ok(Some(output.session))
|
||||
@ -897,6 +898,12 @@ pub struct LaunchAgentInput {
|
||||
/// endpoint/project/requester args) rather than a project-bound one — see
|
||||
/// [`mcp_server_declaration`].
|
||||
pub mcp_runtime: Option<McpRuntime>,
|
||||
/// Internal `ask_agent` seam: when `true`, an existing visible PTY session for
|
||||
/// the same agent must not block the structured/headless launch. This keeps the
|
||||
/// human CLI cell and the inter-agent `AgentSession` input channel separate.
|
||||
///
|
||||
/// Ordinary UI launches keep this `false` and retain the singleton/rebind guard.
|
||||
pub allow_structured_alongside_pty: bool,
|
||||
}
|
||||
|
||||
/// OS/runtime facts injected by the composition root (`app-tauri`) to materialise
|
||||
@ -1449,42 +1456,53 @@ impl LaunchAgent {
|
||||
// (rend la session existante, pas de respawn) ;
|
||||
// - **second lancement neuf** : on vise un **autre** node, sans signal de
|
||||
// réattache ⇒ refus [`AppError::AgentAlreadyRunning`] (node hôte rapporté).
|
||||
if let Some(existing_id) = self.sessions.session_for_agent(&input.agent_id) {
|
||||
let existing_pty = self.sessions.session_for_agent(&input.agent_id);
|
||||
if let Some(existing_id) = existing_pty {
|
||||
let host_node = self.sessions.node_for_agent(&input.agent_id);
|
||||
match reattach_decision(input.node_id, host_node, input.conversation_id.as_deref()) {
|
||||
ReattachDecision::Rebind { node_id } => {
|
||||
if let Some(session) = self.sessions.rebind_agent_node(&input.agent_id, node_id)
|
||||
{
|
||||
return Ok(LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
// Réattache (rebind de vue) : aucun profil re-résolu.
|
||||
profile: None,
|
||||
if input.allow_structured_alongside_pty && input.node_id.is_none() {
|
||||
crate::diag!(
|
||||
"[launch] existing PTY kept while structured launch proceeds: agent={} \
|
||||
pty_session={existing_id}",
|
||||
input.agent_id
|
||||
);
|
||||
} else {
|
||||
match reattach_decision(input.node_id, host_node, input.conversation_id.as_deref())
|
||||
{
|
||||
ReattachDecision::Rebind { node_id } => {
|
||||
if let Some(session) =
|
||||
self.sessions.rebind_agent_node(&input.agent_id, node_id)
|
||||
{
|
||||
return Ok(LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
// Réattache (rebind de vue) : aucun profil re-résolu.
|
||||
profile: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
ReattachDecision::Idempotent => {}
|
||||
ReattachDecision::Refuse { node_id } => {
|
||||
return Err(AppError::AgentAlreadyRunning {
|
||||
agent_id: input.agent_id,
|
||||
node_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
ReattachDecision::Idempotent => {}
|
||||
ReattachDecision::Refuse { node_id } => {
|
||||
return Err(AppError::AgentAlreadyRunning {
|
||||
agent_id: input.agent_id,
|
||||
node_id,
|
||||
// Idempotent (or a rebind that found no entry to move) — hand back the
|
||||
// already-registered session, no respawn, nothing new to persist.
|
||||
if let Some(session) = self.sessions.session(&existing_id) {
|
||||
return Ok(LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
// Idempotent (pas de respawn) : aucun profil re-résolu.
|
||||
profile: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Idempotent (or a rebind that found no entry to move) — hand back the
|
||||
// already-registered session, no respawn, nothing new to persist.
|
||||
if let Some(session) = self.sessions.session(&existing_id) {
|
||||
return Ok(LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
// Idempotent (pas de respawn) : aucun profil re-résolu.
|
||||
profile: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Garde structurée (§17.4) : même sémantique côté registre IA. R0a appliqué de
|
||||
// façon identique — rebind de la cellule-vue pour une réattache légitime,
|
||||
@ -1539,6 +1557,13 @@ impl LaunchAgent {
|
||||
.into_iter()
|
||||
.find(|p| p.id == agent.profile_id)
|
||||
.ok_or_else(|| AppError::NotFound(format!("profile {} for agent", agent.profile_id)))?;
|
||||
if input.allow_structured_alongside_pty && profile.structured_adapter.is_none() {
|
||||
return Err(AppError::Invalid(format!(
|
||||
"agent {}: le lancement headless structuré a été demandé, mais le profil {} \
|
||||
ne déclare pas d'adaptateur structured/headless",
|
||||
input.agent_id, profile.id
|
||||
)));
|
||||
}
|
||||
|
||||
// 3. Compute and create the agent's isolated run directory
|
||||
// `<root>/.ideai/run/<agent-id>/` (ARCHITECTURE §14.1). The PTY cwd is
|
||||
|
||||
Reference in New Issue
Block a user