Update to ACP SDK v0.8.0 (#44063)

Ben Brandt created

Uses the latest version of the SDK + schema crate. A bit painful because
we needed to move to `#[non_exhaustive]` on all of these structs/enums,
but will be much easier going forward.

Also, since we depend on unstable features, I am pinning the version so
we don't accidentally introduce compilation errors from other update
cycles.

Release Notes:

- N/A

Change summary

Cargo.lock                                  |   9 
Cargo.toml                                  |   2 
crates/acp_thread/src/acp_thread.rs         | 420 ++++++++--------------
crates/acp_thread/src/connection.rs         |  26 
crates/acp_thread/src/mention.rs            |   2 
crates/acp_thread/src/terminal.rs           |  44 +-
crates/agent/src/agent.rs                   |  55 --
crates/agent/src/db.rs                      |   2 
crates/agent/src/history_store.rs           |   6 
crates/agent/src/tests/mod.rs               | 100 ++---
crates/agent/src/thread.rs                  | 210 ++++-------
crates/agent/src/tools/edit_file_tool.rs    |  20 
crates/agent/src/tools/find_path_tool.rs    |  50 +-
crates/agent/src/tools/read_file_tool.rs    |  22 
crates/agent/src/tools/terminal_tool.rs     |   7 
crates/agent/src/tools/thinking_tool.rs     |   6 
crates/agent/src/tools/web_search_tool.rs   |  47 +-
crates/agent_servers/src/acp.rs             | 218 ++++-------
crates/agent_servers/src/claude.rs          |   4 
crates/agent_servers/src/codex.rs           |   4 
crates/agent_servers/src/custom.rs          |   4 
crates/agent_servers/src/e2e_tests.rs       |  25 -
crates/agent_ui/src/acp/entry_view_state.rs |  23 
crates/agent_ui/src/acp/message_editor.rs   | 186 +++------
crates/agent_ui/src/acp/model_selector.rs   |   2 
crates/agent_ui/src/acp/thread_view.rs      | 226 +++--------
crates/eval/src/example.rs                  |  13 
crates/eval/src/instance.rs                 |   7 
28 files changed, 631 insertions(+), 1,109 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -215,9 +215,9 @@ dependencies = [
 
 [[package]]
 name = "agent-client-protocol"
-version = "0.7.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
+checksum = "3e639d6b544ad39f5b4e05802db5eb04e1518284eb05fda1839931003e0244c8"
 dependencies = [
  "agent-client-protocol-schema",
  "anyhow",
@@ -226,16 +226,15 @@ dependencies = [
  "derive_more 2.0.1",
  "futures 0.3.31",
  "log",
- "parking_lot",
  "serde",
  "serde_json",
 ]
 
 [[package]]
 name = "agent-client-protocol-schema"
-version = "0.6.2"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
+checksum = "f182f5e14bef8232b239719bd99166bb11e986c08fc211f28e392f880d3093ba"
 dependencies = [
  "anyhow",
  "derive_more 2.0.1",

Cargo.toml πŸ”—

@@ -439,7 +439,7 @@ zlog_settings = { path = "crates/zlog_settings" }
 # External crates
 #
 
-agent-client-protocol = { version = "0.7.0", features = ["unstable"] }
+agent-client-protocol = { version = "=0.8.0", features = ["unstable"] }
 aho-corasick = "1.1"
 alacritty_terminal = "0.25.1-rc1"
 any_vec = "0.14"

crates/acp_thread/src/acp_thread.rs πŸ”—

@@ -201,17 +201,19 @@ impl ToolCall {
         };
         let mut content = Vec::with_capacity(tool_call.content.len());
         for item in tool_call.content {
-            content.push(ToolCallContent::from_acp(
+            if let Some(item) = ToolCallContent::from_acp(
                 item,
                 language_registry.clone(),
                 path_style,
                 terminals,
                 cx,
-            )?);
+            )? {
+                content.push(item);
+            }
         }
 
         let result = Self {
-            id: tool_call.id,
+            id: tool_call.tool_call_id,
             label: cx
                 .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
             kind: tool_call.kind,
@@ -241,6 +243,7 @@ impl ToolCall {
             locations,
             raw_input,
             raw_output,
+            ..
         } = fields;
 
         if let Some(kind) = kind {
@@ -262,21 +265,29 @@ impl ToolCall {
         }
 
         if let Some(content) = content {
-            let new_content_len = content.len();
+            let mut new_content_len = content.len();
             let mut content = content.into_iter();
 
             // Reuse existing content if we can
             for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
-                old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
+                let valid_content =
+                    old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
+                if !valid_content {
+                    new_content_len -= 1;
+                }
             }
             for new in content {
-                self.content.push(ToolCallContent::from_acp(
+                if let Some(new) = ToolCallContent::from_acp(
                     new,
                     language_registry.clone(),
                     path_style,
                     terminals,
                     cx,
-                )?)
+                )? {
+                    self.content.push(new);
+                } else {
+                    new_content_len -= 1;
+                }
             }
             self.content.truncate(new_content_len);
         }
@@ -425,6 +436,7 @@ impl From<acp::ToolCallStatus> for ToolCallStatus {
             acp::ToolCallStatus::InProgress => Self::InProgress,
             acp::ToolCallStatus::Completed => Self::Completed,
             acp::ToolCallStatus::Failed => Self::Failed,
+            _ => Self::Pending,
         }
     }
 }
@@ -537,7 +549,7 @@ impl ContentBlock {
                 ..
             }) => Self::resource_link_md(&uri, path_style),
             acp::ContentBlock::Image(image) => Self::image_md(&image),
-            acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(),
+            _ => String::new(),
         }
     }
 
@@ -591,15 +603,17 @@ impl ToolCallContent {
         path_style: PathStyle,
         terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
         cx: &mut App,
-    ) -> Result<Self> {
+    ) -> Result<Option<Self>> {
         match content {
-            acp::ToolCallContent::Content { content } => Ok(Self::ContentBlock(ContentBlock::new(
-                content,
-                &language_registry,
-                path_style,
-                cx,
-            ))),
-            acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
+            acp::ToolCallContent::Content(acp::Content { content, .. }) => {
+                Ok(Some(Self::ContentBlock(ContentBlock::new(
+                    content,
+                    &language_registry,
+                    path_style,
+                    cx,
+                ))))
+            }
+            acp::ToolCallContent::Diff(diff) => Ok(Some(Self::Diff(cx.new(|cx| {
                 Diff::finalized(
                     diff.path.to_string_lossy().into_owned(),
                     diff.old_text,
@@ -607,12 +621,13 @@ impl ToolCallContent {
                     language_registry,
                     cx,
                 )
-            }))),
-            acp::ToolCallContent::Terminal { terminal_id } => terminals
+            })))),
+            acp::ToolCallContent::Terminal(acp::Terminal { terminal_id, .. }) => terminals
                 .get(&terminal_id)
                 .cloned()
-                .map(Self::Terminal)
+                .map(|terminal| Some(Self::Terminal(terminal)))
                 .ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)),
+            _ => Ok(None),
         }
     }
 
@@ -623,9 +638,9 @@ impl ToolCallContent {
         path_style: PathStyle,
         terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
         cx: &mut App,
-    ) -> Result<()> {
+    ) -> Result<bool> {
         let needs_update = match (&self, &new) {
-            (Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => {
+            (Self::Diff(old_diff), acp::ToolCallContent::Diff(new_diff)) => {
                 old_diff.read(cx).needs_update(
                     new_diff.old_text.as_deref().unwrap_or(""),
                     &new_diff.new_text,
@@ -635,10 +650,14 @@ impl ToolCallContent {
             _ => true,
         };
 
-        if needs_update {
-            *self = Self::from_acp(new, language_registry, path_style, terminals, cx)?;
+        if let Some(update) = Self::from_acp(new, language_registry, path_style, terminals, cx)? {
+            if needs_update {
+                *self = update;
+            }
+            Ok(true)
+        } else {
+            Ok(false)
         }
-        Ok(())
     }
 
     pub fn to_markdown(&self, cx: &App) -> String {
@@ -660,7 +679,7 @@ pub enum ToolCallUpdate {
 impl ToolCallUpdate {
     fn id(&self) -> &acp::ToolCallId {
         match self {
-            Self::UpdateFields(update) => &update.id,
+            Self::UpdateFields(update) => &update.tool_call_id,
             Self::UpdateDiff(diff) => &diff.id,
             Self::UpdateTerminal(terminal) => &terminal.id,
         }
@@ -732,6 +751,7 @@ impl Plan {
                 acp::PlanEntryStatus::Completed => {
                     stats.completed += 1;
                 }
+                _ => {}
             }
         }
 
@@ -1154,6 +1174,7 @@ impl AcpThread {
                 current_mode_id,
                 ..
             }) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
+            _ => {}
         }
         Ok(())
     }
@@ -1287,11 +1308,7 @@ impl AcpThread {
                     label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)),
                     kind: acp::ToolKind::Fetch,
                     content: vec![ToolCallContent::ContentBlock(ContentBlock::new(
-                        acp::ContentBlock::Text(acp::TextContent {
-                            text: "Tool call not found".to_string(),
-                            annotations: None,
-                            meta: None,
-                        }),
+                        "Tool call not found".into(),
                         &languages,
                         path_style,
                         cx,
@@ -1315,7 +1332,7 @@ impl AcpThread {
                 let location_updated = update.fields.locations.is_some();
                 call.update_fields(update.fields, languages, path_style, &self.terminals, cx)?;
                 if location_updated {
-                    self.resolve_locations(update.id, cx);
+                    self.resolve_locations(update.tool_call_id, cx);
                 }
             }
             ToolCallUpdate::UpdateDiff(update) => {
@@ -1353,7 +1370,7 @@ impl AcpThread {
     ) -> Result<(), acp::Error> {
         let language_registry = self.project.read(cx).languages().clone();
         let path_style = self.project.read(cx).path_style(cx);
-        let id = update.id.clone();
+        let id = update.tool_call_id.clone();
 
         let agent = self.connection().telemetry_id();
         let session = self.session_id();
@@ -1518,16 +1535,16 @@ impl AcpThread {
             // some tools would (incorrectly) continue to auto-accept.
             if let Some(allow_once_option) = options.iter().find_map(|option| {
                 if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) {
-                    Some(option.id.clone())
+                    Some(option.option_id.clone())
                 } else {
                     None
                 }
             }) {
                 self.upsert_tool_call_inner(tool_call, ToolCallStatus::Pending, cx)?;
                 return Ok(async {
-                    acp::RequestPermissionOutcome::Selected {
-                        option_id: allow_once_option,
-                    }
+                    acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
+                        allow_once_option,
+                    ))
                 }
                 .boxed());
             }
@@ -1543,7 +1560,9 @@ impl AcpThread {
 
         let fut = async {
             match rx.await {
-                Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
+                Ok(option) => acp::RequestPermissionOutcome::Selected(
+                    acp::SelectedPermissionOutcome::new(option),
+                ),
                 Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
             }
         }
@@ -1570,6 +1589,7 @@ impl AcpThread {
             acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => {
                 ToolCallStatus::InProgress
             }
+            _ => ToolCallStatus::InProgress,
         };
 
         let curr_status = mem::replace(&mut call.status, new_status);
@@ -1648,14 +1668,7 @@ impl AcpThread {
         message: &str,
         cx: &mut Context<Self>,
     ) -> BoxFuture<'static, Result<()>> {
-        self.send(
-            vec![acp::ContentBlock::Text(acp::TextContent {
-                text: message.to_string(),
-                annotations: None,
-                meta: None,
-            })],
-            cx,
-        )
+        self.send(vec![message.into()], cx)
     }
 
     pub fn send(
@@ -1669,11 +1682,7 @@ impl AcpThread {
             self.project.read(cx).path_style(cx),
             cx,
         );
-        let request = acp::PromptRequest {
-            prompt: message.clone(),
-            session_id: self.session_id.clone(),
-            meta: None,
-        };
+        let request = acp::PromptRequest::new(self.session_id.clone(), message.clone());
         let git_store = self.project.read(cx).git_store().clone();
 
         let message_id = if self.connection.truncate(&self.session_id, cx).is_some() {
@@ -1765,7 +1774,7 @@ impl AcpThread {
                             result,
                             Ok(Ok(acp::PromptResponse {
                                 stop_reason: acp::StopReason::Cancelled,
-                                meta: None,
+                                ..
                             }))
                         );
 
@@ -1781,7 +1790,7 @@ impl AcpThread {
                         // Handle refusal - distinguish between user prompt and tool call refusals
                         if let Ok(Ok(acp::PromptResponse {
                             stop_reason: acp::StopReason::Refusal,
-                            meta: _,
+                            ..
                         })) = result
                         {
                             if let Some((user_msg_ix, _)) = this.last_user_message() {
@@ -2017,7 +2026,7 @@ impl AcpThread {
                         })?;
                     Ok(project.open_buffer(path, cx))
                 })
-                .map_err(|e| acp::Error::internal_error().with_data(e.to_string()))
+                .map_err(|e| acp::Error::internal_error().data(e.to_string()))
                 .flatten()?;
 
             let buffer = load.await?;
@@ -2050,7 +2059,7 @@ impl AcpThread {
             let start_position = Point::new(line, 0);
 
             if start_position > max_point {
-                return Err(acp::Error::invalid_params().with_data(format!(
+                return Err(acp::Error::invalid_params().data(format!(
                     "Attempting to read beyond the end of the file, line {}:{}",
                     max_point.row + 1,
                     max_point.column
@@ -2202,7 +2211,7 @@ impl AcpThread {
         let language_registry = project.read(cx).languages().clone();
         let is_windows = project.read(cx).path_style(cx).is_windows();
 
-        let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
+        let terminal_id = acp::TerminalId::new(Uuid::new_v4().to_string());
         let terminal_task = cx.spawn({
             let terminal_id = terminal_id.clone();
             async move |_this, cx| {
@@ -2412,7 +2421,7 @@ mod tests {
             .await
             .unwrap();
 
-        let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
+        let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
 
         // Send Output BEFORE Created - should be buffered by acp_thread
         thread.update(cx, |thread, cx| {
@@ -2474,7 +2483,7 @@ mod tests {
             .await
             .unwrap();
 
-        let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
+        let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
 
         // Send Output BEFORE Created
         thread.update(cx, |thread, cx| {
@@ -2492,11 +2501,7 @@ mod tests {
             thread.on_terminal_provider_event(
                 TerminalProviderEvent::Exit {
                     terminal_id: terminal_id.clone(),
-                    status: acp::TerminalExitStatus {
-                        exit_code: Some(0),
-                        signal: None,
-                        meta: None,
-                    },
+                    status: acp::TerminalExitStatus::new().exit_code(0),
                 },
                 cx,
             );
@@ -2553,15 +2558,7 @@ mod tests {
 
         // Test creating a new user message
         thread.update(cx, |thread, cx| {
-            thread.push_user_content_block(
-                None,
-                acp::ContentBlock::Text(acp::TextContent {
-                    annotations: None,
-                    text: "Hello, ".to_string(),
-                    meta: None,
-                }),
-                cx,
-            );
+            thread.push_user_content_block(None, "Hello, ".into(), cx);
         });
 
         thread.update(cx, |thread, cx| {
@@ -2577,15 +2574,7 @@ mod tests {
         // Test appending to existing user message
         let message_1_id = UserMessageId::new();
         thread.update(cx, |thread, cx| {
-            thread.push_user_content_block(
-                Some(message_1_id.clone()),
-                acp::ContentBlock::Text(acp::TextContent {
-                    annotations: None,
-                    text: "world!".to_string(),
-                    meta: None,
-                }),
-                cx,
-            );
+            thread.push_user_content_block(Some(message_1_id.clone()), "world!".into(), cx);
         });
 
         thread.update(cx, |thread, cx| {
@@ -2600,26 +2589,14 @@ mod tests {
 
         // Test creating new user message after assistant message
         thread.update(cx, |thread, cx| {
-            thread.push_assistant_content_block(
-                acp::ContentBlock::Text(acp::TextContent {
-                    annotations: None,
-                    text: "Assistant response".to_string(),
-                    meta: None,
-                }),
-                false,
-                cx,
-            );
+            thread.push_assistant_content_block("Assistant response".into(), false, cx);
         });
 
         let message_2_id = UserMessageId::new();
         thread.update(cx, |thread, cx| {
             thread.push_user_content_block(
                 Some(message_2_id.clone()),
-                acp::ContentBlock::Text(acp::TextContent {
-                    annotations: None,
-                    text: "New user message".to_string(),
-                    meta: None,
-                }),
+                "New user message".into(),
                 cx,
             );
         });
@@ -2647,27 +2624,22 @@ mod tests {
                     thread.update(&mut cx, |thread, cx| {
                         thread
                             .handle_session_update(
-                                acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
-                                    content: "Thinking ".into(),
-                                    meta: None,
-                                }),
+                                acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
+                                    "Thinking ".into(),
+                                )),
                                 cx,
                             )
                             .unwrap();
                         thread
                             .handle_session_update(
-                                acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
-                                    content: "hard!".into(),
-                                    meta: None,
-                                }),
+                                acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
+                                    "hard!".into(),
+                                )),
                                 cx,
                             )
                             .unwrap();
                     })?;
-                    Ok(acp::PromptResponse {
-                        stop_reason: acp::StopReason::EndTurn,
-                        meta: None,
-                    })
+                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
                 }
                 .boxed_local()
             },
@@ -2735,10 +2707,7 @@ mod tests {
                         .unwrap()
                         .await
                         .unwrap();
-                    Ok(acp::PromptResponse {
-                        stop_reason: acp::StopReason::EndTurn,
-                        meta: None,
-                    })
+                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
                 }
                 .boxed_local()
             },
@@ -2969,7 +2938,7 @@ mod tests {
 
         let fs = FakeFs::new(cx.executor());
         let project = Project::test(fs, [], cx).await;
-        let id = acp::ToolCallId("test".into());
+        let id = acp::ToolCallId::new("test");
 
         let connection = Rc::new(FakeAgentConnection::new().on_user_message({
             let id = id.clone();
@@ -2979,26 +2948,17 @@ mod tests {
                     thread
                         .update(&mut cx, |thread, cx| {
                             thread.handle_session_update(
-                                acp::SessionUpdate::ToolCall(acp::ToolCall {
-                                    id: id.clone(),
-                                    title: "Label".into(),
-                                    kind: acp::ToolKind::Fetch,
-                                    status: acp::ToolCallStatus::InProgress,
-                                    content: vec![],
-                                    locations: vec![],
-                                    raw_input: None,
-                                    raw_output: None,
-                                    meta: None,
-                                }),
+                                acp::SessionUpdate::ToolCall(
+                                    acp::ToolCall::new(id.clone(), "Label")
+                                        .kind(acp::ToolKind::Fetch)
+                                        .status(acp::ToolCallStatus::InProgress),
+                                ),
                                 cx,
                             )
                         })
                         .unwrap()
                         .unwrap();
-                    Ok(acp::PromptResponse {
-                        stop_reason: acp::StopReason::EndTurn,
-                        meta: None,
-                    })
+                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
                 }
                 .boxed_local()
             }
@@ -3040,14 +3000,10 @@ mod tests {
         thread
             .update(cx, |thread, cx| {
                 thread.handle_session_update(
-                    acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
+                    acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
                         id,
-                        fields: acp::ToolCallUpdateFields {
-                            status: Some(acp::ToolCallStatus::Completed),
-                            ..Default::default()
-                        },
-                        meta: None,
-                    }),
+                        acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
+                    )),
                     cx,
                 )
             })
@@ -3079,33 +3035,21 @@ mod tests {
                     thread
                         .update(&mut cx, |thread, cx| {
                             thread.handle_session_update(
-                                acp::SessionUpdate::ToolCall(acp::ToolCall {
-                                    id: acp::ToolCallId("test".into()),
-                                    title: "Label".into(),
-                                    kind: acp::ToolKind::Edit,
-                                    status: acp::ToolCallStatus::Completed,
-                                    content: vec![acp::ToolCallContent::Diff {
-                                        diff: acp::Diff {
-                                            path: "/test/test.txt".into(),
-                                            old_text: None,
-                                            new_text: "foo".into(),
-                                            meta: None,
-                                        },
-                                    }],
-                                    locations: vec![],
-                                    raw_input: None,
-                                    raw_output: None,
-                                    meta: None,
-                                }),
+                                acp::SessionUpdate::ToolCall(
+                                    acp::ToolCall::new("test", "Label")
+                                        .kind(acp::ToolKind::Edit)
+                                        .status(acp::ToolCallStatus::Completed)
+                                        .content(vec![acp::ToolCallContent::Diff(acp::Diff::new(
+                                            "/test/test.txt",
+                                            "foo",
+                                        ))]),
+                                ),
                                 cx,
                             )
                         })
                         .unwrap()
                         .unwrap();
-                    Ok(acp::PromptResponse {
-                        stop_reason: acp::StopReason::EndTurn,
-                        meta: None,
-                    })
+                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
                 }
                 .boxed_local()
             }
@@ -3158,18 +3102,14 @@ mod tests {
                     thread.update(&mut cx, |thread, cx| {
                         thread
                             .handle_session_update(
-                                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
-                                    content: content.text.to_uppercase().into(),
-                                    meta: None,
-                                }),
+                                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
+                                    content.text.to_uppercase().into(),
+                                )),
                                 cx,
                             )
                             .unwrap();
                     })?;
-                    Ok(acp::PromptResponse {
-                        stop_reason: acp::StopReason::EndTurn,
-                        meta: None,
-                    })
+                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
                 }
                 .boxed_local()
             }
@@ -3325,34 +3265,22 @@ mod tests {
                         thread.update(&mut cx, |thread, cx| {
                             thread
                                 .handle_session_update(
-                                    acp::SessionUpdate::ToolCall(acp::ToolCall {
-                                        id: acp::ToolCallId("tool1".into()),
-                                        title: "Test Tool".into(),
-                                        kind: acp::ToolKind::Fetch,
-                                        status: acp::ToolCallStatus::Completed,
-                                        content: vec![],
-                                        locations: vec![],
-                                        raw_input: Some(serde_json::json!({"query": "test"})),
-                                        raw_output: Some(
-                                            serde_json::json!({"result": "inappropriate content"}),
-                                        ),
-                                        meta: None,
-                                    }),
+                                    acp::SessionUpdate::ToolCall(
+                                        acp::ToolCall::new("tool1", "Test Tool")
+                                            .kind(acp::ToolKind::Fetch)
+                                            .status(acp::ToolCallStatus::Completed)
+                                            .raw_input(serde_json::json!({"query": "test"}))
+                                            .raw_output(serde_json::json!({"result": "inappropriate content"})),
+                                    ),
                                     cx,
                                 )
                                 .unwrap();
                         })?;
 
                         // Now return refusal because of the tool result
-                        Ok(acp::PromptResponse {
-                            stop_reason: acp::StopReason::Refusal,
-                            meta: None,
-                        })
+                        Ok(acp::PromptResponse::new(acp::StopReason::Refusal))
                     } else {
-                        Ok(acp::PromptResponse {
-                            stop_reason: acp::StopReason::EndTurn,
-                            meta: None,
-                        })
+                        Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
                     }
                 }
                 .boxed_local()
@@ -3380,16 +3308,7 @@ mod tests {
         });
 
         // Send a user message - this will trigger tool call and then refusal
-        let send_task = thread.update(cx, |thread, cx| {
-            thread.send(
-                vec![acp::ContentBlock::Text(acp::TextContent {
-                    text: "Hello".into(),
-                    annotations: None,
-                    meta: None,
-                })],
-                cx,
-            )
-        });
+        let send_task = thread.update(cx, |thread, cx| thread.send(vec!["Hello".into()], cx));
         cx.background_executor.spawn(send_task).detach();
         cx.run_until_parked();
 
@@ -3435,21 +3354,11 @@ mod tests {
             let refuse_next = refuse_next.clone();
             move |_request, _thread, _cx| {
                 if refuse_next.load(SeqCst) {
-                    async move {
-                        Ok(acp::PromptResponse {
-                            stop_reason: acp::StopReason::Refusal,
-                            meta: None,
-                        })
-                    }
-                    .boxed_local()
+                    async move { Ok(acp::PromptResponse::new(acp::StopReason::Refusal)) }
+                        .boxed_local()
                 } else {
-                    async move {
-                        Ok(acp::PromptResponse {
-                            stop_reason: acp::StopReason::EndTurn,
-                            meta: None,
-                        })
-                    }
-                    .boxed_local()
+                    async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }
+                        .boxed_local()
                 }
             }
         }));
@@ -3506,10 +3415,7 @@ mod tests {
                 let refuse_next = refuse_next.clone();
                 async move {
                     if refuse_next.load(SeqCst) {
-                        return Ok(acp::PromptResponse {
-                            stop_reason: acp::StopReason::Refusal,
-                            meta: None,
-                        });
+                        return Ok(acp::PromptResponse::new(acp::StopReason::Refusal));
                     }
 
                     let acp::ContentBlock::Text(content) = &request.prompt[0] else {
@@ -3518,18 +3424,14 @@ mod tests {
                     thread.update(&mut cx, |thread, cx| {
                         thread
                             .handle_session_update(
-                                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
-                                    content: content.text.to_uppercase().into(),
-                                    meta: None,
-                                }),
+                                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
+                                    content.text.to_uppercase().into(),
+                                )),
                                 cx,
                             )
                             .unwrap();
                     })?;
-                    Ok(acp::PromptResponse {
-                        stop_reason: acp::StopReason::EndTurn,
-                        meta: None,
-                    })
+                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
                 }
                 .boxed_local()
             }
@@ -3668,13 +3570,12 @@ mod tests {
             _cwd: &Path,
             cx: &mut App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
-            let session_id = acp::SessionId(
+            let session_id = acp::SessionId::new(
                 rand::rng()
                     .sample_iter(&distr::Alphanumeric)
                     .take(7)
                     .map(char::from)
-                    .collect::<String>()
-                    .into(),
+                    .collect::<String>(),
             );
             let action_log = cx.new(|_| ActionLog::new(project.clone()));
             let thread = cx.new(|cx| {
@@ -3684,12 +3585,12 @@ mod tests {
                     project,
                     action_log,
                     session_id.clone(),
-                    watch::Receiver::constant(acp::PromptCapabilities {
-                        image: true,
-                        audio: true,
-                        embedded_context: true,
-                        meta: None,
-                    }),
+                    watch::Receiver::constant(
+                        acp::PromptCapabilities::new()
+                            .image(true)
+                            .audio(true)
+                            .embedded_context(true),
+                    ),
                     cx,
                 )
             });
@@ -3718,10 +3619,7 @@ mod tests {
                 let thread = thread.clone();
                 cx.spawn(async move |cx| handler(params, thread, cx.clone()).await)
             } else {
-                Task::ready(Ok(acp::PromptResponse {
-                    stop_reason: acp::StopReason::EndTurn,
-                    meta: None,
-                }))
+                Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
             }
         }
 
@@ -3776,17 +3674,13 @@ mod tests {
             .unwrap();
 
         // Try to update a tool call that doesn't exist
-        let nonexistent_id = acp::ToolCallId("nonexistent-tool-call".into());
+        let nonexistent_id = acp::ToolCallId::new("nonexistent-tool-call");
         thread.update(cx, |thread, cx| {
             let result = thread.handle_session_update(
-                acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
-                    id: nonexistent_id.clone(),
-                    fields: acp::ToolCallUpdateFields {
-                        status: Some(acp::ToolCallStatus::Completed),
-                        ..Default::default()
-                    },
-                    meta: None,
-                }),
+                acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
+                    nonexistent_id.clone(),
+                    acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
+                )),
                 cx,
             );
 
@@ -3861,7 +3755,7 @@ mod tests {
         .unwrap();
 
         // Create 2 terminals BEFORE the checkpoint that have completed running
-        let terminal_id_1 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
+        let terminal_id_1 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
         let mock_terminal_1 = cx.new(|cx| {
             let builder = ::terminal::TerminalBuilder::new_display_only(
                 ::terminal::terminal_settings::CursorShape::default(),
@@ -3900,17 +3794,13 @@ mod tests {
             thread.on_terminal_provider_event(
                 TerminalProviderEvent::Exit {
                     terminal_id: terminal_id_1.clone(),
-                    status: acp::TerminalExitStatus {
-                        exit_code: Some(0),
-                        signal: None,
-                        meta: None,
-                    },
+                    status: acp::TerminalExitStatus::new().exit_code(0),
                 },
                 cx,
             );
         });
 
-        let terminal_id_2 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
+        let terminal_id_2 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
         let mock_terminal_2 = cx.new(|cx| {
             let builder = ::terminal::TerminalBuilder::new_display_only(
                 ::terminal::terminal_settings::CursorShape::default(),
@@ -3949,11 +3839,7 @@ mod tests {
             thread.on_terminal_provider_event(
                 TerminalProviderEvent::Exit {
                     terminal_id: terminal_id_2.clone(),
-                    status: acp::TerminalExitStatus {
-                        exit_code: Some(0),
-                        signal: None,
-                        meta: None,
-                    },
+                    status: acp::TerminalExitStatus::new().exit_code(0),
                 },
                 cx,
             );
@@ -3973,7 +3859,7 @@ mod tests {
 
         // Create a terminal AFTER the checkpoint we'll restore to.
         // This simulates the AI agent starting a long-running terminal command.
-        let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
+        let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
         let mock_terminal = cx.new(|cx| {
             let builder = ::terminal::TerminalBuilder::new_display_only(
                 ::terminal::terminal_settings::CursorShape::default(),
@@ -4015,21 +3901,15 @@ mod tests {
         thread.update(cx, |thread, cx| {
             thread
                 .handle_session_update(
-                    acp::SessionUpdate::ToolCall(acp::ToolCall {
-                        id: acp::ToolCallId("terminal-tool-1".into()),
-                        title: "Running command".into(),
-                        kind: acp::ToolKind::Execute,
-                        status: acp::ToolCallStatus::InProgress,
-                        content: vec![acp::ToolCallContent::Terminal {
-                            terminal_id: terminal_id.clone(),
-                        }],
-                        locations: vec![],
-                        raw_input: Some(
-                            serde_json::json!({"command": "sleep 1000", "cd": "/test"}),
-                        ),
-                        raw_output: None,
-                        meta: None,
-                    }),
+                    acp::SessionUpdate::ToolCall(
+                        acp::ToolCall::new("terminal-tool-1", "Running command")
+                            .kind(acp::ToolKind::Execute)
+                            .status(acp::ToolCallStatus::InProgress)
+                            .content(vec![acp::ToolCallContent::Terminal(acp::Terminal::new(
+                                terminal_id.clone(),
+                            ))])
+                            .raw_input(serde_json::json!({"command": "sleep 1000", "cd": "/test"})),
+                    ),
                     cx,
                 )
                 .unwrap();

crates/acp_thread/src/connection.rs πŸ”—

@@ -336,7 +336,7 @@ mod test_support {
             _cwd: &Path,
             cx: &mut gpui::App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
-            let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
+            let session_id = acp::SessionId::new(self.sessions.lock().len().to_string());
             let action_log = cx.new(|_| ActionLog::new(project.clone()));
             let thread = cx.new(|cx| {
                 AcpThread::new(
@@ -345,12 +345,12 @@ mod test_support {
                     project,
                     action_log,
                     session_id.clone(),
-                    watch::Receiver::constant(acp::PromptCapabilities {
-                        image: true,
-                        audio: true,
-                        embedded_context: true,
-                        meta: None,
-                    }),
+                    watch::Receiver::constant(
+                        acp::PromptCapabilities::new()
+                            .image(true)
+                            .audio(true)
+                            .embedded_context(true),
+                    ),
                     cx,
                 )
             });
@@ -389,10 +389,7 @@ mod test_support {
                 response_tx.replace(tx);
                 cx.spawn(async move |_| {
                     let stop_reason = rx.await?;
-                    Ok(acp::PromptResponse {
-                        stop_reason,
-                        meta: None,
-                    })
+                    Ok(acp::PromptResponse::new(stop_reason))
                 })
             } else {
                 for update in self.next_prompt_updates.lock().drain(..) {
@@ -400,7 +397,7 @@ mod test_support {
                     let update = update.clone();
                     let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
                         &update
-                        && let Some(options) = self.permission_requests.get(&tool_call.id)
+                        && let Some(options) = self.permission_requests.get(&tool_call.tool_call_id)
                     {
                         Some((tool_call.clone(), options.clone()))
                     } else {
@@ -429,10 +426,7 @@ mod test_support {
 
                 cx.spawn(async move |_| {
                     try_join_all(tasks).await?;
-                    Ok(acp::PromptResponse {
-                        stop_reason: acp::StopReason::EndTurn,
-                        meta: None,
-                    })
+                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
                 })
             }
         }

crates/acp_thread/src/mention.rs πŸ”—

@@ -108,7 +108,7 @@ impl MentionUri {
                 if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
                     let name = single_query_param(&url, "name")?.context("Missing thread name")?;
                     Ok(Self::Thread {
-                        id: acp::SessionId(thread_id.into()),
+                        id: acp::SessionId::new(thread_id),
                         name,
                     })
                 } else if let Some(path) = path.strip_prefix("/agent/text-thread/") {

crates/acp_thread/src/terminal.rs πŸ”—

@@ -75,11 +75,15 @@ impl Terminal {
 
                     let exit_status = exit_status.map(portable_pty::ExitStatus::from);
 
-                    acp::TerminalExitStatus {
-                        exit_code: exit_status.as_ref().map(|e| e.exit_code()),
-                        signal: exit_status.and_then(|e| e.signal().map(Into::into)),
-                        meta: None,
+                    let mut status = acp::TerminalExitStatus::new();
+
+                    if let Some(exit_status) = exit_status.as_ref() {
+                        status = status.exit_code(exit_status.exit_code());
+                        if let Some(signal) = exit_status.signal() {
+                            status = status.signal(signal);
+                        }
                     }
+                    status
                 })
                 .shared(),
         }
@@ -101,27 +105,23 @@ impl Terminal {
 
     pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
         if let Some(output) = self.output.as_ref() {
-            let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
-
-            acp::TerminalOutputResponse {
-                output: output.content.clone(),
-                truncated: output.original_content_len > output.content.len(),
-                exit_status: Some(acp::TerminalExitStatus {
-                    exit_code: exit_status.as_ref().map(|e| e.exit_code()),
-                    signal: exit_status.and_then(|e| e.signal().map(Into::into)),
-                    meta: None,
-                }),
-                meta: None,
+            let mut exit_status = acp::TerminalExitStatus::new();
+            if let Some(status) = output.exit_status.map(portable_pty::ExitStatus::from) {
+                exit_status = exit_status.exit_code(status.exit_code());
+                if let Some(signal) = status.signal() {
+                    exit_status = exit_status.signal(signal);
+                }
             }
+
+            acp::TerminalOutputResponse::new(
+                output.content.clone(),
+                output.original_content_len > output.content.len(),
+            )
+            .exit_status(exit_status)
         } else {
             let (current_content, original_len) = self.truncated_output(cx);
-
-            acp::TerminalOutputResponse {
-                truncated: current_content.len() < original_len,
-                output: current_content,
-                exit_status: None,
-                meta: None,
-            }
+            let truncated = current_content.len() < original_len;
+            acp::TerminalOutputResponse::new(current_content, truncated)
         }
     }
 

crates/agent/src/agent.rs πŸ”—

@@ -170,7 +170,7 @@ impl LanguageModels {
     }
 
     fn model_id(model: &Arc<dyn LanguageModel>) -> acp::ModelId {
-        acp::ModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
+        acp::ModelId::new(format!("{}/{}", model.provider_id().0, model.id().0))
     }
 
     fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
@@ -789,28 +789,12 @@ impl NativeAgentConnection {
                             }
                             ThreadEvent::AgentText(text) => {
                                 acp_thread.update(cx, |thread, cx| {
-                                    thread.push_assistant_content_block(
-                                        acp::ContentBlock::Text(acp::TextContent {
-                                            text,
-                                            annotations: None,
-                                            meta: None,
-                                        }),
-                                        false,
-                                        cx,
-                                    )
+                                    thread.push_assistant_content_block(text.into(), false, cx)
                                 })?;
                             }
                             ThreadEvent::AgentThinking(text) => {
                                 acp_thread.update(cx, |thread, cx| {
-                                    thread.push_assistant_content_block(
-                                        acp::ContentBlock::Text(acp::TextContent {
-                                            text,
-                                            annotations: None,
-                                            meta: None,
-                                        }),
-                                        true,
-                                        cx,
-                                    )
+                                    thread.push_assistant_content_block(text.into(), true, cx)
                                 })?;
                             }
                             ThreadEvent::ToolCallAuthorization(ToolCallAuthorization {
@@ -824,8 +808,9 @@ impl NativeAgentConnection {
                                     )
                                 })??;
                                 cx.background_spawn(async move {
-                                    if let acp::RequestPermissionOutcome::Selected { option_id } =
-                                        outcome_task.await
+                                    if let acp::RequestPermissionOutcome::Selected(
+                                        acp::SelectedPermissionOutcome { option_id, .. },
+                                    ) = outcome_task.await
                                     {
                                         response
                                             .send(option_id)
@@ -852,10 +837,7 @@ impl NativeAgentConnection {
                             }
                             ThreadEvent::Stop(stop_reason) => {
                                 log::debug!("Assistant message complete: {:?}", stop_reason);
-                                return Ok(acp::PromptResponse {
-                                    stop_reason,
-                                    meta: None,
-                                });
+                                return Ok(acp::PromptResponse::new(stop_reason));
                             }
                         }
                     }
@@ -867,10 +849,7 @@ impl NativeAgentConnection {
             }
 
             log::debug!("Response stream completed");
-            anyhow::Ok(acp::PromptResponse {
-                stop_reason: acp::StopReason::EndTurn,
-                meta: None,
-            })
+            anyhow::Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
         })
     }
 }
@@ -1374,7 +1353,7 @@ mod internal_tests {
             IndexMap::from_iter([(
                 AgentModelGroupName("Fake".into()),
                 vec![AgentModelInfo {
-                    id: acp::ModelId("fake/fake".into()),
+                    id: acp::ModelId::new("fake/fake"),
                     name: "Fake".into(),
                     description: None,
                     icon: Some(ui::IconName::ZedAssistant),
@@ -1435,7 +1414,7 @@ mod internal_tests {
 
         // Select a model
         let selector = connection.model_selector(&session_id).unwrap();
-        let model_id = acp::ModelId("fake/fake".into());
+        let model_id = acp::ModelId::new("fake/fake");
         cx.update(|cx| selector.select_model(model_id.clone(), cx))
             .await
             .unwrap();
@@ -1521,20 +1500,14 @@ mod internal_tests {
             thread.send(
                 vec![
                     "What does ".into(),
-                    acp::ContentBlock::ResourceLink(acp::ResourceLink {
-                        name: "b.md".into(),
-                        uri: MentionUri::File {
+                    acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
+                        "b.md",
+                        MentionUri::File {
                             abs_path: path!("/a/b.md").into(),
                         }
                         .to_uri()
                         .to_string(),
-                        annotations: None,
-                        description: None,
-                        mime_type: None,
-                        size: None,
-                        title: None,
-                        meta: None,
-                    }),
+                    )),
                     " mean?".into(),
                 ],
                 cx,

crates/agent/src/db.rs πŸ”—

@@ -366,7 +366,7 @@ impl ThreadsDatabase {
 
             for (id, summary, updated_at) in rows {
                 threads.push(DbThreadMetadata {
-                    id: acp::SessionId(id),
+                    id: acp::SessionId::new(id),
                     title: summary.into(),
                     updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc),
                 });

crates/agent/src/history_store.rs πŸ”—

@@ -354,9 +354,9 @@ impl HistoryStore {
                 .into_iter()
                 .take(MAX_RECENTLY_OPENED_ENTRIES)
                 .flat_map(|entry| match entry {
-                    SerializedRecentOpen::AcpThread(id) => Some(HistoryEntryId::AcpThread(
-                        acp::SessionId(id.as_str().into()),
-                    )),
+                    SerializedRecentOpen::AcpThread(id) => {
+                        Some(HistoryEntryId::AcpThread(acp::SessionId::new(id.as_str())))
+                    }
                     SerializedRecentOpen::TextThread(file_name) => Some(
                         HistoryEntryId::TextThread(text_threads_dir().join(file_name).into()),
                     ),

crates/agent/src/tests/mod.rs πŸ”—

@@ -493,14 +493,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
     // Approve the first
     tool_call_auth_1
         .response
-        .send(tool_call_auth_1.options[1].id.clone())
+        .send(tool_call_auth_1.options[1].option_id.clone())
         .unwrap();
     cx.run_until_parked();
 
     // Reject the second
     tool_call_auth_2
         .response
-        .send(tool_call_auth_1.options[2].id.clone())
+        .send(tool_call_auth_1.options[2].option_id.clone())
         .unwrap();
     cx.run_until_parked();
 
@@ -510,14 +510,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
         message.content,
         vec![
             language_model::MessageContent::ToolResult(LanguageModelToolResult {
-                tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
+                tool_use_id: tool_call_auth_1.tool_call.tool_call_id.0.to_string().into(),
                 tool_name: ToolRequiringPermission::name().into(),
                 is_error: false,
                 content: "Allowed".into(),
                 output: Some("Allowed".into())
             }),
             language_model::MessageContent::ToolResult(LanguageModelToolResult {
-                tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
+                tool_use_id: tool_call_auth_2.tool_call.tool_call_id.0.to_string().into(),
                 tool_name: ToolRequiringPermission::name().into(),
                 is_error: true,
                 content: "Permission to run tool denied by user".into(),
@@ -543,7 +543,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
     let tool_call_auth_3 = next_tool_call_authorization(&mut events).await;
     tool_call_auth_3
         .response
-        .send(tool_call_auth_3.options[0].id.clone())
+        .send(tool_call_auth_3.options[0].option_id.clone())
         .unwrap();
     cx.run_until_parked();
     let completion = fake_model.pending_completions().pop().unwrap();
@@ -552,7 +552,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
         message.content,
         vec![language_model::MessageContent::ToolResult(
             LanguageModelToolResult {
-                tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
+                tool_use_id: tool_call_auth_3.tool_call.tool_call_id.0.to_string().into(),
                 tool_name: ToolRequiringPermission::name().into(),
                 is_error: false,
                 content: "Allowed".into(),
@@ -1353,20 +1353,20 @@ async fn test_cancellation(cx: &mut TestAppContext) {
             ThreadEvent::ToolCall(tool_call) => {
                 assert_eq!(tool_call.title, expected_tools.remove(0));
                 if tool_call.title == "Echo" {
-                    echo_id = Some(tool_call.id);
+                    echo_id = Some(tool_call.tool_call_id);
                 }
             }
             ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
                 acp::ToolCallUpdate {
-                    id,
+                    tool_call_id,
                     fields:
                         acp::ToolCallUpdateFields {
                             status: Some(acp::ToolCallStatus::Completed),
                             ..
                         },
-                    meta: None,
+                    ..
                 },
-            )) if Some(&id) == echo_id.as_ref() => {
+            )) if Some(&tool_call_id) == echo_id.as_ref() => {
                 echo_completed = true;
             }
             _ => {}
@@ -1995,11 +1995,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
         .update(|cx| {
             connection.prompt(
                 Some(acp_thread::UserMessageId::new()),
-                acp::PromptRequest {
-                    session_id: session_id.clone(),
-                    prompt: vec!["ghi".into()],
-                    meta: None,
-                },
+                acp::PromptRequest::new(session_id.clone(), vec!["ghi".into()]),
                 cx,
             )
         })
@@ -2056,68 +2052,50 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
     let tool_call = expect_tool_call(&mut events).await;
     assert_eq!(
         tool_call,
-        acp::ToolCall {
-            id: acp::ToolCallId("1".into()),
-            title: "Thinking".into(),
-            kind: acp::ToolKind::Think,
-            status: acp::ToolCallStatus::Pending,
-            content: vec![],
-            locations: vec![],
-            raw_input: Some(json!({})),
-            raw_output: None,
-            meta: Some(json!({ "tool_name": "thinking" })),
-        }
+        acp::ToolCall::new("1", "Thinking")
+            .kind(acp::ToolKind::Think)
+            .raw_input(json!({}))
+            .meta(acp::Meta::from_iter([(
+                "tool_name".into(),
+                "thinking".into()
+            )]))
     );
     let update = expect_tool_call_update_fields(&mut events).await;
     assert_eq!(
         update,
-        acp::ToolCallUpdate {
-            id: acp::ToolCallId("1".into()),
-            fields: acp::ToolCallUpdateFields {
-                title: Some("Thinking".into()),
-                kind: Some(acp::ToolKind::Think),
-                raw_input: Some(json!({ "content": "Thinking hard!" })),
-                ..Default::default()
-            },
-            meta: None,
-        }
+        acp::ToolCallUpdate::new(
+            "1",
+            acp::ToolCallUpdateFields::new()
+                .title("Thinking")
+                .kind(acp::ToolKind::Think)
+                .raw_input(json!({ "content": "Thinking hard!"}))
+        )
     );
     let update = expect_tool_call_update_fields(&mut events).await;
     assert_eq!(
         update,
-        acp::ToolCallUpdate {
-            id: acp::ToolCallId("1".into()),
-            fields: acp::ToolCallUpdateFields {
-                status: Some(acp::ToolCallStatus::InProgress),
-                ..Default::default()
-            },
-            meta: None,
-        }
+        acp::ToolCallUpdate::new(
+            "1",
+            acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress)
+        )
     );
     let update = expect_tool_call_update_fields(&mut events).await;
     assert_eq!(
         update,
-        acp::ToolCallUpdate {
-            id: acp::ToolCallId("1".into()),
-            fields: acp::ToolCallUpdateFields {
-                content: Some(vec!["Thinking hard!".into()]),
-                ..Default::default()
-            },
-            meta: None,
-        }
+        acp::ToolCallUpdate::new(
+            "1",
+            acp::ToolCallUpdateFields::new().content(vec!["Thinking hard!".into()])
+        )
     );
     let update = expect_tool_call_update_fields(&mut events).await;
     assert_eq!(
         update,
-        acp::ToolCallUpdate {
-            id: acp::ToolCallId("1".into()),
-            fields: acp::ToolCallUpdateFields {
-                status: Some(acp::ToolCallStatus::Completed),
-                raw_output: Some("Finished thinking.".into()),
-                ..Default::default()
-            },
-            meta: None,
-        }
+        acp::ToolCallUpdate::new(
+            "1",
+            acp::ToolCallUpdateFields::new()
+                .status(acp::ToolCallStatus::Completed)
+                .raw_output("Finished thinking.".into())
+        )
     );
 }
 

crates/agent/src/thread.rs πŸ”—

@@ -619,12 +619,9 @@ pub struct Thread {
 impl Thread {
     fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
         let image = model.map_or(true, |model| model.supports_images());
-        acp::PromptCapabilities {
-            meta: None,
-            image,
-            audio: false,
-            embedded_context: true,
-        }
+        acp::PromptCapabilities::new()
+            .image(image)
+            .embedded_context(true)
     }
 
     pub fn new(
@@ -640,7 +637,7 @@ impl Thread {
         let (prompt_capabilities_tx, prompt_capabilities_rx) =
             watch::channel(Self::prompt_capabilities(model.as_deref()));
         Self {
-            id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
+            id: acp::SessionId::new(uuid::Uuid::new_v4().to_string()),
             prompt_id: PromptId::new(),
             updated_at: Utc::now(),
             title: None,
@@ -737,17 +734,11 @@ impl Thread {
         let Some(tool) = tool else {
             stream
                 .0
-                .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
-                    meta: None,
-                    id: acp::ToolCallId(tool_use.id.to_string().into()),
-                    title: tool_use.name.to_string(),
-                    kind: acp::ToolKind::Other,
-                    status: acp::ToolCallStatus::Failed,
-                    content: Vec::new(),
-                    locations: Vec::new(),
-                    raw_input: Some(tool_use.input.clone()),
-                    raw_output: None,
-                })))
+                .unbounded_send(Ok(ThreadEvent::ToolCall(
+                    acp::ToolCall::new(tool_use.id.to_string(), tool_use.name.to_string())
+                        .status(acp::ToolCallStatus::Failed)
+                        .raw_input(tool_use.input.clone()),
+                )))
                 .ok();
             return;
         };
@@ -775,24 +766,20 @@ impl Thread {
                 .log_err();
         }
 
-        stream.update_tool_call_fields(
-            &tool_use.id,
-            acp::ToolCallUpdateFields {
-                status: Some(
-                    tool_result
-                        .as_ref()
-                        .map_or(acp::ToolCallStatus::Failed, |result| {
-                            if result.is_error {
-                                acp::ToolCallStatus::Failed
-                            } else {
-                                acp::ToolCallStatus::Completed
-                            }
-                        }),
-                ),
-                raw_output: output,
-                ..Default::default()
+        let mut fields = acp::ToolCallUpdateFields::new().status(tool_result.as_ref().map_or(
+            acp::ToolCallStatus::Failed,
+            |result| {
+                if result.is_error {
+                    acp::ToolCallStatus::Failed
+                } else {
+                    acp::ToolCallStatus::Completed
+                }
             },
-        );
+        ));
+        if let Some(output) = output {
+            fields = fields.raw_output(output);
+        }
+        stream.update_tool_call_fields(&tool_use.id, fields);
     }
 
     pub fn from_db(
@@ -1272,18 +1259,15 @@ impl Thread {
             while let Some(tool_result) = tool_results.next().await {
                 log::debug!("Tool finished {:?}", tool_result);
 
-                event_stream.update_tool_call_fields(
-                    &tool_result.tool_use_id,
-                    acp::ToolCallUpdateFields {
-                        status: Some(if tool_result.is_error {
-                            acp::ToolCallStatus::Failed
-                        } else {
-                            acp::ToolCallStatus::Completed
-                        }),
-                        raw_output: tool_result.output.clone(),
-                        ..Default::default()
-                    },
-                );
+                let mut fields = acp::ToolCallUpdateFields::new().status(if tool_result.is_error {
+                    acp::ToolCallStatus::Failed
+                } else {
+                    acp::ToolCallStatus::Completed
+                });
+                if let Some(output) = &tool_result.output {
+                    fields = fields.raw_output(output.clone());
+                }
+                event_stream.update_tool_call_fields(&tool_result.tool_use_id, fields);
                 this.update(cx, |this, _cx| {
                     this.pending_message()
                         .tool_results
@@ -1560,12 +1544,10 @@ impl Thread {
         } else {
             event_stream.update_tool_call_fields(
                 &tool_use.id,
-                acp::ToolCallUpdateFields {
-                    title: Some(title.into()),
-                    kind: Some(kind),
-                    raw_input: Some(tool_use.input.clone()),
-                    ..Default::default()
-                },
+                acp::ToolCallUpdateFields::new()
+                    .title(title)
+                    .kind(kind)
+                    .raw_input(tool_use.input.clone()),
             );
         }
 
@@ -1587,10 +1569,9 @@ impl Thread {
         let fs = self.project.read(cx).fs().clone();
         let tool_event_stream =
             ToolCallEventStream::new(tool_use.id.clone(), event_stream.clone(), Some(fs));
-        tool_event_stream.update_fields(acp::ToolCallUpdateFields {
-            status: Some(acp::ToolCallStatus::InProgress),
-            ..Default::default()
-        });
+        tool_event_stream.update_fields(
+            acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress),
+        );
         let supports_images = self.model().is_some_and(|model| model.supports_images());
         let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
         log::debug!("Running tool {}", tool_use.name);
@@ -2381,19 +2362,13 @@ impl ThreadEventStream {
         kind: acp::ToolKind,
         input: serde_json::Value,
     ) -> acp::ToolCall {
-        acp::ToolCall {
-            meta: Some(serde_json::json!({
-                "tool_name": tool_name
-            })),
-            id: acp::ToolCallId(id.to_string().into()),
-            title,
-            kind,
-            status: acp::ToolCallStatus::Pending,
-            content: vec![],
-            locations: vec![],
-            raw_input: Some(input),
-            raw_output: None,
-        }
+        acp::ToolCall::new(id.to_string(), title)
+            .kind(kind)
+            .raw_input(input)
+            .meta(acp::Meta::from_iter([(
+                "tool_name".into(),
+                tool_name.into(),
+            )]))
     }
 
     fn update_tool_call_fields(
@@ -2403,12 +2378,7 @@ impl ThreadEventStream {
     ) {
         self.0
             .unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
-                acp::ToolCallUpdate {
-                    meta: None,
-                    id: acp::ToolCallId(tool_use_id.to_string().into()),
-                    fields,
-                }
-                .into(),
+                acp::ToolCallUpdate::new(tool_use_id.to_string(), fields).into(),
             )))
             .ok();
     }
@@ -2471,7 +2441,7 @@ impl ToolCallEventStream {
             .0
             .unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
                 acp_thread::ToolCallUpdateDiff {
-                    id: acp::ToolCallId(self.tool_use_id.to_string().into()),
+                    id: acp::ToolCallId::new(self.tool_use_id.to_string()),
                     diff,
                 }
                 .into(),
@@ -2489,33 +2459,26 @@ impl ToolCallEventStream {
             .0
             .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
                 ToolCallAuthorization {
-                    tool_call: acp::ToolCallUpdate {
-                        meta: None,
-                        id: acp::ToolCallId(self.tool_use_id.to_string().into()),
-                        fields: acp::ToolCallUpdateFields {
-                            title: Some(title.into()),
-                            ..Default::default()
-                        },
-                    },
+                    tool_call: acp::ToolCallUpdate::new(
+                        self.tool_use_id.to_string(),
+                        acp::ToolCallUpdateFields::new().title(title),
+                    ),
                     options: vec![
-                        acp::PermissionOption {
-                            id: acp::PermissionOptionId("always_allow".into()),
-                            name: "Always Allow".into(),
-                            kind: acp::PermissionOptionKind::AllowAlways,
-                            meta: None,
-                        },
-                        acp::PermissionOption {
-                            id: acp::PermissionOptionId("allow".into()),
-                            name: "Allow".into(),
-                            kind: acp::PermissionOptionKind::AllowOnce,
-                            meta: None,
-                        },
-                        acp::PermissionOption {
-                            id: acp::PermissionOptionId("deny".into()),
-                            name: "Deny".into(),
-                            kind: acp::PermissionOptionKind::RejectOnce,
-                            meta: None,
-                        },
+                        acp::PermissionOption::new(
+                            acp::PermissionOptionId::new("always_allow"),
+                            "Always Allow",
+                            acp::PermissionOptionKind::AllowAlways,
+                        ),
+                        acp::PermissionOption::new(
+                            acp::PermissionOptionId::new("allow"),
+                            "Allow",
+                            acp::PermissionOptionKind::AllowOnce,
+                        ),
+                        acp::PermissionOption::new(
+                            acp::PermissionOptionId::new("deny"),
+                            "Deny",
+                            acp::PermissionOptionKind::RejectOnce,
+                        ),
                     ],
                     response: response_tx,
                 },
@@ -2660,7 +2623,15 @@ impl UserMessageContent {
                     // TODO
                     Self::Text("[blob]".to_string())
                 }
+                other => {
+                    log::warn!("Unexpected content type: {:?}", other);
+                    Self::Text("[unknown]".to_string())
+                }
             },
+            other => {
+                log::warn!("Unexpected content type: {:?}", other);
+                Self::Text("[unknown]".to_string())
+            }
         }
     }
 }
@@ -2668,32 +2639,15 @@ impl UserMessageContent {
 impl From<UserMessageContent> for acp::ContentBlock {
     fn from(content: UserMessageContent) -> Self {
         match content {
-            UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent {
-                text,
-                annotations: None,
-                meta: None,
-            }),
-            UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent {
-                data: image.source.to_string(),
-                mime_type: "image/png".to_string(),
-                meta: None,
-                annotations: None,
-                uri: None,
-            }),
-            UserMessageContent::Mention { uri, content } => {
-                acp::ContentBlock::Resource(acp::EmbeddedResource {
-                    meta: None,
-                    resource: acp::EmbeddedResourceResource::TextResourceContents(
-                        acp::TextResourceContents {
-                            meta: None,
-                            mime_type: None,
-                            text: content,
-                            uri: uri.to_uri().to_string(),
-                        },
-                    ),
-                    annotations: None,
-                })
+            UserMessageContent::Text(text) => text.into(),
+            UserMessageContent::Image(image) => {
+                acp::ContentBlock::Image(acp::ImageContent::new(image.source, "image/png"))
             }
+            UserMessageContent::Mention { uri, content } => acp::ContentBlock::Resource(
+                acp::EmbeddedResource::new(acp::EmbeddedResourceResource::TextResourceContents(
+                    acp::TextResourceContents::new(content, uri.to_uri().to_string()),
+                )),
+            ),
         }
     }
 }

crates/agent/src/tools/edit_file_tool.rs πŸ”—

@@ -273,14 +273,9 @@ impl AgentTool for EditFileTool {
         };
         let abs_path = project.read(cx).absolute_path(&project_path, cx);
         if let Some(abs_path) = abs_path.clone() {
-            event_stream.update_fields(ToolCallUpdateFields {
-                locations: Some(vec![acp::ToolCallLocation {
-                    path: abs_path,
-                    line: None,
-                    meta: None,
-                }]),
-                ..Default::default()
-            });
+            event_stream.update_fields(
+                ToolCallUpdateFields::new().locations(vec![acp::ToolCallLocation::new(abs_path)]),
+            );
         }
 
         let authorize = self.authorize(&input, &event_stream, cx);
@@ -389,10 +384,11 @@ impl AgentTool for EditFileTool {
                                 range.start.to_point(&buffer.snapshot()).row
                             }).ok();
                             if let Some(abs_path) = abs_path.clone() {
-                                event_stream.update_fields(ToolCallUpdateFields {
-                                    locations: Some(vec![ToolCallLocation { path: abs_path, line, meta: None }]),
-                                    ..Default::default()
-                                });
+                                let mut location = ToolCallLocation::new(abs_path);
+                                if let Some(line) = line {
+                                    location = location.line(line);
+                                }
+                                event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
                             }
                             emitted_location = true;
                         }

crates/agent/src/tools/find_path_tool.rs πŸ”—

@@ -118,33 +118,29 @@ impl AgentTool for FindPathTool {
             let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len())
                 ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
 
-            event_stream.update_fields(acp::ToolCallUpdateFields {
-                title: Some(if paginated_matches.is_empty() {
-                    "No matches".into()
-                } else if paginated_matches.len() == 1 {
-                    "1 match".into()
-                } else {
-                    format!("{} matches", paginated_matches.len())
-                }),
-                content: Some(
-                    paginated_matches
-                        .iter()
-                        .map(|path| acp::ToolCallContent::Content {
-                            content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
-                                uri: format!("file://{}", path.display()),
-                                name: path.to_string_lossy().into(),
-                                annotations: None,
-                                description: None,
-                                mime_type: None,
-                                size: None,
-                                title: None,
-                                meta: None,
-                            }),
-                        })
-                        .collect(),
-                ),
-                ..Default::default()
-            });
+            event_stream.update_fields(
+                acp::ToolCallUpdateFields::new()
+                    .title(if paginated_matches.is_empty() {
+                        "No matches".into()
+                    } else if paginated_matches.len() == 1 {
+                        "1 match".into()
+                    } else {
+                        format!("{} matches", paginated_matches.len())
+                    })
+                    .content(
+                        paginated_matches
+                            .iter()
+                            .map(|path| {
+                                acp::ToolCallContent::Content(acp::Content::new(
+                                    acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
+                                        path.to_string_lossy(),
+                                        format!("file://{}", path.display()),
+                                    )),
+                                ))
+                            })
+                            .collect(),
+                    ),
+            );
 
             Ok(FindPathToolOutput {
                 offset: input.offset,

crates/agent/src/tools/read_file_tool.rs πŸ”—

@@ -152,15 +152,12 @@ impl AgentTool for ReadFileTool {
         }
 
         let file_path = input.path.clone();
+        let mut location = acp::ToolCallLocation::new(&abs_path);
+        if let Some(line) = input.start_line {
+            location = location.line(line.saturating_sub(1));
+        }
 
-        event_stream.update_fields(ToolCallUpdateFields {
-            locations: Some(vec![acp::ToolCallLocation {
-                path: abs_path.clone(),
-                line: input.start_line.map(|line| line.saturating_sub(1)),
-                meta: None,
-            }]),
-            ..Default::default()
-        });
+        event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
 
         if image_store::is_image_file(&self.project, &project_path, cx) {
             return cx.spawn(async move |cx| {
@@ -289,12 +286,9 @@ impl AgentTool for ReadFileTool {
                         text,
                     }
                     .to_string();
-                    event_stream.update_fields(ToolCallUpdateFields {
-                        content: Some(vec![acp::ToolCallContent::Content {
-                            content: markdown.into(),
-                        }]),
-                        ..Default::default()
-                    })
+                    event_stream.update_fields(ToolCallUpdateFields::new().content(vec![
+                        acp::ToolCallContent::Content(acp::Content::new(markdown)),
+                    ]));
                 }
             })?;
 

crates/agent/src/tools/terminal_tool.rs πŸ”—

@@ -112,10 +112,9 @@ impl AgentTool for TerminalTool {
                 .await?;
 
             let terminal_id = terminal.id(cx)?;
-            event_stream.update_fields(acp::ToolCallUpdateFields {
-                content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]),
-                ..Default::default()
-            });
+            event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![
+                acp::ToolCallContent::Terminal(acp::Terminal::new(terminal_id)),
+            ]));
 
             let exit_status = terminal.wait_for_exit(cx)?.await;
             let output = terminal.current_output(cx)?;

crates/agent/src/tools/thinking_tool.rs πŸ”—

@@ -43,10 +43,8 @@ impl AgentTool for ThinkingTool {
         event_stream: ToolCallEventStream,
         _cx: &mut App,
     ) -> Task<Result<String>> {
-        event_stream.update_fields(acp::ToolCallUpdateFields {
-            content: Some(vec![input.content.into()]),
-            ..Default::default()
-        });
+        event_stream
+            .update_fields(acp::ToolCallUpdateFields::new().content(vec![input.content.into()]));
         Task::ready(Ok("Finished thinking.".to_string()))
     }
 }

crates/agent/src/tools/web_search_tool.rs πŸ”—

@@ -76,10 +76,8 @@ impl AgentTool for WebSearchTool {
             let response = match search_task.await {
                 Ok(response) => response,
                 Err(err) => {
-                    event_stream.update_fields(acp::ToolCallUpdateFields {
-                        title: Some("Web Search Failed".to_string()),
-                        ..Default::default()
-                    });
+                    event_stream
+                        .update_fields(acp::ToolCallUpdateFields::new().title("Web Search Failed"));
                     return Err(err);
                 }
             };
@@ -107,26 +105,23 @@ fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream)
     } else {
         format!("{} results", response.results.len())
     };
-    event_stream.update_fields(acp::ToolCallUpdateFields {
-        title: Some(format!("Searched the web: {result_text}")),
-        content: Some(
-            response
-                .results
-                .iter()
-                .map(|result| acp::ToolCallContent::Content {
-                    content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
-                        name: result.title.clone(),
-                        uri: result.url.clone(),
-                        title: Some(result.title.clone()),
-                        description: Some(result.text.clone()),
-                        mime_type: None,
-                        annotations: None,
-                        size: None,
-                        meta: None,
-                    }),
-                })
-                .collect(),
-        ),
-        ..Default::default()
-    });
+    event_stream.update_fields(
+        acp::ToolCallUpdateFields::new()
+            .title(format!("Searched the web: {result_text}"))
+            .content(
+                response
+                    .results
+                    .iter()
+                    .map(|result| {
+                        acp::ToolCallContent::Content(acp::Content::new(
+                            acp::ContentBlock::ResourceLink(
+                                acp::ResourceLink::new(result.title.clone(), result.url.clone())
+                                    .title(result.title.clone())
+                                    .description(result.text.clone()),
+                            ),
+                        ))
+                    })
+                    .collect(),
+            ),
+    );
 }

crates/agent_servers/src/acp.rs πŸ”—

@@ -76,7 +76,7 @@ pub async fn connect(
     Ok(Rc::new(conn) as _)
 }
 
-const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
+const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1;
 
 impl AcpConnection {
     pub async fn stdio(
@@ -173,29 +173,27 @@ impl AcpConnection {
             });
         })?;
 
+        let mut client_info = acp::Implementation::new("zed", version);
+        if let Some(release_channel) = release_channel {
+            client_info = client_info.title(release_channel);
+        }
         let response = connection
-            .initialize(acp::InitializeRequest {
-                protocol_version: acp::VERSION,
-                client_capabilities: acp::ClientCapabilities {
-                    fs: acp::FileSystemCapability {
-                        read_text_file: true,
-                        write_text_file: true,
-                        meta: None,
-                    },
-                    terminal: true,
-                    meta: Some(serde_json::json!({
-                        // Experimental: Allow for rendering terminal output from the agents
-                        "terminal_output": true,
-                        "terminal-auth": true,
-                    })),
-                },
-                client_info: Some(acp::Implementation {
-                    name: "zed".to_owned(),
-                    title: release_channel.map(|c| c.to_owned()),
-                    version,
-                }),
-                meta: None,
-            })
+            .initialize(
+                acp::InitializeRequest::new(acp::ProtocolVersion::V1)
+                    .client_capabilities(
+                        acp::ClientCapabilities::new()
+                            .fs(acp::FileSystemCapability::new()
+                                .read_text_file(true)
+                                .write_text_file(true))
+                            .terminal(true)
+                            // Experimental: Allow for rendering terminal output from the agents
+                            .meta(acp::Meta::from_iter([
+                                ("terminal_output".into(), true.into()),
+                                ("terminal-auth".into(), true.into()),
+                            ])),
+                    )
+                    .client_info(client_info),
+            )
             .await?;
 
         if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
@@ -253,14 +251,13 @@ impl AgentConnection for AcpConnection {
         let default_model = self.default_model.clone();
         let cwd = cwd.to_path_buf();
         let context_server_store = project.read(cx).context_server_store().read(cx);
-        let mcp_servers =
-            if project.read(cx).is_local() {
-                context_server_store
-                    .configured_server_ids()
-                    .iter()
-                    .filter_map(|id| {
-                        let configuration = context_server_store.configuration_for_server(id)?;
-                        match &*configuration {
+        let mcp_servers = if project.read(cx).is_local() {
+            context_server_store
+                .configured_server_ids()
+                .iter()
+                .filter_map(|id| {
+                    let configuration = context_server_store.configuration_for_server(id)?;
+                    match &*configuration {
                         project::context_server_store::ContextServerConfiguration::Custom {
                             command,
                             ..
@@ -268,47 +265,41 @@ impl AgentConnection for AcpConnection {
                         | project::context_server_store::ContextServerConfiguration::Extension {
                             command,
                             ..
-                        } => Some(acp::McpServer::Stdio {
-                            name: id.0.to_string(),
-                            command: command.path.clone(),
-                            args: command.args.clone(),
-                            env: if let Some(env) = command.env.as_ref() {
-                                env.iter()
-                                    .map(|(name, value)| acp::EnvVariable {
-                                        name: name.clone(),
-                                        value: value.clone(),
-                                        meta: None,
-                                    })
-                                    .collect()
-                            } else {
-                                vec![]
-                            },
-                        }),
+                        } => Some(acp::McpServer::Stdio(
+                            acp::McpServerStdio::new(id.0.to_string(), &command.path)
+                                .args(command.args.clone())
+                                .env(if let Some(env) = command.env.as_ref() {
+                                    env.iter()
+                                        .map(|(name, value)| acp::EnvVariable::new(name, value))
+                                        .collect()
+                                } else {
+                                    vec![]
+                                }),
+                        )),
                         project::context_server_store::ContextServerConfiguration::Http {
                             url,
                             headers,
-                        } => Some(acp::McpServer::Http {
-                            name: id.0.to_string(),
-                            url: url.to_string(),
-                            headers: headers.iter().map(|(name, value)| acp::HttpHeader {
-                                name: name.clone(),
-                                value: value.clone(),
-                                meta: None,
-                            }).collect(),
-                        }),
+                        } => Some(acp::McpServer::Http(
+                            acp::McpServerHttp::new(id.0.to_string(), url.to_string()).headers(
+                                headers
+                                    .iter()
+                                    .map(|(name, value)| acp::HttpHeader::new(name, value))
+                                    .collect(),
+                            ),
+                        )),
                     }
-                    })
-                    .collect()
-            } else {
-                // In SSH projects, the external agent is running on the remote
-                // machine, and currently we only run MCP servers on the local
-                // machine. So don't pass any MCP servers to the agent in that case.
-                Vec::new()
-            };
+                })
+                .collect()
+        } else {
+            // In SSH projects, the external agent is running on the remote
+            // machine, and currently we only run MCP servers on the local
+            // machine. So don't pass any MCP servers to the agent in that case.
+            Vec::new()
+        };
 
         cx.spawn(async move |cx| {
             let response = conn
-                .new_session(acp::NewSessionRequest { mcp_servers, cwd, meta: None })
+                .new_session(acp::NewSessionRequest::new(cwd).mcp_servers(mcp_servers))
                 .await
                 .map_err(|err| {
                     if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
@@ -341,11 +332,7 @@ impl AgentConnection for AcpConnection {
                             let modes = modes.clone();
                             let conn = conn.clone();
                             async move |_| {
-                                let result = conn.set_session_mode(acp::SetSessionModeRequest {
-                                    session_id,
-                                    mode_id: default_mode,
-                                    meta: None,
-                                })
+                                let result = conn.set_session_mode(acp::SetSessionModeRequest::new(session_id, default_mode))
                                 .await.log_err();
 
                                 if result.is_none() {
@@ -388,11 +375,7 @@ impl AgentConnection for AcpConnection {
                             let models = models.clone();
                             let conn = conn.clone();
                             async move |_| {
-                                let result = conn.set_session_model(acp::SetSessionModelRequest {
-                                    session_id,
-                                    model_id: default_model,
-                                    meta: None,
-                                })
+                                let result = conn.set_session_model(acp::SetSessionModelRequest::new(session_id, default_model))
                                 .await.log_err();
 
                                 if result.is_none() {
@@ -456,12 +439,8 @@ impl AgentConnection for AcpConnection {
     fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
         let conn = self.connection.clone();
         cx.foreground_executor().spawn(async move {
-            conn.authenticate(acp::AuthenticateRequest {
-                method_id: method_id.clone(),
-                meta: None,
-            })
-            .await?;
-
+            conn.authenticate(acp::AuthenticateRequest::new(method_id))
+                .await?;
             Ok(())
         })
     }
@@ -515,10 +494,7 @@ impl AgentConnection for AcpConnection {
                                 && (details.contains("This operation was aborted")
                                     || details.contains("The user aborted a request"))
                             {
-                                Ok(acp::PromptResponse {
-                                    stop_reason: acp::StopReason::Cancelled,
-                                    meta: None,
-                                })
+                                Ok(acp::PromptResponse::new(acp::StopReason::Cancelled))
                             } else {
                                 Err(anyhow!(details))
                             }
@@ -535,10 +511,7 @@ impl AgentConnection for AcpConnection {
             session.suppress_abort_err = true;
         }
         let conn = self.connection.clone();
-        let params = acp::CancelNotification {
-            session_id: session_id.clone(),
-            meta: None,
-        };
+        let params = acp::CancelNotification::new(session_id.clone());
         cx.foreground_executor()
             .spawn(async move { conn.cancel(params).await })
             .detach();
@@ -619,11 +592,7 @@ impl acp_thread::AgentSessionModes for AcpSessionModes {
         let state = self.state.clone();
         cx.foreground_executor().spawn(async move {
             let result = connection
-                .set_session_mode(acp::SetSessionModeRequest {
-                    session_id,
-                    mode_id,
-                    meta: None,
-                })
+                .set_session_mode(acp::SetSessionModeRequest::new(session_id, mode_id))
                 .await;
 
             if result.is_err() {
@@ -682,11 +651,7 @@ impl acp_thread::AgentModelSelector for AcpModelSelector {
         let state = self.state.clone();
         cx.foreground_executor().spawn(async move {
             let result = connection
-                .set_session_model(acp::SetSessionModelRequest {
-                    session_id,
-                    model_id,
-                    meta: None,
-                })
+                .set_session_model(acp::SetSessionModelRequest::new(session_id, model_id))
                 .await;
 
             if result.is_err() {
@@ -748,10 +713,7 @@ impl acp::Client for ClientDelegate {
 
         let outcome = task.await;
 
-        Ok(acp::RequestPermissionResponse {
-            outcome,
-            meta: None,
-        })
+        Ok(acp::RequestPermissionResponse::new(outcome))
     }
 
     async fn write_text_file(
@@ -783,10 +745,7 @@ impl acp::Client for ClientDelegate {
 
         let content = task.await?;
 
-        Ok(acp::ReadTextFileResponse {
-            content,
-            meta: None,
-        })
+        Ok(acp::ReadTextFileResponse::new(content))
     }
 
     async fn session_notification(
@@ -821,7 +780,7 @@ impl acp::Client for ClientDelegate {
                 if let Some(terminal_info) = meta.get("terminal_info") {
                     if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str())
                     {
-                        let terminal_id = acp::TerminalId(id_str.into());
+                        let terminal_id = acp::TerminalId::new(id_str);
                         let cwd = terminal_info
                             .get("cwd")
                             .and_then(|v| v.as_str().map(PathBuf::from));
@@ -837,7 +796,7 @@ impl acp::Client for ClientDelegate {
                             let lower = cx.new(|cx| builder.subscribe(cx));
                             thread.on_terminal_provider_event(
                                 TerminalProviderEvent::Created {
-                                    terminal_id: terminal_id.clone(),
+                                    terminal_id,
                                     label: tc.title.clone(),
                                     cwd,
                                     output_byte_limit: None,
@@ -862,15 +821,12 @@ impl acp::Client for ClientDelegate {
             if let Some(meta) = &tcu.meta {
                 if let Some(term_out) = meta.get("terminal_output") {
                     if let Some(id_str) = term_out.get("terminal_id").and_then(|v| v.as_str()) {
-                        let terminal_id = acp::TerminalId(id_str.into());
+                        let terminal_id = acp::TerminalId::new(id_str);
                         if let Some(s) = term_out.get("data").and_then(|v| v.as_str()) {
                             let data = s.as_bytes().to_vec();
                             let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
                                 thread.on_terminal_provider_event(
-                                    TerminalProviderEvent::Output {
-                                        terminal_id: terminal_id.clone(),
-                                        data,
-                                    },
+                                    TerminalProviderEvent::Output { terminal_id, data },
                                     cx,
                                 );
                             });
@@ -881,21 +837,19 @@ impl acp::Client for ClientDelegate {
                 // terminal_exit
                 if let Some(term_exit) = meta.get("terminal_exit") {
                     if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) {
-                        let terminal_id = acp::TerminalId(id_str.into());
-                        let status = acp::TerminalExitStatus {
-                            exit_code: term_exit
-                                .get("exit_code")
-                                .and_then(|v| v.as_u64())
-                                .map(|i| i as u32),
-                            signal: term_exit
-                                .get("signal")
-                                .and_then(|v| v.as_str().map(|s| s.to_string())),
-                            meta: None,
-                        };
+                        let terminal_id = acp::TerminalId::new(id_str);
+                        let mut status = acp::TerminalExitStatus::new();
+                        if let Some(code) = term_exit.get("exit_code").and_then(|v| v.as_u64()) {
+                            status = status.exit_code(code as u32)
+                        }
+                        if let Some(signal) = term_exit.get("signal").and_then(|v| v.as_str()) {
+                            status = status.signal(signal);
+                        }
+
                         let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
                             thread.on_terminal_provider_event(
                                 TerminalProviderEvent::Exit {
-                                    terminal_id: terminal_id.clone(),
+                                    terminal_id,
                                     status,
                                 },
                                 cx,
@@ -932,7 +886,7 @@ impl acp::Client for ClientDelegate {
         // Register with renderer
         let terminal_entity = thread.update(&mut self.cx.clone(), |thread, cx| {
             thread.register_terminal_created(
-                acp::TerminalId(uuid::Uuid::new_v4().to_string().into()),
+                acp::TerminalId::new(uuid::Uuid::new_v4().to_string()),
                 format!("{} {}", args.command, args.args.join(" ")),
                 args.cwd.clone(),
                 args.output_byte_limit,
@@ -942,10 +896,7 @@ impl acp::Client for ClientDelegate {
         })?;
         let terminal_id =
             terminal_entity.read_with(&self.cx, |terminal, _| terminal.id().clone())?;
-        Ok(acp::CreateTerminalResponse {
-            terminal_id,
-            meta: None,
-        })
+        Ok(acp::CreateTerminalResponse::new(terminal_id))
     }
 
     async fn kill_terminal_command(
@@ -1006,10 +957,7 @@ impl acp::Client for ClientDelegate {
             })??
             .await;
 
-        Ok(acp::WaitForTerminalExitResponse {
-            exit_status,
-            meta: None,
-        })
+        Ok(acp::WaitForTerminalExitResponse::new(exit_status))
     }
 }
 

crates/agent_servers/src/claude.rs πŸ”—

@@ -41,7 +41,7 @@ impl AgentServer for ClaudeCode {
 
         settings
             .as_ref()
-            .and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
+            .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
     }
 
     fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
@@ -62,7 +62,7 @@ impl AgentServer for ClaudeCode {
 
         settings
             .as_ref()
-            .and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
+            .and_then(|s| s.default_model.clone().map(acp::ModelId::new))
     }
 
     fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {

crates/agent_servers/src/codex.rs πŸ”—

@@ -42,7 +42,7 @@ impl AgentServer for Codex {
 
         settings
             .as_ref()
-            .and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
+            .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
     }
 
     fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
@@ -63,7 +63,7 @@ impl AgentServer for Codex {
 
         settings
             .as_ref()
-            .and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
+            .and_then(|s| s.default_model.clone().map(acp::ModelId::new))
     }
 
     fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {

crates/agent_servers/src/custom.rs πŸ”—

@@ -44,7 +44,7 @@ impl crate::AgentServer for CustomAgentServer {
 
         settings
             .as_ref()
-            .and_then(|s| s.default_mode().map(|m| acp::SessionModeId(m.into())))
+            .and_then(|s| s.default_mode().map(acp::SessionModeId::new))
     }
 
     fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
@@ -80,7 +80,7 @@ impl crate::AgentServer for CustomAgentServer {
 
         settings
             .as_ref()
-            .and_then(|s| s.default_model().map(|m| acp::ModelId(m.into())))
+            .and_then(|s| s.default_model().map(acp::ModelId::new))
     }
 
     fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {

crates/agent_servers/src/e2e_tests.rs πŸ”—

@@ -82,26 +82,9 @@ where
         .update(cx, |thread, cx| {
             thread.send(
                 vec![
-                    acp::ContentBlock::Text(acp::TextContent {
-                        text: "Read the file ".into(),
-                        annotations: None,
-                        meta: None,
-                    }),
-                    acp::ContentBlock::ResourceLink(acp::ResourceLink {
-                        uri: "foo.rs".into(),
-                        name: "foo.rs".into(),
-                        annotations: None,
-                        description: None,
-                        mime_type: None,
-                        size: None,
-                        title: None,
-                        meta: None,
-                    }),
-                    acp::ContentBlock::Text(acp::TextContent {
-                        text: " and tell me what the content of the println! is".into(),
-                        annotations: None,
-                        meta: None,
-                    }),
+                    "Read the file ".into(),
+                    acp::ContentBlock::ResourceLink(acp::ResourceLink::new("foo.rs", "foo.rs")),
+                    " and tell me what the content of the println! is".into(),
                 ],
                 cx,
             )
@@ -429,7 +412,7 @@ macro_rules! common_e2e_tests {
             async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) {
                 $crate::e2e_tests::test_tool_call_with_permission(
                     $server,
-                    ::agent_client_protocol::PermissionOptionId($allow_option_id.into()),
+                    ::agent_client_protocol::PermissionOptionId::new($allow_option_id),
                     cx,
                 )
                 .await;

crates/agent_ui/src/acp/entry_view_state.rs πŸ”—

@@ -432,24 +432,11 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let tool_call = acp::ToolCall {
-            id: acp::ToolCallId("tool".into()),
-            title: "Tool call".into(),
-            kind: acp::ToolKind::Other,
-            status: acp::ToolCallStatus::InProgress,
-            content: vec![acp::ToolCallContent::Diff {
-                diff: acp::Diff {
-                    path: "/project/hello.txt".into(),
-                    old_text: Some("hi world".into()),
-                    new_text: "hello world".into(),
-                    meta: None,
-                },
-            }],
-            locations: vec![],
-            raw_input: None,
-            raw_output: None,
-            meta: None,
-        };
+        let tool_call = acp::ToolCall::new("tool", "Tool call")
+            .status(acp::ToolCallStatus::InProgress)
+            .content(vec![acp::ToolCallContent::Diff(
+                acp::Diff::new("/project/hello.txt", "hello world").old_text("hi world"),
+            )]);
         let connection = Rc::new(StubAgentConnection::new());
         let thread = cx
             .update(|_, cx| {

crates/agent_ui/src/acp/message_editor.rs πŸ”—

@@ -225,8 +225,13 @@ impl MessageEditor {
             .iter()
             .find(|command| command.name == command_name)?;
 
-        let acp::AvailableCommandInput::Unstructured { mut hint } =
-            available_command.input.clone()?;
+        let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput {
+            mut hint,
+            ..
+        }) = available_command.input.clone()?
+        else {
+            return None;
+        };
 
         let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;
         if hint_pos > snapshot.len() {
@@ -403,34 +408,28 @@ impl MessageEditor {
                             } => {
                                 all_tracked_buffers.extend(tracked_buffers.iter().cloned());
                                 if supports_embedded_context {
-                                    acp::ContentBlock::Resource(acp::EmbeddedResource {
-                                        annotations: None,
-                                        resource:
-                                            acp::EmbeddedResourceResource::TextResourceContents(
-                                                acp::TextResourceContents {
-                                                    mime_type: None,
-                                                    text: content.clone(),
-                                                    uri: uri.to_uri().to_string(),
-                                                    meta: None,
-                                                },
+                                    acp::ContentBlock::Resource(acp::EmbeddedResource::new(
+                                        acp::EmbeddedResourceResource::TextResourceContents(
+                                            acp::TextResourceContents::new(
+                                                content.clone(),
+                                                uri.to_uri().to_string(),
                                             ),
-                                        meta: None,
-                                    })
+                                        ),
+                                    ))
                                 } else {
-                                    acp::ContentBlock::ResourceLink(acp::ResourceLink {
-                                        name: uri.name(),
-                                        uri: uri.to_uri().to_string(),
-                                        annotations: None,
-                                        description: None,
-                                        mime_type: None,
-                                        size: None,
-                                        title: None,
-                                        meta: None,
-                                    })
+                                    acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
+                                        uri.name(),
+                                        uri.to_uri().to_string(),
+                                    ))
                                 }
                             }
                             Mention::Image(mention_image) => {
-                                let uri = match uri {
+                                let mut image = acp::ImageContent::new(
+                                    mention_image.data.clone(),
+                                    mention_image.format.mime_type(),
+                                );
+
+                                if let Some(uri) = match uri {
                                     MentionUri::File { .. } => Some(uri.to_uri().to_string()),
                                     MentionUri::PastedImage => None,
                                     other => {
@@ -440,25 +439,14 @@ impl MessageEditor {
                                         );
                                         None
                                     }
+                                } {
+                                    image = image.uri(uri)
                                 };
-                                acp::ContentBlock::Image(acp::ImageContent {
-                                    annotations: None,
-                                    data: mention_image.data.to_string(),
-                                    mime_type: mention_image.format.mime_type().into(),
-                                    uri,
-                                    meta: None,
-                                })
+                                acp::ContentBlock::Image(image)
                             }
-                            Mention::Link => acp::ContentBlock::ResourceLink(acp::ResourceLink {
-                                name: uri.name(),
-                                uri: uri.to_uri().to_string(),
-                                annotations: None,
-                                description: None,
-                                mime_type: None,
-                                size: None,
-                                title: None,
-                                meta: None,
-                            }),
+                            Mention::Link => acp::ContentBlock::ResourceLink(
+                                acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()),
+                            ),
                         };
                         chunks.push(chunk);
                         ix = crease_range.end.0;
@@ -746,8 +734,7 @@ impl MessageEditor {
                     uri,
                     data,
                     mime_type,
-                    annotations: _,
-                    meta: _,
+                    ..
                 }) => {
                     let mention_uri = if let Some(uri) = uri {
                         MentionUri::parse(&uri, path_style)
@@ -773,7 +760,7 @@ impl MessageEditor {
                         }),
                     ));
                 }
-                acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
+                _ => {}
             }
         }
 
@@ -1092,12 +1079,7 @@ mod tests {
         assert!(error_message.contains("Available commands: none"));
 
         // Now simulate Claude providing its list of available commands (which doesn't include file)
-        available_commands.replace(vec![acp::AvailableCommand {
-            name: "help".to_string(),
-            description: "Get help".to_string(),
-            input: None,
-            meta: None,
-        }]);
+        available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
 
         // Test that unsupported slash commands trigger an error when we have a list of available commands
         editor.update_in(cx, |editor, window, cx| {
@@ -1211,20 +1193,12 @@ mod tests {
         let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
         let available_commands = Rc::new(RefCell::new(vec![
-            acp::AvailableCommand {
-                name: "quick-math".to_string(),
-                description: "2 + 2 = 4 - 1 = 3".to_string(),
-                input: None,
-                meta: None,
-            },
-            acp::AvailableCommand {
-                name: "say-hello".to_string(),
-                description: "Say hello to whoever you want".to_string(),
-                input: Some(acp::AvailableCommandInput::Unstructured {
-                    hint: "<name>".to_string(),
-                }),
-                meta: None,
-            },
+            acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
+            acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
+                acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
+                    "<name>",
+                )),
+            ),
         ]));
 
         let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
@@ -1504,12 +1478,12 @@ mod tests {
             editor.set_text("", window, cx);
         });
 
-        prompt_capabilities.replace(acp::PromptCapabilities {
-            image: true,
-            audio: true,
-            embedded_context: true,
-            meta: None,
-        });
+        prompt_capabilities.replace(
+            acp::PromptCapabilities::new()
+                .image(true)
+                .audio(true)
+                .embedded_context(true),
+        );
 
         cx.simulate_input("Lorem ");
 
@@ -1960,11 +1934,9 @@ mod tests {
                     cx,
                 );
                 // Enable embedded context so files are actually included
-                editor.prompt_capabilities.replace(acp::PromptCapabilities {
-                    embedded_context: true,
-                    meta: None,
-                    ..Default::default()
-                });
+                editor
+                    .prompt_capabilities
+                    .replace(acp::PromptCapabilities::new().embedded_context(true));
                 editor
             })
         });
@@ -2043,7 +2015,7 @@ mod tests {
 
         // Create a thread metadata to insert as summary
         let thread_metadata = agent::DbThreadMetadata {
-            id: acp::SessionId("thread-123".into()),
+            id: acp::SessionId::new("thread-123"),
             title: "Previous Conversation".into(),
             updated_at: chrono::Utc::now(),
         };
@@ -2150,14 +2122,7 @@ mod tests {
             .await
             .unwrap();
 
-        assert_eq!(
-            content,
-            vec![acp::ContentBlock::Text(acp::TextContent {
-                text: "してhello world".into(),
-                annotations: None,
-                meta: None
-            })]
-        );
+        assert_eq!(content, vec!["してhello world".into()]);
     }
 
     #[gpui::test]
@@ -2236,38 +2201,24 @@ mod tests {
             .0;
 
         let main_rs_uri = if cfg!(windows) {
-            "file:///C:/project/src/main.rs".to_string()
+            "file:///C:/project/src/main.rs"
         } else {
-            "file:///project/src/main.rs".to_string()
+            "file:///project/src/main.rs"
         };
 
         // When embedded context is `false` we should get a resource link
         pretty_assertions::assert_eq!(
             content,
             vec![
-                acp::ContentBlock::Text(acp::TextContent {
-                    text: "What is in ".to_string(),
-                    annotations: None,
-                    meta: None
-                }),
-                acp::ContentBlock::ResourceLink(acp::ResourceLink {
-                    uri: main_rs_uri.clone(),
-                    name: "main.rs".to_string(),
-                    annotations: None,
-                    meta: None,
-                    description: None,
-                    mime_type: None,
-                    size: None,
-                    title: None,
-                })
+                "What is in ".into(),
+                acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
             ]
         );
 
         message_editor.update(cx, |editor, _cx| {
-            editor.prompt_capabilities.replace(acp::PromptCapabilities {
-                embedded_context: true,
-                ..Default::default()
-            })
+            editor
+                .prompt_capabilities
+                .replace(acp::PromptCapabilities::new().embedded_context(true))
         });
 
         let content = message_editor
@@ -2280,23 +2231,12 @@ mod tests {
         pretty_assertions::assert_eq!(
             content,
             vec![
-                acp::ContentBlock::Text(acp::TextContent {
-                    text: "What is in ".to_string(),
-                    annotations: None,
-                    meta: None
-                }),
-                acp::ContentBlock::Resource(acp::EmbeddedResource {
-                    resource: acp::EmbeddedResourceResource::TextResourceContents(
-                        acp::TextResourceContents {
-                            text: file_content.to_string(),
-                            uri: main_rs_uri,
-                            mime_type: None,
-                            meta: None
-                        }
-                    ),
-                    annotations: None,
-                    meta: None
-                })
+                "What is in ".into(),
+                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
+                    acp::EmbeddedResourceResource::TextResourceContents(
+                        acp::TextResourceContents::new(file_content, main_rs_uri)
+                    )
+                ))
             ]
         );
     }

crates/agent_ui/src/acp/model_selector.rs πŸ”—

@@ -464,7 +464,7 @@ mod tests {
                     models
                         .into_iter()
                         .map(|model| acp_thread::AgentModelInfo {
-                            id: acp::ModelId(model.to_string().into()),
+                            id: acp::ModelId::new(model.to_string()),
                             name: model.to_string().into(),
                             description: None,
                             icon: None,

crates/agent_ui/src/acp/thread_view.rs πŸ”—

@@ -1476,18 +1476,8 @@ impl AcpThreadView {
                     .iter()
                     .any(|method| method.id.0.as_ref() == "claude-login")
                 {
-                    available_commands.push(acp::AvailableCommand {
-                        name: "login".to_owned(),
-                        description: "Authenticate".to_owned(),
-                        input: None,
-                        meta: None,
-                    });
-                    available_commands.push(acp::AvailableCommand {
-                        name: "logout".to_owned(),
-                        description: "Authenticate".to_owned(),
-                        input: None,
-                        meta: None,
-                    });
+                    available_commands.push(acp::AvailableCommand::new("login", "Authenticate"));
+                    available_commands.push(acp::AvailableCommand::new("logout", "Authenticate"));
                 }
 
                 let has_commands = !available_commands.is_empty();
@@ -2562,7 +2552,7 @@ impl AcpThreadView {
                 acp::ToolKind::Think => IconName::ToolThink,
                 acp::ToolKind::Fetch => IconName::ToolWeb,
                 acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
-                acp::ToolKind::Other => IconName::ToolHammer,
+                acp::ToolKind::Other | _ => IconName::ToolHammer,
             })
         }
         .size(IconSize::Small)
@@ -2814,7 +2804,7 @@ impl AcpThreadView {
             })
             .gap_0p5()
             .children(options.iter().map(move |option| {
-                let option_id = SharedString::from(option.id.0.clone());
+                let option_id = SharedString::from(option.option_id.0.clone());
                 Button::new((option_id, entry_ix), option.name.clone())
                     .map(|this| {
                         let (this, action) = match option.kind {
@@ -2830,7 +2820,7 @@ impl AcpThreadView {
                                 this.icon(IconName::Close).icon_color(Color::Error),
                                 Some(&RejectOnce as &dyn Action),
                             ),
-                            acp::PermissionOptionKind::RejectAlways => {
+                            acp::PermissionOptionKind::RejectAlways | _ => {
                                 (this.icon(IconName::Close).icon_color(Color::Error), None)
                             }
                         };
@@ -2855,7 +2845,7 @@ impl AcpThreadView {
                     .label_size(LabelSize::Small)
                     .on_click(cx.listener({
                         let tool_call_id = tool_call_id.clone();
-                        let option_id = option.id.clone();
+                        let option_id = option.option_id.clone();
                         let option_kind = option.kind;
                         move |this, _, window, cx| {
                             this.authorize_tool_call(
@@ -3543,7 +3533,7 @@ impl AcpThreadView {
                                                 );
 
                                                 this.authenticate(
-                                                    acp::AuthMethodId(method_id.clone()),
+                                                    acp::AuthMethodId::new(method_id.clone()),
                                                     window,
                                                     cx,
                                                 )
@@ -3837,10 +3827,6 @@ impl AcpThreadView {
                             .text_xs()
                             .text_color(cx.theme().colors().text_muted)
                             .child(match entry.status {
-                                acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
-                                    .size(IconSize::Small)
-                                    .color(Color::Muted)
-                                    .into_any_element(),
                                 acp::PlanEntryStatus::InProgress => {
                                     Icon::new(IconName::TodoProgress)
                                         .size(IconSize::Small)
@@ -3854,6 +3840,12 @@ impl AcpThreadView {
                                         .color(Color::Success)
                                         .into_any_element()
                                 }
+                                acp::PlanEntryStatus::Pending | _ => {
+                                    Icon::new(IconName::TodoPending)
+                                        .size(IconSize::Small)
+                                        .color(Color::Muted)
+                                        .into_any_element()
+                                }
                             })
                             .child(MarkdownElement::new(
                                 entry.content.clone(),
@@ -4427,7 +4419,7 @@ impl AcpThreadView {
 
         self.authorize_tool_call(
             tool_call.id.clone(),
-            option.id.clone(),
+            option.option_id.clone(),
             option.kind,
             window,
             cx,
@@ -6243,27 +6235,18 @@ pub(crate) mod tests {
     async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
         init_test(cx);
 
-        let tool_call_id = acp::ToolCallId("1".into());
-        let tool_call = acp::ToolCall {
-            id: tool_call_id.clone(),
-            title: "Label".into(),
-            kind: acp::ToolKind::Edit,
-            status: acp::ToolCallStatus::Pending,
-            content: vec!["hi".into()],
-            locations: vec![],
-            raw_input: None,
-            raw_output: None,
-            meta: None,
-        };
+        let tool_call_id = acp::ToolCallId::new("1");
+        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label")
+            .kind(acp::ToolKind::Edit)
+            .content(vec!["hi".into()]);
         let connection =
             StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
                 tool_call_id,
-                vec![acp::PermissionOption {
-                    id: acp::PermissionOptionId("1".into()),
-                    name: "Allow".into(),
-                    kind: acp::PermissionOptionKind::AllowOnce,
-                    meta: None,
-                }],
+                vec![acp::PermissionOption::new(
+                    "1".into(),
+                    "Allow",
+                    acp::PermissionOptionKind::AllowOnce,
+                )],
             )]));
 
         connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
@@ -6482,10 +6465,7 @@ pub(crate) mod tests {
         fn default_response() -> Self {
             let conn = StubAgentConnection::new();
             conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
-                acp::ContentChunk {
-                    content: "Default response".into(),
-                    meta: None,
-                },
+                acp::ContentChunk::new("Default response".into()),
             )]);
             Self::new(conn)
         }
@@ -6542,13 +6522,13 @@ pub(crate) mod tests {
                     self,
                     project,
                     action_log,
-                    SessionId("test".into()),
-                    watch::Receiver::constant(acp::PromptCapabilities {
-                        image: true,
-                        audio: true,
-                        embedded_context: true,
-                        meta: None,
-                    }),
+                    SessionId::new("test"),
+                    watch::Receiver::constant(
+                        acp::PromptCapabilities::new()
+                            .image(true)
+                            .audio(true)
+                            .embedded_context(true),
+                    ),
                     cx,
                 )
             })))
@@ -6606,13 +6586,13 @@ pub(crate) mod tests {
                     self,
                     project,
                     action_log,
-                    SessionId("test".into()),
-                    watch::Receiver::constant(acp::PromptCapabilities {
-                        image: true,
-                        audio: true,
-                        embedded_context: true,
-                        meta: None,
-                    }),
+                    SessionId::new("test"),
+                    watch::Receiver::constant(
+                        acp::PromptCapabilities::new()
+                            .image(true)
+                            .audio(true)
+                            .embedded_context(true),
+                    ),
                     cx,
                 )
             })))
@@ -6636,10 +6616,7 @@ pub(crate) mod tests {
             _params: acp::PromptRequest,
             _cx: &mut App,
         ) -> Task<gpui::Result<acp::PromptResponse>> {
-            Task::ready(Ok(acp::PromptResponse {
-                stop_reason: acp::StopReason::Refusal,
-                meta: None,
-            }))
+            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
         }
 
         fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
@@ -6707,24 +6684,14 @@ pub(crate) mod tests {
             .unwrap();
 
         // First user message
-        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
-            id: acp::ToolCallId("tool1".into()),
-            title: "Edit file 1".into(),
-            kind: acp::ToolKind::Edit,
-            status: acp::ToolCallStatus::Completed,
-            content: vec![acp::ToolCallContent::Diff {
-                diff: acp::Diff {
-                    path: "/project/test1.txt".into(),
-                    old_text: Some("old content 1".into()),
-                    new_text: "new content 1".into(),
-                    meta: None,
-                },
-            }],
-            locations: vec![],
-            raw_input: None,
-            raw_output: None,
-            meta: None,
-        })]);
+        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
+            acp::ToolCall::new("tool1", "Edit file 1")
+                .kind(acp::ToolKind::Edit)
+                .status(acp::ToolCallStatus::Completed)
+                .content(vec![acp::ToolCallContent::Diff(
+                    acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"),
+                )]),
+        )]);
 
         thread
             .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
@@ -6750,24 +6717,14 @@ pub(crate) mod tests {
         });
 
         // Second user message
-        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
-            id: acp::ToolCallId("tool2".into()),
-            title: "Edit file 2".into(),
-            kind: acp::ToolKind::Edit,
-            status: acp::ToolCallStatus::Completed,
-            content: vec![acp::ToolCallContent::Diff {
-                diff: acp::Diff {
-                    path: "/project/test2.txt".into(),
-                    old_text: Some("old content 2".into()),
-                    new_text: "new content 2".into(),
-                    meta: None,
-                },
-            }],
-            locations: vec![],
-            raw_input: None,
-            raw_output: None,
-            meta: None,
-        })]);
+        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
+            acp::ToolCall::new("tool2", "Edit file 2")
+                .kind(acp::ToolKind::Edit)
+                .status(acp::ToolCallStatus::Completed)
+                .content(vec![acp::ToolCallContent::Diff(
+                    acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"),
+                )]),
+        )]);
 
         thread
             .update(cx, |thread, cx| thread.send_raw("Another one", cx))
@@ -6841,14 +6798,7 @@ pub(crate) mod tests {
         let connection = StubAgentConnection::new();
 
         connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
-            acp::ContentChunk {
-                content: acp::ContentBlock::Text(acp::TextContent {
-                    text: "Response".into(),
-                    annotations: None,
-                    meta: None,
-                }),
-                meta: None,
-            },
+            acp::ContentChunk::new("Response".into()),
         )]);
 
         let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -6934,14 +6884,7 @@ pub(crate) mod tests {
         let connection = StubAgentConnection::new();
 
         connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
-            acp::ContentChunk {
-                content: acp::ContentBlock::Text(acp::TextContent {
-                    text: "Response".into(),
-                    annotations: None,
-                    meta: None,
-                }),
-                meta: None,
-            },
+            acp::ContentChunk::new("Response".into()),
         )]);
 
         let (thread_view, cx) =
@@ -6981,14 +6924,7 @@ pub(crate) mod tests {
 
         // Send
         connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
-            acp::ContentChunk {
-                content: acp::ContentBlock::Text(acp::TextContent {
-                    text: "New Response".into(),
-                    annotations: None,
-                    meta: None,
-                }),
-                meta: None,
-            },
+            acp::ContentChunk::new("New Response".into()),
         )]);
 
         user_message_editor.update_in(cx, |_editor, window, cx| {
@@ -7076,14 +7012,7 @@ pub(crate) mod tests {
         cx.update(|_, cx| {
             connection.send_update(
                 session_id.clone(),
-                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
-                    content: acp::ContentBlock::Text(acp::TextContent {
-                        text: "Response".into(),
-                        annotations: None,
-                        meta: None,
-                    }),
-                    meta: None,
-                }),
+                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
                 cx,
             );
             connection.end_turn(session_id, acp::StopReason::EndTurn);
@@ -7135,10 +7064,9 @@ pub(crate) mod tests {
         cx.update(|_, cx| {
             connection.send_update(
                 session_id.clone(),
-                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
-                    content: "Message 1 resp".into(),
-                    meta: None,
-                }),
+                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
+                    "Message 1 resp".into(),
+                )),
                 cx,
             );
         });
@@ -7172,10 +7100,7 @@ pub(crate) mod tests {
             // Simulate a response sent after beginning to cancel
             connection.send_update(
                 session_id.clone(),
-                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
-                    content: "onse".into(),
-                    meta: None,
-                }),
+                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
                 cx,
             );
         });
@@ -7206,10 +7131,9 @@ pub(crate) mod tests {
         cx.update(|_, cx| {
             connection.send_update(
                 session_id.clone(),
-                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
-                    content: "Message 2 response".into(),
-                    meta: None,
-                }),
+                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
+                    "Message 2 response".into(),
+                )),
                 cx,
             );
             connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
@@ -7248,14 +7172,7 @@ pub(crate) mod tests {
 
         let connection = StubAgentConnection::new();
         connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
-            acp::ContentChunk {
-                content: acp::ContentBlock::Text(acp::TextContent {
-                    text: "Response".into(),
-                    annotations: None,
-                    meta: None,
-                }),
-                meta: None,
-            },
+            acp::ContentChunk::new("Response".into()),
         )]);
 
         let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -7334,14 +7251,7 @@ pub(crate) mod tests {
 
         let connection = StubAgentConnection::new();
         connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
-            acp::ContentChunk {
-                content: acp::ContentBlock::Text(acp::TextContent {
-                    text: "Response".into(),
-                    annotations: None,
-                    meta: None,
-                }),
-                meta: None,
-            },
+            acp::ContentChunk::new("Response".into()),
         )]);
 
         let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;

crates/eval/src/example.rs πŸ”—

@@ -261,7 +261,7 @@ impl ExampleContext {
                             .expect("Unknown tool_name content in meta");
 
                         tool_uses_by_id.insert(
-                            tool_call.id,
+                            tool_call.tool_call_id,
                             ToolUse {
                                 name: tool_name.to_string(),
                                 value: tool_call.raw_input.unwrap_or_default(),
@@ -277,7 +277,9 @@ impl ExampleContext {
                     ThreadEvent::ToolCallUpdate(tool_call_update) => {
                         if let acp_thread::ToolCallUpdate::UpdateFields(update) = tool_call_update {
                             if let Some(raw_input) = update.fields.raw_input {
-                                if let Some(tool_use) = tool_uses_by_id.get_mut(&update.id) {
+                                if let Some(tool_use) =
+                                    tool_uses_by_id.get_mut(&update.tool_call_id)
+                                {
                                     tool_use.value = raw_input;
                                 }
                             }
@@ -290,7 +292,7 @@ impl ExampleContext {
                                     update.fields.status == Some(acp::ToolCallStatus::Completed);
 
                                 let tool_use = tool_uses_by_id
-                                    .remove(&update.id)
+                                    .remove(&update.tool_call_id)
                                     .expect("Unrecognized tool call completed");
 
                                 let log_message = if succeeded {
@@ -337,10 +339,7 @@ impl ExampleContext {
                         acp::StopReason::MaxTurnRequests => {
                             return Err(anyhow!("Exceeded maximum turn requests"));
                         }
-                        acp::StopReason::Refusal => {
-                            return Err(anyhow!("Refusal"));
-                        }
-                        acp::StopReason::Cancelled => return Err(anyhow!("Cancelled")),
+                        stop_reason => return Err(anyhow!("{stop_reason:?}")),
                     },
                 }
             }

crates/eval/src/instance.rs πŸ”—

@@ -303,13 +303,12 @@ impl ExampleInstance {
                 let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 
                 let thread = if let Some(json) = &meta.existing_thread_json {
-                    let session_id = acp::SessionId(
+                    let session_id = acp::SessionId::new(
                         rand::rng()
                             .sample_iter(&distr::Alphanumeric)
                             .take(7)
                             .map(char::from)
-                            .collect::<String>()
-                            .into(),
+                            .collect::<String>(),
                     );
 
                     let db_thread = agent::DbThread::from_json(json.as_bytes()).expect("Can't read serialized thread");
@@ -640,7 +639,7 @@ impl agent::ThreadEnvironment for EvalThreadEnvironment {
         cx.spawn(async move |cx| {
             let language_registry =
                 project.read_with(cx, |project, _cx| project.languages().clone())?;
-            let id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
+            let id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
             let terminal =
                 acp_thread::create_terminal_entity(command, &[], vec![], cwd.clone(), &project, cx)
                     .await?;