feat(orchestrator): modèle de désignation d'orchestrateur + sink de diagnostic
Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).
Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -24,6 +24,11 @@ use domain::profile::{
|
||||
StructuredAdapter,
|
||||
};
|
||||
|
||||
/// Codex's interactive TUI is sensitive to receiving a large pasted block and the
|
||||
/// submit key too close together. Keep the default conservative so delegated
|
||||
/// prompts are actually submitted instead of remaining in the input editor.
|
||||
pub const CODEX_SUBMIT_DELAY_MS: u32 = 350;
|
||||
|
||||
/// A fixed UUID namespace used to derive stable ids for reference profiles.
|
||||
/// (Random-looking but constant; only its stability matters.)
|
||||
const REFERENCE_NAMESPACE: uuid::Uuid = uuid::uuid!("6f9b1d2a-7c34-4e58-9a1b-2c3d4e5f6a7b");
|
||||
@ -82,6 +87,7 @@ pub fn reference_profiles() -> Vec<AgentProfile> {
|
||||
.expect("codex reference profile is valid")
|
||||
.with_structured_adapter(StructuredAdapter::Codex)
|
||||
.with_projector(ProjectorKey::Codex)
|
||||
.with_submit_delay_ms(CODEX_SUBMIT_DELAY_MS)
|
||||
.with_mcp(McpCapability::new(
|
||||
// Codex lit ses serveurs MCP dans `$CODEX_HOME/config.toml`, pas `.mcp.json` :
|
||||
// IdeA écrit ce TOML DANS le run dir et pointe `CODEX_HOME` dessus pour
|
||||
|
||||
@ -21,14 +21,14 @@ use domain::ports::{
|
||||
StoreError,
|
||||
};
|
||||
use domain::profile::{McpConfigStrategy, StructuredAdapter};
|
||||
use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan};
|
||||
use domain::{
|
||||
Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile, ContextInjection, ConversationId,
|
||||
ConversationParty, DomainEvent, EffectivePermissions, Handoff, HandoffStore, ManifestEntry,
|
||||
MarkdownDoc, MemoryIndexEntry, MemoryType, NodeId, PermissionProjector, ProfileId,
|
||||
ProjectedFile, ProjectionContext, ProjectorKey, Project, ProjectPath, ProviderSessionStore,
|
||||
PtySize, SessionId, SessionKind, SessionStatus, Skill, TerminalSession,
|
||||
MarkdownDoc, MemoryIndexEntry, MemoryType, NodeId, PermissionProjector, ProfileId, Project,
|
||||
ProjectPath, ProjectedFile, ProjectionContext, ProjectorKey, ProviderSessionStore, PtySize,
|
||||
SessionId, SessionKind, SessionStatus, Skill, TerminalSession,
|
||||
};
|
||||
use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan};
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::layout::{persist_doc, resolve_doc};
|
||||
@ -1446,6 +1446,7 @@ impl LaunchAgent {
|
||||
let prepared = PreparedContext {
|
||||
content: content.clone(),
|
||||
relative_path: agent.context_path.clone(),
|
||||
project_root: input.project.root.as_str().to_owned(),
|
||||
};
|
||||
// 4a. Resolve the session intention (T4). The conversation id is a property
|
||||
// of the *cell*, not the PTY: the caller (which owns the layout) passes
|
||||
|
||||
@ -22,7 +22,9 @@ pub use structured::{
|
||||
drain_with_readiness, drain_with_readiness_outcome, send_blocking, TurnOutcome,
|
||||
};
|
||||
|
||||
pub use catalogue::{reference_profile_id, reference_profiles, selectable_reference_profiles};
|
||||
pub use catalogue::{
|
||||
reference_profile_id, reference_profiles, selectable_reference_profiles, CODEX_SUBMIT_DELAY_MS,
|
||||
};
|
||||
pub use inspect::{InspectConversation, InspectConversationInput, InspectConversationOutput};
|
||||
pub use lifecycle::{
|
||||
ChangeAgentProfile, ChangeAgentProfileInput, ChangeAgentProfileOutput, CreateAgentFromScratch,
|
||||
|
||||
@ -35,8 +35,7 @@ use crate::error::AppError;
|
||||
/// `--resume` (via [`domain::ports::SessionPlan::Resume`]) porte déjà tout
|
||||
/// l'historique : ce prompt n'a qu'à **réamorcer** le tour, pas reconstruire le
|
||||
/// contexte. Volontairement neutre et model-agnostique.
|
||||
pub const RESUME_PROMPT: &str =
|
||||
"La limite de session est levée. Reprends là où tu t'étais arrêté.";
|
||||
pub const RESUME_PROMPT: &str = "La limite de session est levée. Reprends là où tu t'étais arrêté.";
|
||||
|
||||
/// Port applicatif de **reprise d'un agent** (frontière implémentée au composition
|
||||
/// root, LS7). Calqué sur les autres traits-passerelles de l'application
|
||||
@ -161,7 +160,13 @@ impl SessionLimitService {
|
||||
conversation_id,
|
||||
} = plan_resume(now, &limit, conversation_id)
|
||||
{
|
||||
self.arm_scheduled(agent_id, fire_at_ms, node_id, conversation_id, Some(resets_at_ms));
|
||||
self.arm_scheduled(
|
||||
agent_id,
|
||||
fire_at_ms,
|
||||
node_id,
|
||||
conversation_id,
|
||||
Some(resets_at_ms),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,9 +204,14 @@ impl SessionLimitService {
|
||||
conversation_id,
|
||||
},
|
||||
);
|
||||
self.armed.lock().expect("session-limit mutex sain").insert(agent_id, id);
|
||||
self.events
|
||||
.publish(DomainEvent::AgentResumeScheduled { agent_id, fire_at_ms });
|
||||
self.armed
|
||||
.lock()
|
||||
.expect("session-limit mutex sain")
|
||||
.insert(agent_id, id);
|
||||
self.events.publish(DomainEvent::AgentResumeScheduled {
|
||||
agent_id,
|
||||
fire_at_ms,
|
||||
});
|
||||
}
|
||||
|
||||
/// **(b) Exécution de la reprise.** Consomme une [`ScheduledTask::ResumeAgent`]
|
||||
@ -255,7 +265,10 @@ impl SessionLimitService {
|
||||
};
|
||||
|
||||
if self.scheduler.cancel(id) {
|
||||
self.armed.lock().expect("session-limit mutex sain").remove(&agent_id);
|
||||
self.armed
|
||||
.lock()
|
||||
.expect("session-limit mutex sain")
|
||||
.remove(&agent_id);
|
||||
self.events
|
||||
.publish(DomainEvent::AgentResumeCancelled { agent_id });
|
||||
true
|
||||
|
||||
@ -14,8 +14,8 @@
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use domain::input::InputMediator;
|
||||
use domain::ids::AgentId;
|
||||
use domain::input::InputMediator;
|
||||
use domain::ports::{AgentSession, AgentSessionError, ReplyEvent};
|
||||
use domain::readiness::{ReadinessPolicy, ReadinessSignal};
|
||||
|
||||
@ -178,16 +178,15 @@ async fn drain_bounded_events(
|
||||
on_signal: impl FnMut(ReadinessSignal),
|
||||
) -> Result<TurnOutcome, AgentSessionError> {
|
||||
match timeout {
|
||||
Some(dur) => match tokio::time::timeout(
|
||||
dur,
|
||||
drain_to_final(session, prompt, on_event, on_signal),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
// La session **reste vivante** : on ne `shutdown` rien ici (§17.1).
|
||||
Err(_elapsed) => Err(AgentSessionError::Timeout),
|
||||
},
|
||||
Some(dur) => {
|
||||
match tokio::time::timeout(dur, drain_to_final(session, prompt, on_event, on_signal))
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
// La session **reste vivante** : on ne `shutdown` rien ici (§17.1).
|
||||
Err(_elapsed) => Err(AgentSessionError::Timeout),
|
||||
}
|
||||
}
|
||||
None => drain_to_final(session, prompt, on_event, on_signal).await,
|
||||
}
|
||||
}
|
||||
@ -317,7 +316,9 @@ mod tests {
|
||||
let session = FakeSession {
|
||||
events: vec![
|
||||
ReplyEvent::TextDelta { text: "a".into() },
|
||||
ReplyEvent::ToolActivity { label: "lit".into() },
|
||||
ReplyEvent::ToolActivity {
|
||||
label: "lit".into(),
|
||||
},
|
||||
ReplyEvent::Heartbeat,
|
||||
ReplyEvent::Final {
|
||||
content: "fini".into(),
|
||||
|
||||
Reference in New Issue
Block a user