agent conversation fix

This commit is contained in:
2026-06-27 12:42:37 +02:00
parent c2d1a669c5
commit 1fc7869160
38 changed files with 1173 additions and 1549 deletions

View File

@ -164,6 +164,21 @@ pub trait InputMediator: Send + Sync {
/// authority of the FIFO/busy state and correlation only.
fn enqueue(&self, agent: AgentId, ticket: Ticket) -> PendingReply;
/// Headless/system enqueue: appends `ticket` to the same FIFO and marks the agent
/// busy, but does **not** deliver any text to the human terminal surface.
///
/// This is for inter-agent structured/headless turns where the real transport is
/// [`crate::ports::AgentSession::send`] and the answer is its final response. The
/// mailbox ticket remains useful for queue/workstate accounting and cancellation,
/// but publishing [`crate::events::DomainEvent::DelegationReady`] here would leak
/// the same task into the user's CLI cell.
///
/// Default keeps compatibility for simple mediators; production overrides it to
/// suppress delivery.
fn enqueue_silent(&self, agent: AgentId, ticket: Ticket) -> PendingReply {
self.enqueue(agent, ticket)
}
/// Registers (or refreshes) the live input [`PtyHandle`] of `agent` so a later
/// [`InputMediator::enqueue`] delivers the turn through it (cadrage C3 §5.2).
///

View File

@ -427,7 +427,11 @@ mod tests {
// Nothing is live ⇒ aid(1) is an orphan.
let rewritten = state.reconcile_orphans(|_| false, 999);
assert_eq!(rewritten.len(), 1, "the single active-but-dead row is an orphan");
assert_eq!(
rewritten.len(),
1,
"the single active-but-dead row is an orphan"
);
let row = &rewritten[0];
assert_eq!(row.agent_id, aid(1));
assert_eq!(row.status, WorkStatus::Idle, "downgraded to idle");
@ -441,15 +445,12 @@ mod tests {
#[test]
fn reconcile_orphans_covers_working_waiting_blocked_only() {
let mut state = LiveState::default();
state.upsert(
LiveEntry::new(aid(1), None, "w", WorkStatus::Working, None, None, 1).unwrap(),
);
state.upsert(
LiveEntry::new(aid(2), None, "a", WorkStatus::Waiting, None, None, 1).unwrap(),
);
state.upsert(
LiveEntry::new(aid(3), None, "b", WorkStatus::Blocked, None, None, 1).unwrap(),
);
state
.upsert(LiveEntry::new(aid(1), None, "w", WorkStatus::Working, None, None, 1).unwrap());
state
.upsert(LiveEntry::new(aid(2), None, "a", WorkStatus::Waiting, None, None, 1).unwrap());
state
.upsert(LiveEntry::new(aid(3), None, "b", WorkStatus::Blocked, None, None, 1).unwrap());
// idle/done are never orphans, even when the agent is dead.
state.upsert(LiveEntry::new(aid(4), None, "i", WorkStatus::Idle, None, None, 1).unwrap());
state.upsert(LiveEntry::new(aid(5), None, "d", WorkStatus::Done, None, None, 1).unwrap());
@ -479,20 +480,36 @@ mod tests {
let rewritten = state.reconcile_orphans(|a| *a == aid(1), 50);
assert_eq!(rewritten.len(), 1);
assert_eq!(rewritten[0].agent_id, aid(2), "only the dead agent is reconciled");
assert_eq!(
rewritten[0].agent_id,
aid(2),
"only the dead agent is reconciled"
);
}
#[test]
fn reconcile_orphans_does_not_mutate_self() {
let mut state = LiveState::default();
state.upsert(
LiveEntry::new(aid(1), Some(tid(1)), "x", WorkStatus::Working, None, None, 1).unwrap(),
LiveEntry::new(
aid(1),
Some(tid(1)),
"x",
WorkStatus::Working,
None,
None,
1,
)
.unwrap(),
);
let before = state.clone();
let _ = state.reconcile_orphans(|_| false, 999);
assert_eq!(state, before, "reconcile_orphans is a pure read (returns rows to upsert)");
assert_eq!(
state, before,
"reconcile_orphans is a pure read (returns rows to upsert)"
);
}
#[test]