Skip to content

Commit e9c159a

Browse files
authored
fix(acp): unify autonomous turn lifecycle (#61)
1 parent 82e3978 commit e9c159a

3 files changed

Lines changed: 115 additions & 104 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "kit"
3-
version = "0.1.114"
3+
version = "0.1.115"
44
edition = "2024"
55
rust-version = "1.94.0"
66
publish = false

src/protocols/acp/v2.rs

Lines changed: 113 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,48 +1068,16 @@ async fn prepare_prompt<S: ModelSession + Send + 'static>(
10681068
wire::UserMessage::new(user_message_id).content(request.prompt),
10691069
),
10701070
))?;
1071-
send_state(
1072-
sink,
1073-
session_id,
1074-
wire::StateUpdate::Running(wire::RunningStateUpdate::new()),
1075-
)?;
1076-
let stop_reason = match drive_prompt(
1071+
run_active_turn(
10771072
session_id,
1078-
driver,
1073+
integration,
10791074
handle,
1075+
driver,
1076+
sink,
10801077
cancellation_generation,
10811078
structured_completion.then_some((tasks, background_jobs)),
10821079
)
10831080
.await
1084-
{
1085-
Ok(stop_reason) => stop_reason,
1086-
Err(_)
1087-
if handle
1088-
.cancellation_handle()
1089-
.is_cancelled_since(cancellation_generation) =>
1090-
{
1091-
wire::StopReason::Cancelled
1092-
}
1093-
Err(error) => {
1094-
if structured_completion {
1095-
super::cancel_background_jobs(tasks, background_jobs).await;
1096-
let _ = super::settle_background_jobs(tasks, background_jobs).await;
1097-
}
1098-
terminalize_running_error(session_id, integration, handle, sink, &error).await?;
1099-
return Err(error);
1100-
}
1101-
};
1102-
if structured_completion && stop_reason == wire::StopReason::Cancelled {
1103-
super::cancel_background_jobs(tasks, background_jobs).await;
1104-
let _ = super::settle_background_jobs(tasks, background_jobs).await?;
1105-
}
1106-
let _ = integration.flush_session_updates(session_id).await;
1107-
integration.finish_prompt(session_id);
1108-
send_state(
1109-
sink,
1110-
session_id,
1111-
wire::StateUpdate::Idle(wire::IdleStateUpdate::new().stop_reason(stop_reason)),
1112-
)
11131081
}
11141082
.await;
11151083
if result.is_err() && structured_completion {
@@ -1265,80 +1233,110 @@ where
12651233
}
12661234
}
12671235

1268-
async fn drive_autonomous<S: ModelSession + Send + 'static>(
1236+
async fn run_active_turn<S: ModelSession + Send + 'static>(
12691237
session_id: &wire::SessionId,
12701238
integration: &AcpIntegration,
12711239
handle: &AcpSessionHandle,
1272-
busy: &AtomicBool,
12731240
driver: &mut LoopDriver<S>,
12741241
sink: &impl AcpSessionUpdateSink,
1242+
cancellation_generation: u64,
1243+
structured: Option<(&TaskManagerHandle, &BackgroundJobs)>,
12751244
) -> Result<(), AcpRuntimeError> {
1276-
if claim_prompt(busy).is_err() {
1277-
return Ok(());
1278-
}
1279-
handle.prepare_injection_turn();
1280-
integration.finish_prompt(session_id);
1281-
let cancellation_generation = handle.cancellation_handle().generation();
1282-
handle.start_injection_turn();
1283-
let result = async {
1284-
match drive_prompt(session_id, driver, handle, cancellation_generation, None).await {
1285-
Ok(_) => {}
1286-
Err(_)
1287-
if handle
1288-
.cancellation_handle()
1289-
.is_cancelled_since(cancellation_generation) => {}
1290-
Err(error) => {
1291-
terminalize_error(session_id, integration, handle, sink, &error, None).await?;
1292-
return Err(error);
1245+
send_state(
1246+
sink,
1247+
session_id,
1248+
wire::StateUpdate::Running(wire::RunningStateUpdate::new()),
1249+
)?;
1250+
let stop_reason = match drive_prompt(
1251+
session_id,
1252+
driver,
1253+
handle,
1254+
cancellation_generation,
1255+
structured,
1256+
)
1257+
.await
1258+
{
1259+
Ok(stop_reason) => stop_reason,
1260+
Err(_)
1261+
if handle
1262+
.cancellation_handle()
1263+
.is_cancelled_since(cancellation_generation) =>
1264+
{
1265+
wire::StopReason::Cancelled
1266+
}
1267+
Err(error) => {
1268+
if let Some((tasks, background_jobs)) = structured {
1269+
super::cancel_background_jobs(tasks, background_jobs).await;
1270+
let _ = super::settle_background_jobs(tasks, background_jobs).await;
12931271
}
1272+
terminalize_running_error(session_id, integration, handle, sink, &error).await?;
1273+
return Err(error);
12941274
}
1295-
let _ = integration.flush_session_updates(session_id).await;
1296-
Ok(())
1275+
};
1276+
if stop_reason == wire::StopReason::Cancelled
1277+
&& let Some((tasks, background_jobs)) = structured
1278+
{
1279+
super::cancel_background_jobs(tasks, background_jobs).await;
1280+
let _ = super::settle_background_jobs(tasks, background_jobs).await?;
12971281
}
1298-
.await;
1282+
let _ = integration.flush_session_updates(session_id).await;
12991283
integration.finish_prompt(session_id);
1300-
handle.stop_injection_turn();
1301-
busy.store(false, Ordering::Release);
1302-
result
1284+
send_state(
1285+
sink,
1286+
session_id,
1287+
wire::StateUpdate::Idle(wire::IdleStateUpdate::new().stop_reason(stop_reason)),
1288+
)
13031289
}
13041290

1305-
async fn terminalize_running_error(
1291+
async fn drive_autonomous<S: ModelSession + Send + 'static>(
13061292
session_id: &wire::SessionId,
13071293
integration: &AcpIntegration,
13081294
handle: &AcpSessionHandle,
1295+
busy: &AtomicBool,
1296+
driver: &mut LoopDriver<S>,
13091297
sink: &impl AcpSessionUpdateSink,
1310-
error: &AcpRuntimeError,
13111298
) -> Result<(), AcpRuntimeError> {
1312-
terminalize_error(
1299+
if claim_prompt(busy).is_err() {
1300+
return Ok(());
1301+
}
1302+
handle.prepare_injection_turn();
1303+
integration.finish_prompt(session_id);
1304+
let cancellation_generation = handle.cancellation_handle().generation();
1305+
handle.start_injection_turn();
1306+
let result = run_active_turn(
13131307
session_id,
13141308
integration,
13151309
handle,
1310+
driver,
13161311
sink,
1317-
error,
1318-
Some(wire::IdleStateUpdate::new().stop_reason(error_stop_reason())),
1312+
cancellation_generation,
1313+
None,
13191314
)
1320-
.await
1315+
.await;
1316+
integration.finish_prompt(session_id);
1317+
handle.stop_injection_turn();
1318+
busy.store(false, Ordering::Release);
1319+
result
13211320
}
13221321

1323-
async fn terminalize_error(
1322+
async fn terminalize_running_error(
13241323
session_id: &wire::SessionId,
13251324
integration: &AcpIntegration,
13261325
handle: &AcpSessionHandle,
13271326
sink: &impl AcpSessionUpdateSink,
13281327
error: &AcpRuntimeError,
1329-
idle: Option<wire::IdleStateUpdate>,
13301328
) -> Result<(), AcpRuntimeError> {
13311329
handle.stop_injection_turn();
13321330
let _ = integration.flush_session_updates(session_id).await;
13331331
integration.finish_prompt(session_id);
13341332

13351333
let diagnostic_result = sink.update(error_diagnostic_notification(session_id, error));
1336-
if let Some(idle) = idle {
1337-
let idle_result = send_state(sink, session_id, wire::StateUpdate::Idle(idle));
1338-
diagnostic_result.and(idle_result)
1339-
} else {
1340-
diagnostic_result
1341-
}
1334+
let idle_result = send_state(
1335+
sink,
1336+
session_id,
1337+
wire::StateUpdate::Idle(wire::IdleStateUpdate::new().stop_reason(error_stop_reason())),
1338+
);
1339+
diagnostic_result.and(idle_result)
13421340
}
13431341

13441342
fn error_diagnostic_notification(
@@ -1912,6 +1910,31 @@ mod tests {
19121910
}
19131911
}
19141912

1913+
fn assert_running_then_idle(
1914+
updates: &[wire::UpdateSessionNotification],
1915+
stop_reason: wire::StopReason,
1916+
) {
1917+
assert!(matches!(
1918+
updates.first().map(|update| &update.update),
1919+
Some(wire::SessionUpdate::StateUpdate(
1920+
wire::StateUpdate::Running(_)
1921+
))
1922+
));
1923+
assert!(matches!(
1924+
updates.last().map(|update| &update.update),
1925+
Some(wire::SessionUpdate::StateUpdate(wire::StateUpdate::Idle(
1926+
idle
1927+
))) if idle.stop_reason.as_ref() == Some(&stop_reason)
1928+
));
1929+
assert_eq!(
1930+
updates
1931+
.iter()
1932+
.filter(|update| matches!(update.update, wire::SessionUpdate::StateUpdate(_)))
1933+
.count(),
1934+
2
1935+
);
1936+
}
1937+
19151938
#[test]
19161939
fn observer_reports_usage_with_a_known_context_window() {
19171940
let recording = RecordingSink::default();
@@ -2783,7 +2806,7 @@ mod tests {
27832806
}
27842807

27852808
#[tokio::test]
2786-
async fn autonomous_no_work_emits_no_state_update() {
2809+
async fn autonomous_no_work_emits_running_then_idle() {
27872810
let integration = AcpIntegration::default();
27882811
let sink = RecordingSink::default();
27892812
let session_id = wire::SessionId::new("autonomous-no-work");
@@ -2812,11 +2835,11 @@ mod tests {
28122835
assert_eq!(turns.load(Ordering::Relaxed), 0);
28132836
assert!(!busy.load(Ordering::Relaxed));
28142837
assert_eq!(sink.flushes.load(Ordering::Relaxed), 1);
2815-
assert!(sink.updates.lock().unwrap().is_empty());
2838+
assert_running_then_idle(&sink.updates.lock().unwrap(), wire::StopReason::EndTurn);
28162839
}
28172840

28182841
#[tokio::test]
2819-
async fn autonomous_content_emits_no_state_update() {
2842+
async fn autonomous_content_emits_running_then_idle() {
28202843
let integration = AcpIntegration::default();
28212844
let recording = RecordingSink::default();
28222845
let sink = ResponseReplacementSink::new(recording.clone());
@@ -2869,15 +2892,11 @@ mod tests {
28692892
.unwrap()
28702893
.contains("autonomous content")
28712894
}));
2872-
assert!(
2873-
!updates
2874-
.iter()
2875-
.any(|update| matches!(update.update, wire::SessionUpdate::StateUpdate(_)))
2876-
);
2895+
assert_running_then_idle(&updates, wire::StopReason::EndTurn);
28772896
}
28782897

28792898
#[tokio::test]
2880-
async fn autonomous_provider_error_emits_diagnostic_without_state_update() {
2899+
async fn autonomous_provider_error_emits_running_error_idle() {
28812900
let integration = AcpIntegration::default();
28822901
let sink = RecordingSink::default();
28832902
let session_id = wire::SessionId::new("autonomous-provider-error");
@@ -2910,21 +2929,17 @@ mod tests {
29102929
assert!(!busy.load(Ordering::Relaxed));
29112930
assert_eq!(sink.flushes.load(Ordering::Relaxed), 1);
29122931
let updates = sink.updates.lock().unwrap();
2913-
assert_eq!(updates.len(), 1);
2932+
assert_eq!(updates.len(), 3);
29142933
assert!(
2915-
serde_json::to_string(&updates[0].update)
2934+
serde_json::to_string(&updates[1].update)
29162935
.unwrap()
29172936
.contains("provider failed")
29182937
);
29192938
assert!(matches!(
2920-
updates[0].update,
2939+
updates[1].update,
29212940
wire::SessionUpdate::AgentMessage(_)
29222941
));
2923-
assert!(
2924-
!updates
2925-
.iter()
2926-
.any(|update| matches!(update.update, wire::SessionUpdate::StateUpdate(_)))
2927-
);
2942+
assert_running_then_idle(&updates, error_stop_reason());
29282943
}
29292944

29302945
#[tokio::test]
@@ -2964,11 +2979,11 @@ mod tests {
29642979
assert_eq!(turns.load(Ordering::Relaxed), 1);
29652980
assert!(!busy.load(Ordering::Relaxed));
29662981
assert_eq!(sink.flushes.load(Ordering::Relaxed), 1);
2967-
assert!(sink.updates.lock().unwrap().is_empty());
2982+
assert_running_then_idle(&sink.updates.lock().unwrap(), wire::StopReason::Cancelled);
29682983
}
29692984

29702985
#[tokio::test]
2971-
async fn autonomous_finish_error_emits_diagnostic_without_state_update() {
2986+
async fn autonomous_finish_error_emits_running_error_idle() {
29722987
let integration = AcpIntegration::default();
29732988
let sink = RecordingSink::default();
29742989
let session_id = wire::SessionId::new("autonomous-error");
@@ -3001,21 +3016,17 @@ mod tests {
30013016
assert!(!busy.load(Ordering::Relaxed));
30023017
assert_eq!(sink.flushes.load(Ordering::Relaxed), 1);
30033018
let updates = sink.updates.lock().unwrap();
3004-
assert_eq!(updates.len(), 1);
3019+
assert_eq!(updates.len(), 3);
30053020
assert!(matches!(
3006-
updates[0].update,
3021+
updates[1].update,
30073022
wire::SessionUpdate::AgentMessage(_)
30083023
));
30093024
assert!(
3010-
serde_json::to_string(&updates[0].update)
3025+
serde_json::to_string(&updates[1].update)
30113026
.unwrap()
30123027
.contains("loop error: model turn failed")
30133028
);
3014-
assert!(
3015-
!updates
3016-
.iter()
3017-
.any(|update| matches!(update.update, wire::SessionUpdate::StateUpdate(_)))
3018-
);
3029+
assert_running_then_idle(&updates, error_stop_reason());
30193030
}
30203031

30213032
#[test]

0 commit comments

Comments
 (0)