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:
@ -94,6 +94,10 @@ type HeadlessSink =
|
||||
/// la soumission du collage pour esquiver la paste-detection des CLI. Utilisé seulement
|
||||
/// quand le profil de la cible ne fournit pas de `submit_delay_ms`.
|
||||
const DEFAULT_SUBMIT_DELAY_MS: u32 = 60;
|
||||
/// Taille maximale d'un fragment de consigne écrit dans le PTY en mode headless.
|
||||
const DELEGATION_WRITE_CHUNK_BYTES: usize = 512;
|
||||
/// Pause entre deux fragments de consigne en mode headless.
|
||||
const DELEGATION_WRITE_CHUNK_DELAY_MS: u64 = 8;
|
||||
|
||||
/// État de vivacité d'un agent maintenu par le [`BusyTracker`] (lot 2).
|
||||
///
|
||||
@ -486,8 +490,10 @@ impl MediatedInbox {
|
||||
// watcher prompt-ready, tâche tokio du serveur MCP, ou le fil de l'enqueue).
|
||||
// Texte d'abord, puis la séquence de soumission après le délai — comme le
|
||||
// write-portal frontend — pour esquiver la détection de coller des CLI.
|
||||
// La consigne est fragmentée : les TUI (Codex surtout) peuvent traiter un
|
||||
// gros write unique comme un paste et garder/perdre la fin au démarrage.
|
||||
std::thread::spawn(move || {
|
||||
let _ = pty.write(&handle, text.as_bytes());
|
||||
let _ = write_delegation_chunks(pty.as_ref(), &handle, &text);
|
||||
if delay > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(delay));
|
||||
}
|
||||
@ -662,6 +668,30 @@ fn delegation_preamble(requester: &str, ticket: TicketId, task: &str) -> String
|
||||
format!("[IdeA · tâche de {requester} · ticket {ticket}]\n{task}")
|
||||
}
|
||||
|
||||
fn write_delegation_chunks(
|
||||
pty: &dyn PtyPort,
|
||||
handle: &PtyHandle,
|
||||
text: &str,
|
||||
) -> Result<(), domain::ports::PtyError> {
|
||||
let mut start = 0;
|
||||
let mut current_len = 0;
|
||||
|
||||
for (idx, ch) in text.char_indices() {
|
||||
let len = ch.len_utf8();
|
||||
if current_len > 0 && current_len + len > DELEGATION_WRITE_CHUNK_BYTES {
|
||||
pty.write(handle, text[start..idx].as_bytes())?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(
|
||||
DELEGATION_WRITE_CHUNK_DELAY_MS,
|
||||
));
|
||||
start = idx;
|
||||
current_len = 0;
|
||||
}
|
||||
current_len += len;
|
||||
}
|
||||
|
||||
pty.write(handle, text[start..].as_bytes())
|
||||
}
|
||||
|
||||
impl InputMediator for MediatedInbox {
|
||||
fn enqueue(&self, agent: AgentId, ticket: Ticket) -> PendingReply {
|
||||
let ticket_id = ticket.id;
|
||||
@ -1115,6 +1145,43 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_agent_chunks_long_delegation_before_submit() {
|
||||
let pty = Arc::new(FakePty::new());
|
||||
let bus = Arc::new(RecordingBus::default());
|
||||
let inbox = MediatedInbox::with_pty(
|
||||
Arc::new(InMemoryMailbox::new()),
|
||||
Arc::new(FixedClock(1)),
|
||||
Arc::clone(&pty) as Arc<dyn PtyPort>,
|
||||
)
|
||||
.with_events(Arc::clone(&bus) as Arc<dyn EventBus>);
|
||||
let a = agent(1);
|
||||
let task = format!("début {} fin", "x".repeat(1200));
|
||||
inbox.bind_handle_with_prompt(a, handle(1), None, SubmitConfig::default());
|
||||
|
||||
inbox.enqueue(a, ticket(10, &task));
|
||||
|
||||
assert!(
|
||||
wait_until(|| pty.writes.lock().unwrap().len() >= 4),
|
||||
"long headless delivery writes multiple chunks followed by submit"
|
||||
);
|
||||
let writes = pty.writes.lock().unwrap().clone();
|
||||
assert_eq!(writes.last().map(Vec::as_slice), Some(b"\r".as_slice()));
|
||||
let delivered = writes[..writes.len() - 1]
|
||||
.iter()
|
||||
.flat_map(|chunk| chunk.iter().copied())
|
||||
.collect::<Vec<_>>();
|
||||
let delivered = String::from_utf8(delivered).unwrap();
|
||||
assert!(
|
||||
delivered.contains(&task),
|
||||
"chunked writes reconstruct the full task"
|
||||
);
|
||||
assert!(
|
||||
bus.delegation_ready().is_empty(),
|
||||
"headless delivery does not also publish DelegationReady"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enqueue_returns_pending_reply_resolved_via_mailbox() {
|
||||
let inbox = inbox_at(5);
|
||||
|
||||
Reference in New Issue
Block a user