mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-19 11:04:06 +00:00
agent: Upgrade ACP SDK to 3.3 (#65414)
- Advertise the implemented, now-stable session notice and compaction capabilities without the ACP beta gate. - Handle structured error/custom stop reasons. - Honor the stable compaction chunk contract: a first-seen chunk creates an in-progress entry, later chunks append in receive order regardless of existing status, and chunk metadata stays delivery-scoped. Existing status, error, record metadata and timeline positions are preserved. Release Notes: - Enable session notices and compaction for ACP agents.
This commit is contained in:
parent
7fb155e9a2
commit
687d662a0b
8 changed files with 334 additions and 173 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
|
@ -327,9 +327,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "3.2.0"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0d09e5bd7214b0aa5f0fd3f2eefae4a8c0069fd6a4c1ae4e733d3691e36ec19"
|
||||
checksum = "02898d848a20eb09b8a392e85bb3a4c79dba01d369758185d102b001362a60da"
|
||||
dependencies = [
|
||||
"agent-client-protocol-derive",
|
||||
"agent-client-protocol-schema",
|
||||
|
|
@ -345,9 +345,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-derive"
|
||||
version = "3.2.0"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b4e3a708345c6c952bd0091abb58f3169e839ee9c7023285a7e22ab96ba4f3"
|
||||
checksum = "26b57a6a7fae5ebd4368712bfcd0a366a20e1370ac3f3fcd83379b37637501c9"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
|
|
@ -355,9 +355,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-schema"
|
||||
version = "1.10.2"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c3ffd489b0e2f96a134d7966c2f43d874d91299e951b66093b91b54b00b7f8f"
|
||||
checksum = "3d9a84a76a4f44721e0d8dabcd85b219893c43d6171a4a033d84844d85880823"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more",
|
||||
|
|
@ -382,7 +382,6 @@ dependencies = [
|
|||
"client",
|
||||
"collections",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.34",
|
||||
"gpui",
|
||||
|
|
|
|||
|
|
@ -527,7 +527,7 @@ accesskit = { version = "0.24.0", features = ["enumn"] }
|
|||
accesskit_macos = "0.26.0"
|
||||
accesskit_unix = "0.22"
|
||||
accesskit_windows = "0.34"
|
||||
agent-client-protocol = { version = "=3.2.0", features = ["schemars", "unstable", "unstable_protocol_v2"] }
|
||||
agent-client-protocol = { version = "=3.3.0", features = ["schemars", "unstable", "unstable_protocol_v2"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "4c129667ce56611becdc82de6e28218c80e2e88f" }
|
||||
any_vec = "0.14"
|
||||
|
|
|
|||
|
|
@ -4226,7 +4226,11 @@ impl AcpThread {
|
|||
}
|
||||
self.had_error = matches!(
|
||||
stop_reason,
|
||||
Some(acp_v2::StopReason::MaxTokens | acp_v2::StopReason::Refusal)
|
||||
Some(
|
||||
acp_v2::StopReason::MaxTokens
|
||||
| acp_v2::StopReason::Refusal
|
||||
| acp_v2::StopReason::Error(_)
|
||||
)
|
||||
);
|
||||
// Reported refusal does not authorize deleting agent-owned history.
|
||||
cx.emit(AcpThreadEvent::Stopped {
|
||||
|
|
@ -5159,8 +5163,7 @@ impl AcpThread {
|
|||
.rev()
|
||||
.find_map(|(entry_index, entry)| match entry {
|
||||
AgentThreadEntry::ContextCompaction(compaction)
|
||||
if compaction.id.0 == chunk.compaction_id.0
|
||||
&& compaction.is_in_progress() =>
|
||||
if compaction.id.0 == chunk.compaction_id.0 =>
|
||||
{
|
||||
Some((entry_index, compaction))
|
||||
}
|
||||
|
|
@ -5170,7 +5173,17 @@ impl AcpThread {
|
|||
// Chunk metadata is delivery-scoped, not a patch to the compaction record.
|
||||
compaction.append_summary(chunk.content, &language_registry, cx);
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(entry_index));
|
||||
return;
|
||||
}
|
||||
let mut compaction = ContextCompaction {
|
||||
id: ContextCompactionId(chunk.compaction_id.0),
|
||||
status: ContextCompactionStatus::InProgress,
|
||||
error: None,
|
||||
summary: MessageContent::default(),
|
||||
meta: None,
|
||||
};
|
||||
compaction.append_summary(chunk.content, &language_registry, cx);
|
||||
self.push_entry(AgentThreadEntry::ContextCompaction(compaction), cx);
|
||||
}
|
||||
|
||||
pub fn update_context_compaction(
|
||||
|
|
@ -7624,7 +7637,6 @@ fn update_markdown_in_place(markdown: &Entity<Markdown>, text: &str, cx: &mut Ap
|
|||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::anyhow;
|
||||
use feature_flags::FeatureFlag as _;
|
||||
use futures::stream::StreamExt as _;
|
||||
use futures::{channel::mpsc, future::LocalBoxFuture, select};
|
||||
use gpui::UpdateGlobal as _;
|
||||
|
|
@ -8094,25 +8106,6 @@ mod tests {
|
|||
request.await.expect("turn completes");
|
||||
}
|
||||
|
||||
fn enable_acp_beta(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(false, vec![AcpBetaFeatureFlag::NAME.to_string()]);
|
||||
});
|
||||
}
|
||||
|
||||
fn set_acp_beta_override(value: &str, cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |content| {
|
||||
content
|
||||
.feature_flags
|
||||
.get_or_insert_default()
|
||||
.insert(AcpBetaFeatureFlag::NAME.to_string(), value.to_string());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn message_test_image() -> acp_v2::ContentBlock {
|
||||
acp_v2::ContentBlock::Image(acp_v2::ImageContent::new(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
|
||||
|
|
@ -11062,6 +11055,87 @@ mod tests {
|
|||
(2, Some(acp_v2::StopReason::MaxTokens)),
|
||||
]
|
||||
);
|
||||
for (wire, expected_had_error) in [
|
||||
(
|
||||
json!({
|
||||
"state": "idle",
|
||||
"stopReason": "error",
|
||||
"error": {"code": -32000, "message": "work failed", "data": {"detail": [1, true]}}
|
||||
}),
|
||||
true,
|
||||
),
|
||||
(json!({"state": "idle", "stopReason": "error"}), true),
|
||||
(
|
||||
json!({"state": "idle", "stopReason": "error", "error": null}),
|
||||
true,
|
||||
),
|
||||
(
|
||||
json!({"state": "idle", "stopReason": "error", "error": {"message": 42}}),
|
||||
true,
|
||||
),
|
||||
(
|
||||
json!({"state": "idle", "stopReason": "_custom", "detail": {"nested": [null, true]}}),
|
||||
false,
|
||||
),
|
||||
] {
|
||||
let idle: acp_v2::StateUpdate =
|
||||
serde_json::from_value(wire.clone()).expect("idle stop payload");
|
||||
let acp_v2::StateUpdate::Idle(details) = &idle else {
|
||||
panic!("expected idle");
|
||||
};
|
||||
let stop_reason = details.stop_reason.clone().expect("stop reason retained");
|
||||
if let acp_v2::StopReason::Error(error) = &stop_reason {
|
||||
assert_eq!(
|
||||
error
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|error| serde_json::to_value(error).expect("error")),
|
||||
wire.get("error")
|
||||
.filter(|error| error.get("code").is_some())
|
||||
.cloned(),
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
stop_reason,
|
||||
acp_v2::StopReason::Other(acp_v2::OtherStopReason::new(
|
||||
"_custom",
|
||||
std::collections::BTreeMap::from([(
|
||||
"detail".into(),
|
||||
json!({"nested": [null, true]}),
|
||||
)]),
|
||||
)),
|
||||
);
|
||||
}
|
||||
let previous_stops = stopped.borrow().len();
|
||||
let generation = thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.update_session_state(
|
||||
acp_v2::StateUpdate::Running(acp_v2::RunningStateUpdate::new()),
|
||||
cx,
|
||||
)
|
||||
.expect("running");
|
||||
assert!(!thread.had_error());
|
||||
thread.update_session_state(idle.clone(), cx).expect("idle");
|
||||
assert_eq!(thread.foreground_state(), &idle);
|
||||
assert_eq!(thread.had_error(), expected_had_error);
|
||||
assert_eq!(thread.entries().len(), 3);
|
||||
assert!(matches!(
|
||||
thread.submission(late_id).expect("accepted receipt").state,
|
||||
SubmissionState::Accepted { echoed: true, .. }
|
||||
));
|
||||
assert!(thread.recoverable_submissions().next().is_none());
|
||||
thread
|
||||
.update_session_state(idle.clone(), cx)
|
||||
.expect("duplicate idle");
|
||||
thread.activity_generation()
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(stopped.borrow().len(), previous_stops + 1);
|
||||
assert_eq!(
|
||||
stopped.borrow().last(),
|
||||
Some(&(generation, Some(stop_reason)))
|
||||
);
|
||||
}
|
||||
assert_eq!(*legacy_errors.borrow(), 0);
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.forget_submission(late_id, cx);
|
||||
|
|
@ -12843,6 +12917,28 @@ mod tests {
|
|||
.expect("failed to create ACP thread");
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
for text in ["first ", "second"] {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp_v1::SessionUpdate::CompactionSummaryChunk(
|
||||
acp_v1::CompactionSummaryChunk::new(
|
||||
"compaction",
|
||||
acp_v1::ContentBlock::Text(acp_v1::TextContent::new(text)),
|
||||
)
|
||||
.meta(acp_v1::Meta::from_iter([("delivery".into(), true.into())])),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.expect("summary chunks can precede the first status update");
|
||||
}
|
||||
let [AgentThreadEntry::ContextCompaction(compaction)] = thread.entries.as_slice()
|
||||
else {
|
||||
panic!("chunk-first compaction must create one timeline entry");
|
||||
};
|
||||
assert_eq!(compaction.status, ContextCompactionStatus::InProgress);
|
||||
assert!(compaction.meta.is_none());
|
||||
assert!(thread.to_markdown(cx).contains("first second"));
|
||||
|
||||
for summary in ["retained context", "replacement summary"] {
|
||||
thread
|
||||
.handle_session_update(
|
||||
|
|
@ -13260,7 +13356,7 @@ mod tests {
|
|||
);
|
||||
});
|
||||
|
||||
let source = vec![
|
||||
let mut source = vec![
|
||||
acp_v2::ContentBlock::Text(acp_v2::TextContent::new("replacement ").meta(
|
||||
acp_v2::Meta::from_iter([(
|
||||
"content".into(),
|
||||
|
|
@ -13293,11 +13389,13 @@ mod tests {
|
|||
),
|
||||
cx,
|
||||
);
|
||||
let late_text = acp_v2::ContentBlock::from("late text");
|
||||
thread.append_context_compaction_summary(
|
||||
acp_v2::CompactionSummaryChunk::new("first", "ignored late text".into())
|
||||
acp_v2::CompactionSummaryChunk::new("first", late_text.clone())
|
||||
.meta(acp_v2::Meta::from_iter([("delivery".into(), true.into())])),
|
||||
cx,
|
||||
);
|
||||
source.push(late_text);
|
||||
let Some(AgentThreadEntry::ContextCompaction(first)) = thread.entries.first() else {
|
||||
panic!("the original timeline position must remain");
|
||||
};
|
||||
|
|
@ -13319,6 +13417,7 @@ mod tests {
|
|||
Some(0),
|
||||
Some(0),
|
||||
Some(0),
|
||||
Some(0),
|
||||
Some(0)
|
||||
]
|
||||
);
|
||||
|
|
@ -17059,12 +17158,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_elicitation_is_available_without_acp_beta_flag(cx: &mut TestAppContext) {
|
||||
async fn test_elicitation_is_available(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(false, vec![]);
|
||||
});
|
||||
set_acp_beta_override("off", cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
|
||||
|
|
@ -17093,7 +17188,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_form_elicitation_accepts_response(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
let tool_call_id = acp_v2::ToolCallId::new("tool-1");
|
||||
|
|
@ -17155,7 +17249,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_url_elicitation_can_be_completed(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
let url_elicitation_id = acp_v2::ElicitationId::new("url-1");
|
||||
|
|
@ -17220,7 +17313,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_idle_cancel_cancels_accepted_url_elicitation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
let url_elicitation_id = acp_v2::ElicitationId::new("url-1");
|
||||
|
|
@ -17284,7 +17376,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_cancel_accepted_url_elicitation_marks_canceled(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
let url_elicitation_id = acp_v2::ElicitationId::new("url-1");
|
||||
|
|
@ -17356,7 +17447,6 @@ mod tests {
|
|||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let prompt_count = Rc::new(RefCell::new(0usize));
|
||||
|
|
@ -17461,7 +17551,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_request_scoped_elicitation_store_accepts_response(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
|
||||
let request: acp_v2::CreateElicitationRequest = serde_json::from_value(serde_json::json!({
|
||||
"mode": "form",
|
||||
|
|
@ -17537,7 +17626,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_request_elicitation_store_ignores_duplicate_response(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
|
||||
let responded_ids = Rc::new(RefCell::new(Vec::new()));
|
||||
let _subscription = cx.update(|cx| {
|
||||
|
|
@ -17608,7 +17696,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_cancel_session_elicitation_by_id_resolves_cancel(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
|
||||
|
|
@ -17646,7 +17733,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_cancel_pending_session_elicitation_resolves_cancel(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
|
||||
|
|
@ -17710,7 +17796,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_prompt_error_cancels_pending_session_elicitation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let elicitation_action = Rc::new(RefCell::new(None));
|
||||
|
|
@ -17765,7 +17850,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_max_tokens_cancels_pending_session_elicitation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let elicitation_action = Rc::new(RefCell::new(None));
|
||||
|
|
@ -17820,7 +17904,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_cancel_request_scoped_elicitation_resolves_cancel(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
|
||||
|
||||
let (elicitation_id, response_task) = store.update(cx, |store, cx| {
|
||||
|
|
@ -17904,7 +17987,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_request_elicitation_store_cancel_all_resolves_cancel(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
|
||||
|
||||
let response_task = store.update(cx, |store, cx| {
|
||||
|
|
@ -17937,7 +18019,6 @@ mod tests {
|
|||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
|
||||
|
||||
let first_response_task = store.update(cx, |store, cx| {
|
||||
|
|
@ -18001,7 +18082,6 @@ mod tests {
|
|||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
|
||||
let url_elicitation_id = acp_v2::ElicitationId::new("url-1");
|
||||
|
||||
|
|
@ -18113,7 +18193,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_request_url_elicitation_store_can_be_completed(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
|
||||
let url_elicitation_id = acp_v2::ElicitationId::new("url-1");
|
||||
|
||||
|
|
@ -18184,7 +18263,6 @@ mod tests {
|
|||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
|
||||
let url_elicitation_id = acp_v2::ElicitationId::new("url-1");
|
||||
let responded_ids = Rc::new(RefCell::new(Vec::new()));
|
||||
|
|
@ -18270,7 +18348,6 @@ mod tests {
|
|||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
|
||||
|
|
@ -18318,7 +18395,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_session_elicitation_ignores_duplicate_response(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
|
||||
|
|
@ -18372,7 +18448,6 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_url_elicitation_rejects_non_browser_urls(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
enable_acp_beta(cx);
|
||||
let thread = new_test_thread(cx).await;
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
|
||||
|
||||
|
|
|
|||
|
|
@ -2487,6 +2487,12 @@ fn legacy_native_completion(stop_reason: acp_v2::StopReason) -> Result<acp_v1::P
|
|||
acp_v2::StopReason::MaxTurnRequests => acp_v1::StopReason::MaxTurnRequests,
|
||||
acp_v2::StopReason::Refusal => acp_v1::StopReason::Refusal,
|
||||
acp_v2::StopReason::Cancelled => acp_v1::StopReason::Cancelled,
|
||||
acp_v2::StopReason::Error(reason) => {
|
||||
return Err(match reason.error {
|
||||
Some(error) => anyhow::Error::new(*error),
|
||||
None => anyhow!("Native agent stopped with an error"),
|
||||
});
|
||||
}
|
||||
_ => anyhow::bail!("Native agent returned an unsupported stop reason"),
|
||||
};
|
||||
Ok(acp_v1::PromptResponse::new(stop_reason))
|
||||
|
|
@ -4001,9 +4007,21 @@ mod internal_tests {
|
|||
assert_eq!(legacy_native_completion(native)?.stop_reason, legacy);
|
||||
}
|
||||
assert!(
|
||||
legacy_native_completion(acp_v2::StopReason::Other("_future".into())).is_err(),
|
||||
legacy_native_completion(acp_v2::StopReason::Other(acp_v2::OtherStopReason::new(
|
||||
"_future",
|
||||
Default::default(),
|
||||
)))
|
||||
.is_err(),
|
||||
"an unsupported reason must not masquerade as successful completion"
|
||||
);
|
||||
let error = legacy_native_completion(acp_v2::ErrorStopReason::new().into())
|
||||
.expect_err("an error without details is not successful completion");
|
||||
assert_eq!(error.to_string(), "Native agent stopped with an error");
|
||||
let details = acp_v2::Error::auth_required().data(json!({"hint": "sign in"}));
|
||||
let error =
|
||||
legacy_native_completion(acp_v2::ErrorStopReason::new().error(details.clone()).into())
|
||||
.expect_err("reported error details propagate");
|
||||
assert_eq!(error.downcast_ref::<acp_v2::Error>(), Some(&details));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -4297,10 +4315,6 @@ mod internal_tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_compact_prompt_routes_to_manual_compaction(cx: &mut TestAppContext) {
|
||||
use feature_flags::{
|
||||
AcpBetaFeatureFlag, FeatureFlag as _, FeatureFlagAppExt as _, FeatureFlagsSettings,
|
||||
};
|
||||
|
||||
let fake = init_test(cx);
|
||||
let (connection, agent, project, acp_thread) = setup_native_agent_session(cx).await;
|
||||
let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
|
||||
|
|
@ -4309,16 +4323,6 @@ mod internal_tests {
|
|||
let old_message_id = ClientUserMessageId::new();
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(true, Vec::new());
|
||||
FeatureFlagsSettings::override_global(
|
||||
FeatureFlagsSettings {
|
||||
overrides: HashMap::from_iter([(
|
||||
AcpBetaFeatureFlag::NAME.into(),
|
||||
"off".into(),
|
||||
)]),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_model(model.clone(), cx);
|
||||
|
|
@ -4334,7 +4338,6 @@ mod internal_tests {
|
|||
|
||||
let compact_message_id = ClientUserMessageId::new();
|
||||
let prompt_task = cx.update(|cx| {
|
||||
assert!(!cx.has_flag::<AcpBetaFeatureFlag>());
|
||||
acp_thread::AgentSessionClientUserMessageIds::prompt(
|
||||
connection.as_ref(),
|
||||
compact_message_id,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ env_logger = { workspace = true, optional = true }
|
|||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui_tokio = { workspace = true, optional = true }
|
||||
http_client.workspace = true
|
||||
indoc = { workspace = true, optional = true }
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ use agent_client_protocol::{
|
|||
use anyhow::anyhow;
|
||||
use async_channel;
|
||||
use collections::{HashMap, HashSet};
|
||||
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
|
||||
use futures::channel::mpsc;
|
||||
use futures::future::Shared;
|
||||
use futures::{Future, FutureExt as _, StreamExt as _};
|
||||
|
|
@ -762,10 +761,7 @@ pub fn v2_terminal_client_builder(
|
|||
)
|
||||
}
|
||||
|
||||
fn client_capabilities_for_agent(
|
||||
agent_id: &AgentId,
|
||||
beta_features_enabled: bool,
|
||||
) -> acp::ClientCapabilities {
|
||||
fn client_capabilities_for_agent(agent_id: &AgentId) -> acp::ClientCapabilities {
|
||||
let mut meta = acp::Meta::from_iter([
|
||||
("terminal_output".into(), true.into()),
|
||||
("terminal-auth".into(), true.into()),
|
||||
|
|
@ -775,15 +771,13 @@ fn client_capabilities_for_agent(
|
|||
meta.insert(PARAMETERIZED_MODEL_PICKER_META_KEY.into(), true.into());
|
||||
}
|
||||
|
||||
let mut session_capabilities = acp::ClientSessionCapabilities::new().config_options(
|
||||
acp::SessionConfigOptionsCapabilities::new()
|
||||
.boolean(acp::BooleanConfigOptionCapabilities::new()),
|
||||
);
|
||||
if beta_features_enabled {
|
||||
session_capabilities = session_capabilities
|
||||
.compaction(acp::CompactionCapabilities::new())
|
||||
.notices(acp::NoticeCapabilities::new());
|
||||
}
|
||||
let session_capabilities = acp::ClientSessionCapabilities::new()
|
||||
.config_options(
|
||||
acp::SessionConfigOptionsCapabilities::new()
|
||||
.boolean(acp::BooleanConfigOptionCapabilities::new()),
|
||||
)
|
||||
.compaction(acp::CompactionCapabilities::new())
|
||||
.notices(acp::NoticeCapabilities::new());
|
||||
|
||||
acp::ClientCapabilities::new()
|
||||
.fs(acp::FileSystemCapabilities::new()
|
||||
|
|
@ -923,14 +917,10 @@ impl AcpConnection {
|
|||
}
|
||||
};
|
||||
|
||||
let beta_features_enabled = cx.update(|cx| cx.has_flag::<AcpBetaFeatureFlag>());
|
||||
let initialize_response = connection
|
||||
.send_request(
|
||||
acp::InitializeRequest::new(ProtocolVersion::V1)
|
||||
.client_capabilities(client_capabilities_for_agent(
|
||||
&agent_id,
|
||||
beta_features_enabled,
|
||||
))
|
||||
.client_capabilities(client_capabilities_for_agent(&agent_id))
|
||||
.client_info(
|
||||
acp::Implementation::new("zed", version)
|
||||
.title(release_channel.map(ToOwned::to_owned)),
|
||||
|
|
@ -2762,7 +2752,6 @@ mod tests {
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use super::*;
|
||||
use feature_flags::{AcpBetaFeatureFlag, FeatureFlag as _};
|
||||
use settings::Settings as _;
|
||||
|
||||
#[test]
|
||||
|
|
@ -3384,21 +3373,16 @@ mod tests {
|
|||
assert!(harness.received.lock().expect("receive mutex").is_empty());
|
||||
}
|
||||
|
||||
fn init_feature_flags_test(cx: &mut gpui::TestAppContext) {
|
||||
fn init_settings_test(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let mut settings_store = SettingsStore::test(cx);
|
||||
settings_store.register_setting::<feature_flags::FeatureFlagsSettings>();
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
cx.update_flags(false, vec![]);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn client_capabilities_include_elicitation_without_acp_beta(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_feature_flags_test(cx);
|
||||
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"), false);
|
||||
#[test]
|
||||
fn client_capabilities_include_elicitation() {
|
||||
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"));
|
||||
let elicitation = capabilities
|
||||
.elicitation
|
||||
.expect("elicitation should always be advertised");
|
||||
|
|
@ -3411,10 +3395,7 @@ mod tests {
|
|||
async fn request_scoped_elicitation_during_auth_uses_connection_store(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_feature_flags_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(false, vec![AcpBetaFeatureFlag::NAME.to_string()]);
|
||||
});
|
||||
init_settings_test(cx);
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/", serde_json::json!({ "a": {} })).await;
|
||||
|
|
@ -3520,10 +3501,7 @@ mod tests {
|
|||
async fn request_scoped_url_elicitation_completion_before_consent_is_ignored(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_feature_flags_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(false, vec![AcpBetaFeatureFlag::NAME.to_string()]);
|
||||
});
|
||||
init_settings_test(cx);
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/", serde_json::json!({ "a": {} })).await;
|
||||
|
|
@ -3612,10 +3590,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn request_scoped_elicitation_ignores_open_sessions(cx: &mut gpui::TestAppContext) {
|
||||
init_feature_flags_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(false, vec![AcpBetaFeatureFlag::NAME.to_string()]);
|
||||
});
|
||||
init_settings_test(cx);
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/", serde_json::json!({ "a": {} })).await;
|
||||
|
|
@ -3720,7 +3695,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn cursor_client_capabilities_include_parameterized_model_picker_meta() {
|
||||
let capabilities = client_capabilities_for_agent(&AgentId::new(CURSOR_ID), false);
|
||||
let capabilities = client_capabilities_for_agent(&AgentId::new(CURSOR_ID));
|
||||
let meta = capabilities
|
||||
.meta
|
||||
.expect("expected client capabilities meta");
|
||||
|
|
@ -3735,7 +3710,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn non_cursor_client_capabilities_do_not_include_parameterized_model_picker_meta() {
|
||||
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"), false);
|
||||
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"));
|
||||
let meta = capabilities
|
||||
.meta
|
||||
.expect("expected client capabilities meta");
|
||||
|
|
@ -3745,7 +3720,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn client_capabilities_include_boolean_config_options() {
|
||||
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"), false);
|
||||
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"));
|
||||
|
||||
assert!(
|
||||
capabilities
|
||||
|
|
@ -3757,30 +3732,25 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn client_capabilities_gate_compaction_and_notices_with_acp_beta() {
|
||||
for (beta_enabled, expected_capability) in
|
||||
[(false, None), (true, Some(serde_json::json!({})))]
|
||||
{
|
||||
let capabilities =
|
||||
client_capabilities_for_agent(&AgentId::new("codex-acp"), beta_enabled);
|
||||
let capabilities =
|
||||
serde_json::to_value(capabilities).expect("client capabilities should serialize");
|
||||
let session = capabilities
|
||||
.get("session")
|
||||
.expect("session capabilities should be advertised");
|
||||
for capability in ["compaction", "notices"] {
|
||||
assert_eq!(
|
||||
session.get(capability),
|
||||
expected_capability.as_ref(),
|
||||
"{capability} with ACP beta enabled: {beta_enabled}"
|
||||
);
|
||||
}
|
||||
fn client_capabilities_include_compaction_and_notices() {
|
||||
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"));
|
||||
let capabilities =
|
||||
serde_json::to_value(capabilities).expect("client capabilities should serialize");
|
||||
let session = capabilities
|
||||
.get("session")
|
||||
.expect("session capabilities should be advertised");
|
||||
for capability in ["compaction", "notices"] {
|
||||
assert_eq!(
|
||||
session.get(capability),
|
||||
Some(&serde_json::json!({})),
|
||||
"{capability} should be advertised"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn connection_routes_terminal_auth_without_acp_beta(cx: &mut gpui::TestAppContext) {
|
||||
init_feature_flags_test(cx);
|
||||
async fn connection_routes_terminal_auth(cx: &mut gpui::TestAppContext) {
|
||||
init_settings_test(cx);
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/", serde_json::json!({ "project": {} }))
|
||||
|
|
@ -3847,21 +3817,8 @@ mod tests {
|
|||
.auth_methods = methods;
|
||||
|
||||
let terminal_task = cx
|
||||
.update(|cx| {
|
||||
cx.update_flags(true, Vec::new());
|
||||
feature_flags::FeatureFlagsSettings::override_global(
|
||||
feature_flags::FeatureFlagsSettings {
|
||||
overrides: HashMap::from_iter([(
|
||||
AcpBetaFeatureFlag::NAME.into(),
|
||||
"off".into(),
|
||||
)]),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
assert!(!cx.has_flag::<AcpBetaFeatureFlag>());
|
||||
harness.connection.terminal_auth_task(&method_id, cx)
|
||||
})
|
||||
.expect("first-class terminal auth should be routed without ACP beta");
|
||||
.update(|cx| harness.connection.terminal_auth_task(&method_id, cx))
|
||||
.expect("first-class terminal auth should be routed");
|
||||
let terminal_task = terminal_task
|
||||
.await
|
||||
.expect("first-class routing should resolve the test agent's external command");
|
||||
|
|
@ -5510,7 +5467,7 @@ exit 7
|
|||
let response = client_conn
|
||||
.send_request(
|
||||
acp::InitializeRequest::new(ProtocolVersion::V1).client_capabilities(
|
||||
client_capabilities_for_agent(&AgentId::new("fake-agent"), true),
|
||||
client_capabilities_for_agent(&AgentId::new("fake-agent")),
|
||||
),
|
||||
)
|
||||
.block_task()
|
||||
|
|
@ -6632,6 +6589,10 @@ exit 7
|
|||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(acp::ContentBlock::Text(
|
||||
acp::TextContent::new(String::from("hi user")),
|
||||
))),
|
||||
acp::SessionUpdate::CompactionUpdate(acp::CompactionUpdate::new(
|
||||
"replayed-compaction",
|
||||
acp::CompactionStatus::Completed,
|
||||
)),
|
||||
];
|
||||
|
||||
let session_id = acp_v2::SessionId::new("session-replay");
|
||||
|
|
@ -6667,7 +6628,7 @@ exit 7
|
|||
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec!["user", "assistant"],
|
||||
vec!["user", "assistant", "compaction"],
|
||||
"replayed notifications should be applied to the thread"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,6 +172,10 @@ impl From<anyhow::Error> for ThreadError {
|
|||
&& acp_error.code == acp_v1::ErrorCode::AuthRequired
|
||||
{
|
||||
Self::AuthenticationRequired(acp_error.message.clone().into())
|
||||
} else if let Some(acp_error) = error.downcast_ref::<acp_v2::Error>()
|
||||
&& acp_error.code == acp_v2::ErrorCode::AuthRequired
|
||||
{
|
||||
Self::AuthenticationRequired(acp_error.message.clone().into())
|
||||
} else if let Some(lm_error) = error.downcast_ref::<LanguageModelCompletionError>() {
|
||||
use LanguageModelCompletionError::*;
|
||||
match lm_error {
|
||||
|
|
@ -238,7 +242,12 @@ impl From<anyhow::Error> for ThreadError {
|
|||
// Extract ACP error code if available
|
||||
let acp_error_code = error
|
||||
.downcast_ref::<acp_v1::Error>()
|
||||
.map(|acp_error| SharedString::from(acp_error.code.to_string()));
|
||||
.map(|acp_error| SharedString::from(acp_error.code.to_string()))
|
||||
.or_else(|| {
|
||||
error
|
||||
.downcast_ref::<acp_v2::Error>()
|
||||
.map(|acp_error| SharedString::from(i32::from(acp_error.code).to_string()))
|
||||
});
|
||||
|
||||
Self::Other {
|
||||
message,
|
||||
|
|
@ -1970,6 +1979,28 @@ impl ConversationView {
|
|||
);
|
||||
return;
|
||||
}
|
||||
if let Some(acp_v2::StopReason::Error(details)) = stop_reason
|
||||
&& thread.read(cx).uses_reported_activity()
|
||||
{
|
||||
if let Some(active) = self.root_thread_view() {
|
||||
let error = details
|
||||
.error
|
||||
.as_deref()
|
||||
.map(|error| anyhow::Error::new(error.clone()))
|
||||
.unwrap_or_else(|| anyhow!("Agent stopped because of an error"));
|
||||
active.update(cx, |active, cx| {
|
||||
active.message_queue.pause();
|
||||
active.handle_thread_error(error, cx);
|
||||
});
|
||||
}
|
||||
self.notify_with_sound(
|
||||
"Agent stopped because of an error",
|
||||
IconName::Warning,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let sent_queued_message = if let Some(active) = self.root_thread_view() {
|
||||
active.update(cx, |active, cx| {
|
||||
|
|
@ -3981,7 +4012,7 @@ pub(crate) mod tests {
|
|||
use agent_servers::FakeAcpAgentServer;
|
||||
use editor::MultiBufferOffset;
|
||||
use editor::actions::Paste;
|
||||
use feature_flags::{AcpBetaFeatureFlag, FeatureFlag as _, FeatureFlagAppExt as _};
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::FakeFs;
|
||||
use gpui::{ClipboardItem, EventEmitter, TestAppContext, VisualTestContext, point, size};
|
||||
use parking_lot::Mutex;
|
||||
|
|
@ -4100,9 +4131,6 @@ pub(crate) mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_drop_preserves_shared_pending_request_elicitations(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(true, vec![AcpBetaFeatureFlag::NAME.to_string()]);
|
||||
});
|
||||
|
||||
let response = Arc::new(Mutex::new(None));
|
||||
let server = ReleaseRequestElicitationServer {
|
||||
|
|
@ -4152,9 +4180,6 @@ pub(crate) mod tests {
|
|||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(true, vec![AcpBetaFeatureFlag::NAME.to_string()]);
|
||||
});
|
||||
|
||||
let response = Arc::new(Mutex::new(None));
|
||||
let server = ReleaseRequestElicitationServer {
|
||||
|
|
@ -4207,9 +4232,6 @@ pub(crate) mod tests {
|
|||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(true, vec![AcpBetaFeatureFlag::NAME.to_string()]);
|
||||
});
|
||||
|
||||
let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
|
||||
let response = Arc::new(Mutex::new(None));
|
||||
|
|
@ -5089,6 +5111,103 @@ pub(crate) mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reported_error_after_acceptance_pauses_queue(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let connection = StubAgentConnection::new().with_receipt_submissions(true);
|
||||
let (conversation_view, cx) =
|
||||
setup_conversation_view(StubAgentServer::new(connection.clone()), cx).await;
|
||||
add_to_workspace(conversation_view.clone(), cx);
|
||||
let thread_view = active_thread(&conversation_view, cx);
|
||||
let thread = thread_view.read_with(cx, |view, _| view.thread.clone());
|
||||
let finish_receipt = connection.defer_next_receipt_response();
|
||||
message_editor(&conversation_view, cx).update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("accepted work", window, cx);
|
||||
});
|
||||
thread_view.update_in(cx, |view, window, cx| view.send(window, cx));
|
||||
cx.run_until_parked();
|
||||
finish_receipt
|
||||
.send(Ok(acp_v2::PromptResponse::new("accepted-message")))
|
||||
.expect("pending receipt");
|
||||
cx.run_until_parked();
|
||||
let submission_id =
|
||||
thread_view.read_with(cx, |view, _| view.current_submission.expect("submission"));
|
||||
thread_view.update_in(cx, |view, window, cx| {
|
||||
view.add_to_queue(vec!["follow-up".into()], vec![], window, cx);
|
||||
});
|
||||
for (details, expected_message, expected_code) in [
|
||||
(
|
||||
Some(acp_v2::Error::new(
|
||||
acp_v2::ErrorCode::AuthRequired.into(),
|
||||
"Sign in again",
|
||||
)),
|
||||
"Sign in again",
|
||||
None,
|
||||
),
|
||||
(
|
||||
Some(acp_v2::Error::new(-32099, "Backend failed")),
|
||||
"Backend failed",
|
||||
Some("-32099"),
|
||||
),
|
||||
(None, "Agent stopped because of an error", None),
|
||||
] {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.update_session_state(
|
||||
acp_v2::StateUpdate::Running(acp_v2::RunningStateUpdate::new()),
|
||||
cx,
|
||||
)
|
||||
.expect("running");
|
||||
thread
|
||||
.update_session_state(
|
||||
acp_v2::StateUpdate::Idle(acp_v2::IdleStateUpdate::new().stop_reason(
|
||||
acp_v2::StopReason::Error(
|
||||
acp_v2::ErrorStopReason::new().error(details),
|
||||
),
|
||||
)),
|
||||
cx,
|
||||
)
|
||||
.expect("failed work");
|
||||
});
|
||||
cx.run_until_parked();
|
||||
thread_view.read_with(cx, |view, cx| {
|
||||
match view.thread_error.as_ref().expect("reported error") {
|
||||
ThreadError::AuthenticationRequired(message) => {
|
||||
assert_eq!(message.as_ref(), expected_message);
|
||||
}
|
||||
ThreadError::Other {
|
||||
message,
|
||||
acp_error_code,
|
||||
} => {
|
||||
assert!(message.contains(expected_message));
|
||||
assert_eq!(acp_error_code.as_deref(), expected_code);
|
||||
}
|
||||
error => panic!("unexpected error: {error:?}"),
|
||||
}
|
||||
assert_eq!(view.message_queue.len(), 1);
|
||||
assert!(view.message_queue.auto_send_candidate(false).is_none());
|
||||
assert!(view.turn_fields.turn_started_at.is_none());
|
||||
let thread = view.thread.read(cx);
|
||||
assert!(thread.had_error());
|
||||
assert!(thread.entries().is_empty());
|
||||
assert!(matches!(
|
||||
thread
|
||||
.submission(submission_id)
|
||||
.expect("accepted work")
|
||||
.state,
|
||||
acp_thread::SubmissionState::Accepted { echoed: false, .. }
|
||||
));
|
||||
assert_eq!(
|
||||
thread
|
||||
.recoverable_submissions()
|
||||
.map(|(id, _)| id)
|
||||
.collect::<Vec<_>>(),
|
||||
[submission_id],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_stale_send_result_preserves_new_prompt(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
|
@ -5627,9 +5746,6 @@ pub(crate) mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_thread_view_seeds_existing_elicitation_form_state(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(true, vec![AcpBetaFeatureFlag::NAME.to_string()]);
|
||||
});
|
||||
|
||||
let connection = PreloadedElicitationConnection::default();
|
||||
let elicitation_id = connection.elicitation_id.clone();
|
||||
|
|
|
|||
|
|
@ -1525,7 +1525,8 @@ impl ThreadView {
|
|||
Some(
|
||||
acp_v2::StopReason::MaxTokens
|
||||
| acp_v2::StopReason::MaxTurnRequests
|
||||
| acp_v2::StopReason::Refusal,
|
||||
| acp_v2::StopReason::Refusal
|
||||
| acp_v2::StopReason::Error(_),
|
||||
) => "failure",
|
||||
_ => "unknown",
|
||||
}
|
||||
|
|
@ -13742,12 +13743,19 @@ mod tests {
|
|||
use acp_v2::StopReason::*;
|
||||
for (reason, expected) in [
|
||||
(None, "unknown"),
|
||||
(Some(Other("_custom".into())), "unknown"),
|
||||
(
|
||||
Some(Other(acp_v2::OtherStopReason::new(
|
||||
"_custom",
|
||||
Default::default(),
|
||||
))),
|
||||
"unknown",
|
||||
),
|
||||
(Some(EndTurn), "success"),
|
||||
(Some(Cancelled), "cancelled"),
|
||||
(Some(Refusal), "failure"),
|
||||
(Some(MaxTokens), "failure"),
|
||||
(Some(MaxTurnRequests), "failure"),
|
||||
(Some(Error(acp_v2::ErrorStopReason::new())), "failure"),
|
||||
] {
|
||||
assert_eq!(
|
||||
ThreadView::activity_completion_status(reason.as_ref()),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue