chore(wip): checkpoint P8/C avant chantier Codex inter-agents
Sauvegarde de l'arbre de travail en cours (persistance P8, conversations C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le support de la délégation inter-agents pour les profils Codex. Le round-trip inter-agent question/réponse est couvert sans tokens par les tests loopback existants (state::mcp_e2e_loopback_tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -19,15 +19,15 @@ use std::time::Duration;
|
||||
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
use domain::conversation::{
|
||||
ConversationParty, ConversationRegistry, SessionRef, WaitForGraph,
|
||||
};
|
||||
use domain::conversation::{ConversationParty, ConversationRegistry, SessionRef, WaitForGraph};
|
||||
use domain::conversation_log::{ConversationTurn, TurnId, TurnRole};
|
||||
use domain::input::{InputMediator, InputSource};
|
||||
use domain::input::{InputMediator, InputSource, SubmitConfig};
|
||||
use domain::mailbox::{Ticket, TicketId};
|
||||
use domain::ports::{Clock, EventBus, ProfileStore, PtyHandle};
|
||||
use domain::project::ProjectPath;
|
||||
use domain::{AgentId, DomainEvent, OrchestratorCommand, OrchestratorVisibility, ProfileId, Project};
|
||||
use domain::{
|
||||
AgentId, DomainEvent, OrchestratorCommand, OrchestratorVisibility, ProfileId, Project,
|
||||
};
|
||||
|
||||
use crate::conversation::RecordTurn;
|
||||
|
||||
@ -416,7 +416,10 @@ impl OrchestratorService {
|
||||
target,
|
||||
content,
|
||||
requester,
|
||||
} => self.propose_context(project, target, content, requester).await,
|
||||
} => {
|
||||
self.propose_context(project, target, content, requester)
|
||||
.await
|
||||
}
|
||||
OrchestratorCommand::ReadMemory { slug, requester } => {
|
||||
self.read_memory(project, slug, requester).await
|
||||
}
|
||||
@ -453,10 +456,7 @@ impl OrchestratorService {
|
||||
})
|
||||
.await?;
|
||||
Ok(OrchestratorOutcome {
|
||||
detail: format!(
|
||||
"read {} context",
|
||||
target.as_deref().unwrap_or("project")
|
||||
),
|
||||
detail: format!("read {} context", target.as_deref().unwrap_or("project")),
|
||||
reply: Some(md.into_string()),
|
||||
})
|
||||
}
|
||||
@ -481,15 +481,17 @@ impl OrchestratorService {
|
||||
})
|
||||
.await?;
|
||||
let detail = match outcome {
|
||||
ProposeOutcome::Written => format!(
|
||||
"wrote {} context",
|
||||
target.as_deref().unwrap_or("project")
|
||||
),
|
||||
ProposeOutcome::Written => {
|
||||
format!("wrote {} context", target.as_deref().unwrap_or("project"))
|
||||
}
|
||||
ProposeOutcome::Proposed { path } => {
|
||||
format!("filed proposal for project context at {path}")
|
||||
}
|
||||
};
|
||||
Ok(OrchestratorOutcome { detail, reply: None })
|
||||
Ok(OrchestratorOutcome {
|
||||
detail,
|
||||
reply: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// `memory.read` → reads a note (or the index) under a shared read-lease; the
|
||||
@ -755,8 +757,8 @@ impl OrchestratorService {
|
||||
.await?;
|
||||
// Arm prompt-ready detection (C5) with the target profile's literal marker, so a
|
||||
// return-to-prompt frees the turn (the other OR signal being `idea_reply`).
|
||||
let prompt_pattern = self.prompt_pattern_for_agent(project, agent_id).await;
|
||||
input.bind_handle_with_prompt(agent_id, handle.clone(), prompt_pattern);
|
||||
let (prompt_pattern, submit) = self.prompt_and_submit_for_agent(project, agent_id).await;
|
||||
input.bind_handle_with_prompt(agent_id, handle.clone(), prompt_pattern, submit);
|
||||
|
||||
// 2. Enregistrer le ticket (slot de réponse) + livrer le tour via le médiateur
|
||||
// (écriture sérialisée dans le PTY — plus d'écriture ad hoc ici). Le ticket
|
||||
@ -866,8 +868,8 @@ impl OrchestratorService {
|
||||
let handle = self
|
||||
.ensure_live_pty(project, agent_id, conversation_id, target)
|
||||
.await?;
|
||||
let prompt_pattern = self.prompt_pattern_for_agent(project, agent_id).await;
|
||||
input.bind_handle_with_prompt(agent_id, handle, prompt_pattern);
|
||||
let (prompt_pattern, submit) = self.prompt_and_submit_for_agent(project, agent_id).await;
|
||||
input.bind_handle_with_prompt(agent_id, handle, prompt_pattern, submit);
|
||||
|
||||
// Enqueue a human-sourced ticket in the SAME FIFO as delegations. Fire-and-
|
||||
// forget: we drop the PendingReply (the human reads the terminal). The
|
||||
@ -922,6 +924,34 @@ impl OrchestratorService {
|
||||
})
|
||||
}
|
||||
|
||||
/// **Ack de livraison** (ARCHITECTURE §20.3) — best-effort, observabilité only.
|
||||
///
|
||||
/// The frontend write-portal calls this (via the `delegation_delivered` Tauri
|
||||
/// command) once it has **physically written** a delegation `ticket` into the agent's
|
||||
/// native PTY, to distinguish "queued because the human line was busy" from "written"
|
||||
/// in logs/observability. It **does not** change correlation: the requester's `ask`
|
||||
/// is still woken by `idea_reply`→`resolve_ticket`, and the per-turn timeout/cycle
|
||||
/// guards are untouched. It is a pure log no-op when nothing is wired, so a missing
|
||||
/// mediator/registry never breaks the rendezvous.
|
||||
pub fn note_delegation_delivered(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent_id: AgentId,
|
||||
ticket: TicketId,
|
||||
) {
|
||||
// Observability beacon only: never mutates the mailbox/busy state nor resolves
|
||||
// the ticket (that stays `idea_reply`-driven). Kept best-effort by construction —
|
||||
// a pure, infallible hook so a missing mediator/registry never breaks the
|
||||
// rendezvous. The application crate carries no logging framework (zero new dep);
|
||||
// the ack is materialised as an `eprintln!` trace, the same lightweight channel
|
||||
// the orchestrator already uses for best-effort diagnostics.
|
||||
let _ = project;
|
||||
eprintln!(
|
||||
"[orchestrator] delegation delivered into agent {agent_id}'s native terminal \
|
||||
(front ack, ticket {ticket})"
|
||||
);
|
||||
}
|
||||
|
||||
/// Resolves the conversation thread id for an ask: `A↔B` when an agent requests,
|
||||
/// else `User↔B` (cadrage C3 §5.2). Without a wired registry, falls back to a
|
||||
/// stable per-agent id derived from the target (legacy routing — never panics).
|
||||
@ -944,7 +974,6 @@ impl OrchestratorService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// `agent.reply` / `idea_reply`: the target agent renders the result of the task
|
||||
/// it is currently processing (Option 1, lot B-4).
|
||||
///
|
||||
@ -1039,19 +1068,18 @@ impl OrchestratorService {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let session_id = self
|
||||
.sessions
|
||||
.session_for_agent(&agent_id)
|
||||
.ok_or_else(|| {
|
||||
AppError::Process(format!(
|
||||
"agent {target} n'a pas de session terminal vivante après lancement"
|
||||
))
|
||||
})?;
|
||||
let session_id = self.sessions.session_for_agent(&agent_id).ok_or_else(|| {
|
||||
AppError::Process(format!(
|
||||
"agent {target} n'a pas de session terminal vivante après lancement"
|
||||
))
|
||||
})?;
|
||||
// Lier la session fraîchement lancée à CE fil (registre terminal + registre de
|
||||
// conversations) ⇒ un prochain ask sur le même fil la réutilise.
|
||||
self.bind_conversation_session(conversation_id, session_id);
|
||||
self.sessions.handle(&session_id).ok_or_else(|| {
|
||||
AppError::Process(format!("handle PTY de l'agent {target} introuvable après lancement"))
|
||||
AppError::Process(format!(
|
||||
"handle PTY de l'agent {target} introuvable après lancement"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@ -1318,30 +1346,41 @@ impl OrchestratorService {
|
||||
)))
|
||||
}
|
||||
|
||||
/// Resolves the **prompt-ready pattern** (cadrage §6, lot C5) of the agent's
|
||||
/// profile, used to arm the [`InputMediator`]'s prompt detection at `bind_handle`
|
||||
/// time. Returns `None` when the agent, its profile, or the pattern is absent —
|
||||
/// the safe fallback: no pattern ⇒ Idle only via explicit signal/timeout.
|
||||
async fn prompt_pattern_for_agent(
|
||||
/// Resolves the target agent profile's **prompt-ready pattern** (§6, lot C5) **and**
|
||||
/// its **submit config** (`submit_sequence`/`submit_delay_ms`, ARCHITECTURE §20.3) in
|
||||
/// a single profile lookup. Both are carried into `bind_handle_with_prompt`: the
|
||||
/// pattern arms prompt detection, the submit config is echoed on the next
|
||||
/// `DelegationReady` so the frontend write-portal knows how to submit. Returns
|
||||
/// `(None, default)` when the agent, its profile, or the field is absent — the safe
|
||||
/// fallback (no pattern ⇒ Idle only via explicit signal/timeout; no submit ⇒ the
|
||||
/// front applies its own `"\r"`/~60 ms default).
|
||||
async fn prompt_and_submit_for_agent(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent_id: AgentId,
|
||||
) -> Option<String> {
|
||||
let agent = self
|
||||
) -> (Option<String>, SubmitConfig) {
|
||||
let Some(agent) = self
|
||||
.list_agents
|
||||
.execute(ListAgentsInput {
|
||||
project: project.clone(),
|
||||
})
|
||||
.await
|
||||
.ok()?
|
||||
.agents
|
||||
.into_iter()
|
||||
.find(|a| a.id == agent_id)?;
|
||||
let profiles = self.profiles.list().await.ok()?;
|
||||
profiles
|
||||
.into_iter()
|
||||
.find(|p| p.id == agent.profile_id)
|
||||
.and_then(|p| p.prompt_ready_pattern)
|
||||
.ok()
|
||||
.and_then(|out| out.agents.into_iter().find(|a| a.id == agent_id))
|
||||
else {
|
||||
return (None, SubmitConfig::default());
|
||||
};
|
||||
let Some(profile) = self
|
||||
.profiles
|
||||
.list()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|ps| ps.into_iter().find(|p| p.id == agent.profile_id))
|
||||
else {
|
||||
return (None, SubmitConfig::default());
|
||||
};
|
||||
let submit = SubmitConfig::new(profile.submit_sequence, profile.submit_delay_ms);
|
||||
(profile.prompt_ready_pattern, submit)
|
||||
}
|
||||
|
||||
/// Resolves a human-friendly profile reference (slug like `claude-code`,
|
||||
|
||||
Reference in New Issue
Block a user