agent conversation fix
This commit is contained in:
@ -762,29 +762,16 @@ impl MediatedInbox {
|
||||
pub fn mailbox(&self) -> Arc<InMemoryMailbox> {
|
||||
Arc::clone(&self.mailbox)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Composes the line delivered into the target's terminal for a delegated turn.
|
||||
/// Composes the text delivered into a human-facing terminal turn.
|
||||
///
|
||||
/// The `[IdeA · tâche de <requester> · ticket <id>]` header is the protocol signal
|
||||
/// (cadrage B-5, agent context) that marks the message as an IdeA delegation the
|
||||
/// target must answer via `idea_reply` — echoing `ticket` for multi-thread
|
||||
/// correlation — rather than as a free-text human prompt.
|
||||
///
|
||||
/// A one-line **imperative reminder** follows the header (durcissement
|
||||
/// comportemental, finding A) : the single most common wedge is a target that ends
|
||||
/// its turn with a *prose* answer and never calls `idea_reply`, leaving the asker
|
||||
/// parked. Reminding it inline — on every delegated task, even trivial ones — attacks
|
||||
/// that behavioural cause at the central point where the turn is composed (no per-agent
|
||||
/// duplication). The raw `task` then follows so the agent reads the request unchanged.
|
||||
/// Inter-agent conversation no longer uses this path: structured/headless asks send the
|
||||
/// raw task through `AgentSession::send` and capture the model `Final`. Therefore this
|
||||
/// terminal delivery must not inject any inter-agent ticket/reply protocol.
|
||||
fn delegation_preamble(requester: &str, ticket: TicketId, task: &str) -> String {
|
||||
format!(
|
||||
"[IdeA · tâche de {requester} · ticket {ticket}]\n\
|
||||
⚠️ Termine IMPÉRATIVEMENT ce tour par un appel `idea_reply(ticket=\"{ticket}\", \
|
||||
result=…)`, même pour une réponse triviale. Ne réponds JAMAIS uniquement en prose \
|
||||
dans le terminal : sans `idea_reply`, l'agent demandeur reste bloqué.\n{task}"
|
||||
)
|
||||
let _ = (requester, ticket);
|
||||
task.to_owned()
|
||||
}
|
||||
|
||||
fn write_delegation_chunks(
|
||||
@ -866,14 +853,6 @@ impl InputMediator for MediatedInbox {
|
||||
busy: true,
|
||||
});
|
||||
let submit = self.submit().get(&agent).cloned().unwrap_or_default();
|
||||
// Préfixe de délégation : c'est le **signal** qui dit à la cible
|
||||
// « ceci est une tâche IdeA, réponds via `idea_reply` (jamais en
|
||||
// texte) en renvoyant ce `ticket` ». Sans lui la cible traite la
|
||||
// ligne comme une invite humaine et répond dans le terminal, et
|
||||
// l'agent demandeur reste bloqué jusqu'au timeout. La tâche brute
|
||||
// reste dans le `Ticket` (mailbox/historique) ; seul le texte livré
|
||||
// au PTY porte le préfixe. Format aligné sur l'instruction injectée
|
||||
// dans le contexte de l'agent (`[IdeA · tâche de … · ticket …]`).
|
||||
let text = delegation_preamble(&ticket.requester, ticket_id, &ticket.task);
|
||||
// **Fix race cold-launch** : si l'agent est en démarrage à froid (inscrit
|
||||
// par `mark_starting` quand un pont MCP le libérera), ON DIFFÈRE la
|
||||
@ -916,6 +895,33 @@ impl InputMediator for MediatedInbox {
|
||||
self.mailbox.enqueue(agent, ticket)
|
||||
}
|
||||
|
||||
fn enqueue_silent(&self, agent: AgentId, ticket: Ticket) -> PendingReply {
|
||||
let ticket_id = ticket.id;
|
||||
// Headless/system turns share the same FIFO and busy/liveness accounting as
|
||||
// terminal-delivered turns, but their prompt is sent through AgentSession::send.
|
||||
// Do not publish DelegationReady here: that would leak the task into the
|
||||
// human-facing terminal/write portal while the headless turn is already running.
|
||||
let now_ms = self.clock.now_ms();
|
||||
let started_turn = self.tracker.start_turn(
|
||||
agent,
|
||||
AgentBusyState::Busy {
|
||||
ticket: ticket_id,
|
||||
since_ms: now_ms,
|
||||
},
|
||||
);
|
||||
if started_turn {
|
||||
let stall_after_ms = self.stall().get(&agent).copied().flatten();
|
||||
self.tracker.arm_liveness(agent, stall_after_ms, now_ms);
|
||||
if let Some(events) = &self.tracker.events {
|
||||
events.publish(DomainEvent::AgentBusyChanged {
|
||||
agent_id: agent,
|
||||
busy: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.mailbox.enqueue(agent, ticket)
|
||||
}
|
||||
|
||||
fn bind_handle(&self, agent: AgentId, handle: PtyHandle) {
|
||||
eprintln!(
|
||||
"[input-mediator] bind handle agent={agent} handle={}",
|
||||
@ -1162,17 +1168,10 @@ mod tests {
|
||||
assert_eq!(ready.len(), 1, "exactly one DelegationReady on turn start");
|
||||
let tid = TicketId::from_uuid(uuid::Uuid::from_u128(10));
|
||||
assert_eq!(ready[0].0, tid);
|
||||
// The delivered text carries the delegation prefix (the signal to answer via
|
||||
// `idea_reply`) followed by the raw task on the next line.
|
||||
assert_eq!(ready[0].1, delegation_preamble("User", tid, "do the thing"));
|
||||
assert!(
|
||||
ready[0].1.starts_with("[IdeA · tâche de User · ticket "),
|
||||
"delivered text must carry the delegation prefix: {:?}",
|
||||
ready[0].1
|
||||
);
|
||||
assert!(
|
||||
ready[0].1.ends_with("\ndo the thing"),
|
||||
"raw task must follow the prefix line: {:?}",
|
||||
ready[0].1 == "do the thing",
|
||||
"delivered text must be the raw task: {:?}",
|
||||
ready[0].1
|
||||
);
|
||||
|
||||
@ -1189,11 +1188,25 @@ mod tests {
|
||||
inbox.enqueue(a, ticket(12, "next turn"));
|
||||
let ready = bus.delegation_ready();
|
||||
assert_eq!(ready.len(), 2, "new turn ⇒ a new DelegationReady");
|
||||
assert_eq!(ready[1].1, "next turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enqueue_silent_marks_busy_without_delivery() {
|
||||
let bus = Arc::new(RecordingBus::default());
|
||||
let inbox = MediatedInbox::new(Arc::new(InMemoryMailbox::new()), Arc::new(FixedClock(1)))
|
||||
.with_events(Arc::clone(&bus) as Arc<dyn EventBus>);
|
||||
let a = agent(1);
|
||||
|
||||
inbox.enqueue_silent(a, ticket(10, "headless turn"));
|
||||
|
||||
assert!(inbox.busy_state(a).is_busy());
|
||||
assert_eq!(bus.busy_events(), vec![(a, true)]);
|
||||
assert!(
|
||||
ready[1].1.ends_with("\nnext turn"),
|
||||
"second turn delivers its own prefixed task: {:?}",
|
||||
ready[1].1
|
||||
bus.delegation_ready().is_empty(),
|
||||
"headless bookkeeping must not leak a prompt to the terminal"
|
||||
);
|
||||
assert_eq!(inbox.mailbox.pending(&a), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1548,7 +1561,10 @@ mod tests {
|
||||
.await
|
||||
.expect("must NOT hang until the long timeout")
|
||||
.expect("a turn outcome, not a closed channel");
|
||||
assert_eq!(res, domain::mailbox::TurnResolution::ReturnedToPromptNoReply);
|
||||
assert_eq!(
|
||||
res,
|
||||
domain::mailbox::TurnResolution::ReturnedToPromptNoReply
|
||||
);
|
||||
assert_eq!(
|
||||
inbox.mailbox().pending(&a),
|
||||
0,
|
||||
@ -1589,7 +1605,10 @@ mod tests {
|
||||
let pending = inbox.enqueue(a, ticket(10, "task"));
|
||||
inbox.turn_ended(a);
|
||||
// turn_ended a marqué Idle et armé la grâce ; on répond pendant la grâce.
|
||||
assert!(!inbox.busy_state(a).is_busy(), "turn_ended marks idle (grace armed)");
|
||||
assert!(
|
||||
!inbox.busy_state(a).is_busy(),
|
||||
"turn_ended marks idle (grace armed)"
|
||||
);
|
||||
inbox.mailbox().resolve(a, "in-time".to_owned()).unwrap();
|
||||
|
||||
let res = tokio::time::timeout(Duration::from_secs(2), pending)
|
||||
@ -1616,7 +1635,10 @@ mod tests {
|
||||
.await
|
||||
.expect("no hang")
|
||||
.expect("outcome");
|
||||
assert_eq!(res, domain::mailbox::TurnResolution::ReturnedToPromptNoReply);
|
||||
assert_eq!(
|
||||
res,
|
||||
domain::mailbox::TurnResolution::ReturnedToPromptNoReply
|
||||
);
|
||||
|
||||
// A late idea_reply now has no head to resolve ⇒ typed NoPendingRequest.
|
||||
assert!(
|
||||
@ -1685,11 +1707,7 @@ mod tests {
|
||||
1,
|
||||
"release_cold_start ⇒ exactement une DelegationReady"
|
||||
);
|
||||
assert!(
|
||||
ready[0].1.ends_with("\ncold task"),
|
||||
"le texte différé porte la tâche brute: {:?}",
|
||||
ready[0].1
|
||||
);
|
||||
assert_eq!(ready[0].1, "cold task");
|
||||
}
|
||||
|
||||
/// **Régression de la race cold-start (ordre INVERSE)** : `release_cold_start`
|
||||
@ -1724,11 +1742,7 @@ mod tests {
|
||||
1,
|
||||
"release AVANT enqueue ⇒ exactement une DelegationReady (jamais zéro, jamais deux)"
|
||||
);
|
||||
assert!(
|
||||
ready[0].1.ends_with("\ncold task"),
|
||||
"le texte livré porte la tâche brute: {:?}",
|
||||
ready[0].1
|
||||
);
|
||||
assert_eq!(ready[0].1, "cold task");
|
||||
assert!(
|
||||
inbox.busy_state(a).is_busy(),
|
||||
"le tour froid livré ⇒ l'agent reste Busy (idle viendra d'idea_reply)"
|
||||
@ -1798,7 +1812,7 @@ mod tests {
|
||||
1,
|
||||
"agent chaud ⇒ DelegationReady immédiate (zéro régression)"
|
||||
);
|
||||
assert!(ready[0].1.ends_with("\nwarm task"));
|
||||
assert_eq!(ready[0].1, "warm task");
|
||||
}
|
||||
|
||||
/// (c) Cas limite : si on ne marque PAS `starting` (l'orchestrateur n'arme pas le
|
||||
@ -1849,7 +1863,7 @@ mod tests {
|
||||
2,
|
||||
"second tour livré immédiatement (plus de gate)"
|
||||
);
|
||||
assert!(ready[1].1.ends_with("\nsecond"));
|
||||
assert_eq!(ready[1].1, "second");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
|
||||
Reference in New Issue
Block a user