fix(domain,application,infrastructure,backend,app-tauri,frontend): Stop demande agent préserve la conversation ouverte — arrêt sans fermeture/clôture du chat, états cohérents après cancel sur les 3 adapters de session, cellule et workstate synchronisés — #343 (QA verte)
This commit is contained in:
@ -48,26 +48,27 @@ use crate::dto::{
|
||||
parse_ticket_id, save_model_server_input, AgentDriftListDto, AgentDto, AgentListDto,
|
||||
AllocateManagedTempRequestDto, AppExitWorkGuardStateDto, AssignSkillRequestDto,
|
||||
AttachBackgroundTaskResultDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto,
|
||||
BackgroundTaskDto, CellKind, ChangeAgentProfileDto, ChangeAgentProfileRequestDto,
|
||||
ChatAttachmentDto, ChatAttachmentInputDto, ChatDraftScopeRequestDto, ChatPromptAttachmentDto,
|
||||
CloneOpenCodeProfileFromSeedRequestDto, CloneProfileFromSeedRequestDto,
|
||||
ConfigureProfilesRequestDto, ConversationDetailsDto, CreateAgentFromTemplateRequestDto,
|
||||
CreateAgentRequestDto, CreateLayoutRequestDto, CreateLayoutResultDto, CreateMemoryRequestDto,
|
||||
CreateProjectRequestDto, CreateSkillRequestDto, CreateTemplateRequestDto,
|
||||
DeleteLayoutRequestDto, DeleteLayoutResultDto, DeliveredDelegationRequestDto,
|
||||
DetectProfilesRequestDto, DetectProfilesResponseDto, EmbedderEnginesDto, EmbedderProfileDto,
|
||||
EmbedderProfileListDto, ErrorDto, ExecuteSlashCommandRequestDto,
|
||||
ExecuteSlashCommandResponseDto, FirstRunStateDto, FrontAttachedRequestDto, GitBranchesDto,
|
||||
GitCheckoutRequestDto, GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto,
|
||||
GitStatusListDto, GraphCommitListDto, HealthRequestDto, HealthResponseDto,
|
||||
ImportChatAttachmentsRequestDto, ImportChatAttachmentsResponseDto,
|
||||
InspectConversationRequestDto, InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto,
|
||||
LayoutOperationDto, ListLayoutsDto, LiveAgentListDto, ManagedTempGcResponseDto,
|
||||
ManagedTempHandleRequestDto, ManagedTempLeaseDto, ManagedTempLeaseResponseDto, MemoryDto,
|
||||
MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto, ModelServerConfigListDto,
|
||||
OpenCodeProviderListDto, OpenProjectRestoreStateDto, OpenTerminalRequestDto,
|
||||
PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProfileModelCatalogDto, ProjectDto,
|
||||
ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSettingsDto,
|
||||
BackgroundTaskDto, CancelAgentChatResponseDto, CellKind, ChangeAgentProfileDto,
|
||||
ChangeAgentProfileRequestDto, ChatAttachmentDto, ChatAttachmentInputDto,
|
||||
ChatDraftScopeRequestDto, ChatPromptAttachmentDto, CloneOpenCodeProfileFromSeedRequestDto,
|
||||
CloneProfileFromSeedRequestDto, ConfigureProfilesRequestDto, ConversationDetailsDto,
|
||||
CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateLayoutRequestDto,
|
||||
CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto,
|
||||
CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto,
|
||||
DeliveredDelegationRequestDto, DetectProfilesRequestDto, DetectProfilesResponseDto,
|
||||
EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto,
|
||||
ExecuteSlashCommandRequestDto, ExecuteSlashCommandResponseDto, FirstRunStateDto,
|
||||
FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto,
|
||||
GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, GraphCommitListDto,
|
||||
HealthRequestDto, HealthResponseDto, ImportChatAttachmentsRequestDto,
|
||||
ImportChatAttachmentsResponseDto, InspectConversationRequestDto, InterruptAgentRequestDto,
|
||||
LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, LiveAgentListDto,
|
||||
ManagedTempGcResponseDto, ManagedTempHandleRequestDto, ManagedTempLeaseDto,
|
||||
ManagedTempLeaseResponseDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
|
||||
ModelServerConfigDto, ModelServerConfigListDto, OpenCodeProviderListDto,
|
||||
OpenProjectRestoreStateDto, OpenTerminalRequestDto, PreviewModelServerCommandDto, ProfileDto,
|
||||
ProfileListDto, ProfileModelCatalogDto, ProjectDto, ProjectListDto,
|
||||
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSettingsDto,
|
||||
ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
|
||||
ReadChatDraftResponseDto, ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto,
|
||||
RecallMemoryRequestDto, RemoveOpenProjectRestoreEntryRequestDto, RemoveProjectRequestDto,
|
||||
@ -2855,18 +2856,29 @@ pub async fn agent_send(
|
||||
.structured_sessions
|
||||
.session(&sid)
|
||||
.ok_or_else(|| ErrorDto::from(AppError::NotFound(format!("structured session {sid}"))))?;
|
||||
let turn_generation = state
|
||||
.structured_sessions
|
||||
.begin_turn(&sid)
|
||||
.ok_or_else(|| ErrorDto::from(AppError::NotFound(format!("structured session {sid}"))))?;
|
||||
|
||||
// (Re-)register the reply channel; bumps the generation so an earlier attach's
|
||||
// pump (if any) is superseded and stops delivering to its stale channel.
|
||||
let gen = state.chat_bridge.register(sid, on_reply);
|
||||
|
||||
let imported_attachments = import_chat_attachments_for_session(
|
||||
let imported_attachments = match import_chat_attachments_for_session(
|
||||
&state,
|
||||
&sid,
|
||||
attachment_paths.unwrap_or_default(),
|
||||
attachments.unwrap_or_default(),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
Ok(attachments) => attachments,
|
||||
Err(error) => {
|
||||
state.structured_sessions.finish_turn(&sid, turn_generation);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let attachment_refs = chat_prompt_attachment_refs(&imported_attachments);
|
||||
let prompt_for_model =
|
||||
prompt_with_imported_attachments(&prompt, &imported_attachments, &attachment_refs);
|
||||
@ -2910,10 +2922,14 @@ pub async fn agent_send(
|
||||
let tap_bridge = std::sync::Arc::clone(&state.chat_bridge);
|
||||
let tap_clock = std::sync::Arc::clone(&state.clock);
|
||||
let tap_sid = sid;
|
||||
let tap_structured = std::sync::Arc::clone(&state.structured_sessions);
|
||||
let tap_turn_guard = turn_guard.as_ref().map(|guard| guard.liveness_handle());
|
||||
let tap_pump = std::thread::spawn(move || {
|
||||
let mut settler = crate::chat::ProgressSettler::default();
|
||||
for event in tap_rx {
|
||||
if !tap_structured.accepts_turn_output(&tap_sid, turn_generation) {
|
||||
break;
|
||||
}
|
||||
if !matches!(event, domain::ports::ReplyEvent::Final { .. }) {
|
||||
if let Some(guard) = &tap_turn_guard {
|
||||
guard.mark_alive();
|
||||
@ -2929,11 +2945,25 @@ pub async fn agent_send(
|
||||
}
|
||||
let _ = tap_bridge.send_output_settled(&tap_sid, &mut settler, chunk);
|
||||
}
|
||||
if let Some(completed) = settler.finish() {
|
||||
let _ = tap_bridge.send_output(&tap_sid, completed);
|
||||
if tap_structured.accepts_turn_output(&tap_sid, turn_generation) {
|
||||
if let Some(completed) = settler.finish() {
|
||||
let _ = tap_bridge.send_output(&tap_sid, completed);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !state
|
||||
.structured_sessions
|
||||
.accepts_turn_output(&sid, turn_generation)
|
||||
{
|
||||
drop(tap_tx);
|
||||
let _ = tap_pump.join();
|
||||
if let Some(guard) = &turn_guard {
|
||||
guard.finish();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Open the turn stream. A start failure leaves the just-registered channel in
|
||||
// place (the cell stays attached, ready for a retry) — mirrors the PTY pump,
|
||||
// which only unregisters on a hard subscribe failure; here the session is
|
||||
@ -2941,6 +2971,16 @@ pub async fn agent_send(
|
||||
let stream_result = session.send_with_tap(&prompt_for_model, tap_tx).await;
|
||||
let _ = tap_pump.join();
|
||||
let mut turn_guard = turn_guard;
|
||||
if !state
|
||||
.structured_sessions
|
||||
.accepts_turn_output(&sid, turn_generation)
|
||||
{
|
||||
if let Some(guard) = &turn_guard {
|
||||
guard.finish();
|
||||
}
|
||||
state.chat_bridge.detach_if(&sid, gen);
|
||||
return Ok(());
|
||||
}
|
||||
let stream = match stream_result {
|
||||
Ok(stream) => stream,
|
||||
Err(_error)
|
||||
@ -2952,7 +2992,10 @@ pub async fn agent_send(
|
||||
// logical turn. The detached drain below waits for the replacement.
|
||||
Box::new(std::iter::empty()) as domain::ports::ReplyStream
|
||||
}
|
||||
Err(error) => return Err(ErrorDto::from(AppError::from(error))),
|
||||
Err(error) => {
|
||||
state.structured_sessions.finish_turn(&sid, turn_generation);
|
||||
return Err(ErrorDto::from(AppError::from(error)));
|
||||
}
|
||||
};
|
||||
|
||||
// Drain the blocking reply iterator on a dedicated OS thread (the stream is a
|
||||
@ -2978,6 +3021,10 @@ pub async fn agent_send(
|
||||
let mut resume = false;
|
||||
let mut resume_prompt = None;
|
||||
for event in current_stream {
|
||||
if !structured.accepts_turn_output(&sid, turn_generation) {
|
||||
terminal_visible = true;
|
||||
break 'logical_turn;
|
||||
}
|
||||
let provider_terminal = matches!(
|
||||
event,
|
||||
domain::ports::ReplyEvent::Final { .. }
|
||||
@ -3126,7 +3173,7 @@ pub async fn agent_send(
|
||||
}
|
||||
}
|
||||
}
|
||||
if !terminal_visible {
|
||||
if !terminal_visible && structured.accepts_turn_output(&sid, turn_generation) {
|
||||
let chunk = crate::chat::with_chunk_timestamp(
|
||||
crate::chat::unfinished_turn_chunk(),
|
||||
u64::try_from(clock.now_millis()).ok(),
|
||||
@ -3136,6 +3183,7 @@ pub async fn agent_send(
|
||||
if let Some(guard) = &turn_guard {
|
||||
guard.finish();
|
||||
}
|
||||
structured.finish_turn(&sid, turn_generation);
|
||||
bridge.detach_if(&sid, gen);
|
||||
});
|
||||
|
||||
@ -3512,6 +3560,7 @@ pub async fn interrupt_agent(
|
||||
return session
|
||||
.cancel_current_turn()
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ErrorDto::from(AppError::from(e)));
|
||||
}
|
||||
}
|
||||
@ -3524,11 +3573,10 @@ pub async fn interrupt_agent(
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `cancel_agent_chat` — stop a live structured chat session and cascade its
|
||||
/// running execution children best-effort.
|
||||
/// `cancel_agent_chat` — cancel only the current request of a live structured chat.
|
||||
///
|
||||
/// This is the session-id based twin of [`stop_live_agent`] for a custom chat
|
||||
/// surface that already owns the structured `sessionId`.
|
||||
/// The reusable session, conversation bridge and runtime state remain intact;
|
||||
/// agent-level teardown stays exclusively on [`stop_live_agent`].
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if no live
|
||||
@ -3537,30 +3585,26 @@ pub async fn interrupt_agent(
|
||||
pub async fn cancel_agent_chat(
|
||||
session_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
) -> Result<CancelAgentChatResponseDto, ErrorDto> {
|
||||
let sid = parse_session_id(&session_id)?;
|
||||
let (project_id, agent_id, _, _) = state
|
||||
.structured_sessions
|
||||
.meta_for_session(&sid)
|
||||
.ok_or_else(|| ErrorDto::from(AppError::NotFound(format!("structured session {sid}"))))?;
|
||||
let project = resolve_project(&project_id.to_string(), &state).await?;
|
||||
let dependencies = state
|
||||
.orchestrator_service
|
||||
.active_wait_dependencies(agent_id);
|
||||
state.stop_turn_watch(project.id, agent_id);
|
||||
for dependency in dependencies {
|
||||
state.stop_turn_watch(project.id, dependency);
|
||||
}
|
||||
state
|
||||
.stop_live_agent
|
||||
.execute(StopLiveAgentInput {
|
||||
project,
|
||||
agent_id,
|
||||
reason: Some("fermeture de la session chat".to_owned()),
|
||||
})
|
||||
let output = state
|
||||
.cancel_agent_chat
|
||||
.execute(sid)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(ErrorDto::from)
|
||||
.map_err(ErrorDto::from)?;
|
||||
if output.outcome == domain::ports::CancelCurrentTurnOutcome::Cancelled {
|
||||
let project = resolve_project(&project_id.to_string(), &state).await?;
|
||||
state
|
||||
.orchestrator_service
|
||||
.cancel_agent_turn_execution_children(&project, agent_id)
|
||||
.await
|
||||
.map_err(ErrorDto::from)?;
|
||||
}
|
||||
Ok(CancelAgentChatResponseDto::from(output))
|
||||
}
|
||||
|
||||
async fn preferred_view_for_agent(
|
||||
|
||||
Reference in New Issue
Block a user