Skip to content

Commit 7a2fce9

Browse files
committed
feat(agent): resume interrupted dialog turns
1 parent 5b0f992 commit 7a2fce9

47 files changed

Lines changed: 4158 additions & 450 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/development/ui-testids-CN.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@
182182
| Chat 输入可编辑区域 | `chat-input-textarea` | 富文本可编辑区域。 |
183183
| Chat 发送按钮 | `chat-input-send-btn` | 输入有效时的发送动作。 |
184184
| Chat 取消按钮 | `chat-input-cancel-btn` | 存在时用于取消进行中的发送/生成。 |
185+
| Chat 继续中断任务按钮 | `chat-input-continue-interrupted-btn` | 输入框为空时继续最近一个可恢复的中断 Turn。 |
185186
| Chat 输入工作区条 | `chat-input-workspace-strip` | composer 上方的活动工作区条。 |
186187
| Chat 输入目标切换器 | `chat-input-target-switcher` | 目标/模式切换器。 |
187188
| Chat 输入图片条 | `chat-input-image-strip` | 已附加图片条。 |

‎docs/development/ui-testids.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ Avoid adding IDs to these surfaces unless there is a clear automated workflow.
183183
| Chat input editable region | `chat-input-textarea` | Rich text editable region. |
184184
| Chat send button | `chat-input-send-btn` | Send action when input is valid. |
185185
| Chat cancel button | `chat-input-cancel-btn` | Cancels in-progress send/generation when present. |
186+
| Chat continue interrupted button | `chat-input-continue-interrupted-btn` | Resumes the latest recoverably interrupted turn when the composer is empty. |
186187
| Chat input workspace strip | `chat-input-workspace-strip` | Active workspace strip above composer. |
187188
| Chat input target switcher | `chat-input-target-switcher` | Target/mode switcher. |
188189
| Chat input image strip | `chat-input-image-strip` | Attached image strip. |

‎src/apps/desktop/src/api/agentic_api.rs‎

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ use crate::runtime::{
1616
use crate::startup_trace::DesktopStartupTrace;
1717
use bitfun_agent_runtime::deep_review::sanitize_focused_review_public_metadata;
1818
use bitfun_agent_runtime::sdk::{
19-
AgentDialogSteerRequest, AgentDialogTurnExecution, AgentDialogTurnRequest,
20-
AgentInputAttachment, AgentSessionCreateResult, AgentSessionModeUpdateRequest,
21-
AgentSessionModelSelection, AgentSessionModelSelectionUpdateRequest,
22-
AgentSessionModelUpdateRequest, AgentSubmissionSource, AgentTurnCancellationRequest,
23-
DialogSteerOutcome, PermissionAuditRecord, PermissionGrant, PermissionGrantKey,
24-
PermissionReply, PermissionRequest,
19+
AgentDialogSteerRequest, AgentDialogTurnExecution, AgentDialogTurnRecoveryOutcome,
20+
AgentDialogTurnRecoveryRequest, AgentDialogTurnRequest, AgentInputAttachment,
21+
AgentSessionCreateResult, AgentSessionModeUpdateRequest, AgentSessionModelSelection,
22+
AgentSessionModelSelectionUpdateRequest, AgentSessionModelUpdateRequest, AgentSubmissionSource,
23+
AgentTurnCancellationRequest, AgentTurnInterruptionRequest, DialogSteerOutcome,
24+
PermissionAuditRecord, PermissionGrant, PermissionGrantKey, PermissionReply, PermissionRequest,
25+
RuntimeError,
2526
};
2627
use bitfun_core::agentic::agents::AgentSource;
2728
use bitfun_core::agentic::coordination::{
@@ -65,7 +66,7 @@ use bitfun_core_types::{
6566
WorktreeError, WorktreeErrorCode,
6667
};
6768
use bitfun_product_domains::tool_permissions::PermissionRule;
68-
use bitfun_runtime_ports::{PermissionMode, SessionTurnWindowRequest};
69+
use bitfun_runtime_ports::{PermissionMode, PortErrorKind, SessionTurnWindowRequest};
6970

7071
const SESSION_VIEW_TOOL_RESULT_TOTAL_CHAR_BUDGET: usize = 512 * 1024;
7172
const SESSION_VIEW_TOOL_RESULT_STRING_CHAR_LIMIT: usize = 16 * 1024;
@@ -740,6 +741,20 @@ pub struct CancelDialogTurnRequest {
740741
pub dialog_turn_id: String,
741742
}
742743

744+
#[derive(Debug, Deserialize)]
745+
#[serde(rename_all = "camelCase")]
746+
pub struct RecoverInterruptedDialogTurnRequest {
747+
pub session_id: String,
748+
pub dialog_turn_id: String,
749+
pub execution_generation: u32,
750+
#[serde(default)]
751+
pub workspace_path: Option<String>,
752+
#[serde(default)]
753+
pub remote_connection_id: Option<String>,
754+
#[serde(default)]
755+
pub remote_ssh_host: Option<String>,
756+
}
757+
743758
#[derive(Debug, Deserialize)]
744759
#[serde(rename_all = "camelCase")]
745760
pub struct SteerDialogTurnRequest {
@@ -2747,6 +2762,97 @@ pub async fn cancel_dialog_turn(
27472762
.map(|_| ())
27482763
}
27492764

2765+
/// Request a recoverable interruption for a native local dialog turn. The
2766+
/// command returns only after the old execution has settled and the
2767+
/// `dialog-turn-interrupted` event is safe to act on.
2768+
#[tauri::command]
2769+
pub async fn interrupt_dialog_turn(
2770+
runtime: State<'_, DesktopRuntimeContext>,
2771+
request: CancelDialogTurnRequest,
2772+
) -> Result<(), String> {
2773+
let interruption = runtime
2774+
.agent_runtime()
2775+
.interrupt_turn(AgentTurnInterruptionRequest {
2776+
session_id: request.session_id.clone(),
2777+
turn_id: request.dialog_turn_id.clone(),
2778+
source: Some(AgentSubmissionSource::DesktopUi),
2779+
wait_timeout_ms: Some(30_000),
2780+
})
2781+
.await;
2782+
match interruption {
2783+
Ok(_) => Ok(()),
2784+
// Stop remains universally available. Unsupported recovery surfaces
2785+
// (external routes, Goal, remote, non-standard sessions) degrade to
2786+
// the existing hard cancellation and emit DialogTurnCancelled.
2787+
Err(RuntimeError::Port(error))
2788+
if matches!(error.kind, PortErrorKind::InvalidRequest | PortErrorKind::Timeout) => runtime
2789+
.agent_runtime()
2790+
.cancel_turn(AgentTurnCancellationRequest {
2791+
session_id: request.session_id.clone(),
2792+
turn_id: Some(request.dialog_turn_id.clone()),
2793+
source: Some(AgentSubmissionSource::DesktopUi),
2794+
requester_session_id: None,
2795+
reason: Some(if error.kind == PortErrorKind::Timeout {
2796+
"recoverable interruption timed out".to_string()
2797+
} else {
2798+
"recoverable interruption unavailable".to_string()
2799+
}),
2800+
wait_timeout_ms: None,
2801+
cancel_descendants: true,
2802+
})
2803+
.await
2804+
.map(|_| ())
2805+
.map_err(|fallback_error| {
2806+
log::error!(
2807+
"Failed to cancel dialog turn after interruption was unavailable: session_id={}, dialog_turn_id={}, error={}",
2808+
request.session_id,
2809+
request.dialog_turn_id,
2810+
fallback_error
2811+
);
2812+
format!("Failed to cancel dialog turn: {}", fallback_error.into_message())
2813+
}),
2814+
Err(error) => {
2815+
log::error!(
2816+
"Failed to interrupt dialog turn: session_id={}, dialog_turn_id={}, error={}",
2817+
request.session_id,
2818+
request.dialog_turn_id,
2819+
error
2820+
);
2821+
Err(format!("Failed to interrupt dialog turn: {}", error.into_message()))
2822+
}
2823+
}
2824+
}
2825+
2826+
#[tauri::command]
2827+
pub async fn recover_interrupted_dialog_turn(
2828+
runtime: State<'_, DesktopRuntimeContext>,
2829+
request: RecoverInterruptedDialogTurnRequest,
2830+
) -> Result<AgentDialogTurnRecoveryOutcome, String> {
2831+
runtime
2832+
.agent_runtime()
2833+
.recover_interrupted_turn(AgentDialogTurnRecoveryRequest {
2834+
session_id: request.session_id.clone(),
2835+
turn_id: request.dialog_turn_id.clone(),
2836+
execution_generation: request.execution_generation,
2837+
workspace_path: request.workspace_path,
2838+
remote_connection_id: request.remote_connection_id,
2839+
remote_ssh_host: request.remote_ssh_host,
2840+
})
2841+
.await
2842+
.map_err(|error| {
2843+
log::error!(
2844+
"Failed to recover interrupted dialog turn: session_id={}, dialog_turn_id={}, error={}",
2845+
request.session_id,
2846+
request.dialog_turn_id,
2847+
error
2848+
);
2849+
format!(
2850+
"Failed to recover interrupted dialog turn: {}",
2851+
error.into_message()
2852+
)
2853+
})
2854+
}
2855+
27502856
#[tauri::command]
27512857
pub async fn steer_dialog_turn(
27522858
runtime: State<'_, DesktopRuntimeContext>,
@@ -4142,6 +4248,8 @@ mod tests {
41424248
token_usage: None,
41434249
finish_reason: None,
41444250
has_final_response: None,
4251+
recovery: None,
4252+
recovery_epoch: None,
41454253
error: None,
41464254
error_detail: None,
41474255
status: TurnStatus::Completed,
@@ -4226,6 +4334,8 @@ mod tests {
42264334
token_usage: None,
42274335
finish_reason: None,
42284336
has_final_response: None,
4337+
recovery: None,
4338+
recovery_epoch: None,
42294339
error: None,
42304340
error_detail: None,
42314341
status: TurnStatus::Completed,
@@ -4291,6 +4401,8 @@ mod tests {
42914401
token_usage: None,
42924402
finish_reason: None,
42934403
has_final_response: None,
4404+
recovery: None,
4405+
recovery_epoch: None,
42944406
error: None,
42954407
error_detail: None,
42964408
status: TurnStatus::Completed,

‎src/apps/desktop/src/api/remote_workspace_policy.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
247247
RemoteWorkspacePolicy::LegacyUnaudited,
248248
),
249249
("cancel_dialog_turn", RemoteWorkspacePolicy::LegacyUnaudited),
250+
("interrupt_dialog_turn", RemoteWorkspacePolicy::LocalOnly),
250251
(
251252
"cancel_insights_generation",
252253
RemoteWorkspacePolicy::LocalOnly,
@@ -1401,6 +1402,10 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
14011402
"remove_project_permission_grant",
14021403
RemoteWorkspacePolicy::WorkspaceAgnostic,
14031404
),
1405+
(
1406+
"recover_interrupted_dialog_turn",
1407+
RemoteWorkspacePolicy::LocalOnly,
1408+
),
14041409
(
14051410
"clear_project_permission_grants",
14061411
RemoteWorkspacePolicy::WorkspaceAgnostic,

‎src/apps/desktop/src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,8 @@ pub async fn run() {
12181218
api::agentic_api::ensure_assistant_bootstrap,
12191219
api::agentic_api::run_init_agents_md,
12201220
api::agentic_api::cancel_dialog_turn,
1221+
api::agentic_api::interrupt_dialog_turn,
1222+
api::agentic_api::recover_interrupted_dialog_turn,
12211223
api::agentic_api::steer_dialog_turn,
12221224
api::agentic_api::control_deep_review_queue,
12231225
api::agentic_api::cancel_session,

0 commit comments

Comments
 (0)