fix(custom-cli): busy/progress/tools/chatbox bloqués après le premier appel — #245 (QA verte)
Backend : StructuredHumanTurnGuard pour partager la comptabilité busy/FIFO entre chat direct humain (agent_send, web chat.send) et asks inter-agents structurés, avec liveness mark_alive et retrait idempotent du tour. Web-server : tap d'événements sur chat.send pour le scrollback temps réel. Frontend : scoping des clés de progression par tour pour éviter la déduplication/ghosting des progress et outils entre appels.
This commit is contained in:
@ -836,6 +836,87 @@ struct AppAgentResumer {
|
||||
contexts: ResumeContexts,
|
||||
}
|
||||
|
||||
/// Guard for a direct human turn sent to a structured agent session.
|
||||
///
|
||||
/// Direct chat adapters (`agent_send`, web `chat.send`) still drive the provider
|
||||
/// through `AgentSession::send`, but they must share the same busy/FIFO accounting
|
||||
/// as structured inter-agent asks. This guard starts a silent human turn and
|
||||
/// retires it on terminal outcome or drop.
|
||||
pub struct StructuredHumanTurnGuard {
|
||||
input: Arc<dyn domain::input::InputMediator>,
|
||||
mailbox: Arc<dyn domain::mailbox::AgentMailbox>,
|
||||
agent: domain::RuntimeAgentKey,
|
||||
ticket_id: domain::mailbox::TicketId,
|
||||
armed: Arc<std::sync::atomic::AtomicBool>,
|
||||
finish_on_drop: bool,
|
||||
}
|
||||
|
||||
impl StructuredHumanTurnGuard {
|
||||
/// Starts the mediated busy turn for a direct human structured chat message.
|
||||
#[must_use]
|
||||
pub fn start(
|
||||
input: Arc<dyn domain::input::InputMediator>,
|
||||
mailbox: Arc<dyn domain::mailbox::AgentMailbox>,
|
||||
project_id: ProjectId,
|
||||
agent_id: AgentId,
|
||||
prompt: &str,
|
||||
) -> Self {
|
||||
let agent = domain::RuntimeAgentKey::new(project_id, agent_id);
|
||||
let ticket_id = domain::mailbox::TicketId::new_random();
|
||||
let owns_turn = if input.busy_state(agent).is_busy() {
|
||||
false
|
||||
} else {
|
||||
let ticket = domain::mailbox::Ticket::new(ticket_id, "user", prompt);
|
||||
let _pending = input.enqueue_silent(agent, ticket);
|
||||
input.busy_state(agent).ticket() == Some(ticket_id)
|
||||
};
|
||||
Self {
|
||||
input,
|
||||
mailbox,
|
||||
agent,
|
||||
ticket_id,
|
||||
armed: Arc::new(std::sync::atomic::AtomicBool::new(owns_turn)),
|
||||
finish_on_drop: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a non-owning handle that can keep the turn alive from relay code.
|
||||
#[must_use]
|
||||
pub fn liveness_handle(&self) -> Self {
|
||||
Self {
|
||||
input: Arc::clone(&self.input),
|
||||
mailbox: Arc::clone(&self.mailbox),
|
||||
agent: self.agent,
|
||||
ticket_id: self.ticket_id,
|
||||
armed: Arc::clone(&self.armed),
|
||||
finish_on_drop: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Records proof of liveness for the running turn.
|
||||
pub fn mark_alive(&self) {
|
||||
if self.armed.load(std::sync::atomic::Ordering::Acquire) {
|
||||
self.input.mark_alive(self.agent);
|
||||
}
|
||||
}
|
||||
|
||||
/// Retires the FIFO head and marks the agent idle. Idempotent across clones.
|
||||
pub fn finish(&self) {
|
||||
if self.armed.swap(false, std::sync::atomic::Ordering::AcqRel) {
|
||||
self.mailbox.cancel_head(self.agent, self.ticket_id);
|
||||
self.input.mark_idle(self.agent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StructuredHumanTurnGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.finish_on_drop {
|
||||
self.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AgentResumer for AppAgentResumer {
|
||||
async fn resume(
|
||||
@ -1384,6 +1465,9 @@ pub struct BackendCore {
|
||||
/// Médiateur d'entrée partagé, capturé pour câbler le callback `turn_ended` du
|
||||
/// [`turn_watcher`](Self::turn_watcher) à l'armement.
|
||||
pub turn_watch_input: Arc<dyn domain::input::InputMediator>,
|
||||
/// Mailbox paired with [`Self::turn_watch_input`], used by direct structured
|
||||
/// human turns to retire the synthetic busy/FIFO ticket on terminal outcome.
|
||||
pub input_mailbox: Arc<dyn domain::mailbox::AgentMailbox>,
|
||||
/// Handles des watches de fin-de-tour vivants, par clé runtime. (Re)lancer un agent
|
||||
/// dans un projet **remplace** son handle (l'ancien est droppé ⇒ polling arrêté) ;
|
||||
/// fermer/arrêter l'agent le retire. `Mutex` car launch/stop y accèdent concurremment.
|
||||
@ -3259,6 +3343,7 @@ impl BackendCore {
|
||||
resume_contexts,
|
||||
turn_watcher,
|
||||
turn_watch_input: Arc::clone(&input_mediator),
|
||||
input_mailbox: Arc::clone(&mailbox),
|
||||
turn_watch_handles: Mutex::new(HashMap::new()),
|
||||
fs_port: Arc::clone(&fs_port),
|
||||
home_dir,
|
||||
|
||||
Reference in New Issue
Block a user