agent: Use ACP v2 content for native prompts (#65417)
Some checks failed
deploy_nightly_docs / deploy_docs (push) Has been cancelled
Congratsbot / check-author (push) Has been cancelled
run_tests / orchestrate (push) Has been cancelled
run_tests / check_style (push) Has been cancelled
Congratsbot / congrats (push) Has been cancelled
run_tests / clippy_windows (push) Has been cancelled
run_tests / clippy_linux (push) Has been cancelled
run_tests / clippy_mac (push) Has been cancelled
run_tests / clippy_mac_x86_64 (push) Has been cancelled
run_tests / run_tests_windows (push) Has been cancelled
run_tests / miri_scheduler (push) Has been cancelled
run_tests / doctests (push) Has been cancelled
run_tests / check_dependencies (push) Has been cancelled
run_tests / check_docs (push) Has been cancelled
run_tests / check_remote_server_mac_aarch64 (push) Has been cancelled
run_tests / check_remote_server_windows_x86_64 (push) Has been cancelled
run_tests / check_postgres_and_protobuf_migrations (push) Has been cancelled
run_tests / extension_tests (push) Has been cancelled
run_tests / tests_pass (push) Has been cancelled
run_tests / run_tests_linux (push) Has been cancelled
run_tests / run_tests_mac (push) Has been cancelled
run_tests / check_workspace_binaries (push) Has been cancelled
run_tests / check_wasm (push) Has been cancelled
run_tests / check_licenses (push) Has been cancelled
run_tests / check_scripts (push) Has been cancelled
run_tests / check_remote_server_linux_x86_64 (push) Has been cancelled

Remove the native agent's internal ACP v2-to-v1 prompt-content round
trip. Native prompt routing, slash-command parsing, MCP/skill dispatch,
thread insertion and user-message event conversion now consume v2
content directly.

- Preserve mention handling, attachment ordering, inline skill
continuation, raw source blocks and submitted v2
metadata/icons/annotations in live shared state.
- Build MCP content directly as v2 and retain audience/priority
annotations.
- Preserve existing provider projections for images, audio/blob
placeholders and assistant messages. Persistence and replay remain
provider-shaped; this does not introduce lossless raw-source
persistence.
- Propagate unsupported-content errors before command dispatch/insertion
rather than silently discarding unknown blocks.

Release Notes:

- N/A
This commit is contained in:
Ben Brandt 2026-10-09 19:44:36 +00:00 • committed by GitHub
parent f54e09c151
commit f16f9652ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 321 additions and 123 deletions

View file

@ -1958,7 +1958,7 @@ impl NativeAgent {
prompt_name: String,
server_id: ContextServerId,
arguments: HashMap<String, String>,
original_content: Vec<acp_v1::ContentBlock>,
original_content: Vec<acp_v2::ContentBlock>,
cx: &mut Context<Self>,
) -> Task<Result<acp_v1::PromptResponse>> {
let Some(state) = self.session_project_state(&session_id) else {
@ -1995,13 +1995,13 @@ impl NativeAgent {
original_content.into_iter().skip(1),
path_style,
cx,
);
});
)
})?;
for message in prompt.messages {
let context_server::types::PromptMessage { role, content } = message;
let block = mcp_message_content_to_acp_content_block(content);
let display_block = acp_thread::content::from_v1(block.clone())?;
let display_block = block.clone();
match role {
context_server::types::Role::User => {
@ -2017,8 +2017,8 @@ impl NativeAgent {
});
thread.update(cx, |thread, cx| {
thread.push_acp_user_block(id, [block], path_style, cx);
});
thread.push_acp_user_block(id, [block], path_style, cx)
})?;
}
context_server::types::Role::Assistant => {
acp_thread.update(cx, |acp_thread, cx| {
@ -2030,9 +2030,7 @@ impl NativeAgent {
);
});
thread.update(cx, |thread, cx| {
thread.push_acp_agent_block(block, cx);
});
thread.update(cx, |thread, cx| thread.push_acp_agent_block(block, cx))?;
}
}
@ -2114,7 +2112,7 @@ impl NativeAgent {
client_user_message_id: ClientUserMessageId,
session_id: acp_v2::SessionId,
skill: Skill,
original_content: Vec<acp_v1::ContentBlock>,
original_content: Vec<acp_v2::ContentBlock>,
cx: &mut Context<Self>,
) -> Task<Result<acp_v1::PromptResponse>> {
let Some(state) = self.session_project_state(&session_id) else {
@ -2158,10 +2156,10 @@ impl NativeAgent {
})?
};
let envelope = crate::tools::render_skill_envelope(&skill, &body);
let envelope_block = acp_v1::ContentBlock::Text(acp_v1::TextContent::new(envelope));
let envelope_block = acp_v2::ContentBlock::Text(acp_v2::TextContent::new(envelope));
let mut user_blocks = original_content;
if let Some(acp_v1::ContentBlock::Text(text_content)) = user_blocks.first_mut() {
if let Some(acp_v2::ContentBlock::Text(text_content)) = user_blocks.first_mut() {
let stripped = strip_slash_command_prefix(&text_content.text);
if stripped.trim().is_empty() {
user_blocks.remove(0);
@ -2175,7 +2173,7 @@ impl NativeAgent {
// user's own typed message is already rendered by the normal
// prompt flow, so we don't push it to the UI again here.
let injected_id = acp_thread::ClientUserMessageId::new();
let display_block = acp_thread::content::from_v1(envelope_block.clone())?;
let display_block = envelope_block.clone();
acp_thread.update(cx, |acp_thread, cx| {
acp_thread.push_user_content_block_with_indent(
Some(injected_id),
@ -2192,8 +2190,8 @@ impl NativeAgent {
combined.extend(user_blocks);
thread.update(cx, |thread, cx| {
thread.push_acp_user_block(client_user_message_id, combined, path_style, cx);
});
thread.push_acp_user_block(client_user_message_id, combined, path_style, cx)
})?;
let response_stream = thread.update(cx, |thread, cx| thread.send_existing(cx))?;
@ -2311,8 +2309,8 @@ impl NativeAgentConnection {
match event {
ThreadEvent::UserMessage(message) => {
let content = message.content.iter().cloned()
.map(|content| acp_thread::content::from_v1(content.into()))
.collect::<Result<Vec<_>>>()?;
.map(acp_v2::ContentBlock::from)
.collect::<Vec<_>>();
acp_thread.update(cx, |thread, cx| {
for content in content {
thread.push_user_content_block(
@ -2514,8 +2512,8 @@ impl<'a> Command<'a> {
&& self.skill_scope.is_none()
}
fn parse(prompt: &'a [acp_v1::ContentBlock]) -> Option<Self> {
let acp_v1::ContentBlock::Text(text_content) = prompt.first()? else {
fn parse(prompt: &'a [acp_v2::ContentBlock]) -> Option<Self> {
let acp_v2::ContentBlock::Text(text_content) = prompt.first()? else {
return None;
};
let text = text_content.text.trim();
@ -2868,7 +2866,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}
fn validate_prompt_content(&self, content: &[acp_v2::ContentBlock]) -> Result<()> {
acp_thread::content::validate_prompt_content_for_v1(content)
content
.iter()
.try_for_each(UserMessageContent::validate_content_block)
}
fn prompt(
@ -2962,10 +2962,6 @@ impl acp_thread::AgentSessionClientUserMessageIds for NativeAgentConnection {
cx: &mut App,
) -> Task<Result<acp_v1::PromptResponse>> {
let session_id = params.session_id.clone();
let params = match acp_thread::content::prompt_to_v1(params) {
Ok(params) => params,
Err(error) => return Task::ready(Err(error)),
};
log::info!("Received prompt request for session: {}", session_id);
log::debug!("Prompt blocks count: {}", params.prompt.len());
@ -2980,6 +2976,12 @@ impl acp_thread::AgentSessionClientUserMessageIds for NativeAgentConnection {
return Task::ready(Err(anyhow::anyhow!("Session not found")));
};
if let Err(error) =
acp_thread::AgentConnection::validate_prompt_content(self, &params.prompt)
{
return Task::ready(Err(error));
}
if let Some(parsed_command) = Command::parse(&params.prompt) {
if parsed_command.is_unqualified(COMPACT_COMMAND_NAME) {
return self.0.update(cx, |agent, cx| {
@ -3101,13 +3103,12 @@ impl acp_thread::AgentSessionClientUserMessageIds for NativeAgentConnection {
};
let path_style = project_state.project.read(cx).path_style(cx);
self.run_turn(session_id, cx, move |thread, cx| {
let content: Vec<UserMessageContent> = params
let content = params
.prompt
.into_iter()
.map(|block| UserMessageContent::from_content_block(block, path_style))
.collect::<Vec<_>>();
.collect::<Result<Vec<_>>>()?;
log::debug!("Converted prompt to message: {} chars", content.len());
log::debug!("Client user message id: {:?}", client_user_message_id);
log::debug!("Message content: {:?}", content);
@ -4272,6 +4273,116 @@ mod internal_tests {
.collect()
}
#[gpui::test]
async fn test_native_prompt_rejects_unknown_content_before_dispatch(cx: &mut TestAppContext) {
let fake = init_test(cx);
let (connection, agent, project, acp_thread) = setup_native_agent_session(cx).await;
let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
let thread = cx.update(|cx| native_thread_for_session(&agent, &session_id, cx));
let unknown = acp_v2::ContentBlock::Other(acp_v2::OtherContentBlock::new(
"_future",
std::collections::BTreeMap::from([("payload".into(), json!("private-payload"))]),
));
for text in ["ordinary prompt", "/compact"] {
let content = vec![text.into(), unknown.clone()];
assert!(connection.validate_prompt_content(&content).is_err());
let error = cx
.update(|cx| {
acp_thread::AgentSessionClientUserMessageIds::prompt(
connection.as_ref(),
ClientUserMessageId::new(),
acp_v2::PromptRequest::new(session_id.clone(), content),
cx,
)
})
.await
.expect_err("unsupported content must not reach command dispatch or the model");
assert!(error.to_string().contains("Native agent does not support"));
assert!(!error.to_string().contains("private-payload"));
}
thread.update(cx, |thread, cx| {
let path_style = project.read(cx).path_style(cx);
assert!(
thread
.push_acp_user_block(
ClientUserMessageId::new(),
["valid prefix".into(), unknown.clone()],
path_style,
cx,
)
.is_err()
);
assert!(thread.push_acp_agent_block(unknown, cx).is_err());
assert!(thread.is_empty());
});
assert!(acp_thread.read_with(cx, |thread, _| thread.entries().is_empty()));
assert!(fake.pending_completions().is_empty());
}
#[gpui::test]
async fn test_native_skill_prompt_preserves_v2_attachment_source(cx: &mut TestAppContext) {
let fake = init_test(cx);
let (_connection, agent, project, acp_thread) = setup_native_agent_session(cx).await;
cx.run_until_parked();
let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
let thread = cx.update(|cx| native_thread_for_session(&agent, &session_id, cx));
let model = fake.model("fake");
thread.update(cx, |thread, cx| thread.set_model(model.clone(), cx));
let mut skill = make_global_skill("test-skill", "Test skill");
skill.embedded_body = Some("skill instructions");
agent.update(cx, |agent, _| {
agent
.projects
.get_mut(&project.entity_id())
.expect("project state")
.skills = Arc::new(vec![skill]);
});
let prompt = vec![
acp_v2::ContentBlock::Text(
acp_v2::TextContent::new("/:test-skill keep\nformatting").meta(
acp_v2::Meta::from_iter([("source".into(), json!({"raw": true}))]),
),
),
acp_v2::ContentBlock::ResourceLink(
acp_v2::ResourceLink::new("attachment", "https://example.com/raw%20path")
.icons(vec![acp_v2::Icon::new("https://example.com/icon.svg")]),
),
acp_v2::ContentBlock::Resource(acp_v2::EmbeddedResource::new(
acp_v2::EmbeddedResourceResource::TextResourceContents(
acp_v2::TextResourceContents::new("attached text", "custom://raw/source").meta(
acp_v2::Meta::from_iter([("nested".into(), json!([null, true]))]),
),
),
)),
];
let send = acp_thread.update(cx, |thread, cx| thread.send(prompt.clone(), cx));
let send = cx.foreground_executor().spawn(send);
cx.run_until_parked();
let request = fake
.pending_completions_for(&model)
.pop()
.expect("skill reaches model");
thread.read_with(cx, |thread, _| {
let Some(Message::User(message)) = thread.last_message() else {
panic!("skill should inject one model-context user message");
};
assert!(matches!(message.content.first(), Some(UserMessageContent::Text(text)) if text.contains("skill instructions")));
assert_eq!(message.content.get(1), Some(&UserMessageContent::Text(" keep\nformatting".into())));
assert!(matches!(message.content.get(3), Some(UserMessageContent::Text(text)) if text.contains("attached text")));
});
acp_thread.read_with(cx, |thread, _| {
let Some(acp_thread::AgentThreadEntry::UserMessage(message)) = thread.entries().first()
else {
panic!("skill prompt should retain its source");
};
assert_eq!(message.content.source_blocks(), prompt.as_slice());
});
fake.send_text(&model, &request, "done");
fake.end_stream(&model, &request);
send.await.expect("skill prompt should complete");
}
#[gpui::test]
async fn test_compact_command_is_available(cx: &mut TestAppContext) {
init_test(cx);
@ -4323,13 +4434,17 @@ mod internal_tests {
let path_style = project.read(cx).path_style(cx);
thread.update(cx, |thread, cx| {
thread.set_model(model.clone(), cx);
thread.push_acp_user_block(
old_message_id,
[acp_v1::ContentBlock::from("old user")],
path_style,
cx,
);
thread.push_acp_agent_block("old assistant".into(), cx);
thread
.push_acp_user_block(
old_message_id,
[acp_v2::ContentBlock::from("old user")],
path_style,
cx,
)
.expect("user content should be supported");
thread
.push_acp_agent_block("old assistant".into(), cx)
.expect("agent content should be supported");
});
});
@ -4714,13 +4829,17 @@ mod internal_tests {
let path_style = project.read(cx).path_style(cx);
thread.update(cx, |thread, cx| {
thread.set_model(model.clone(), cx);
thread.push_acp_user_block(
ClientUserMessageId::new(),
[acp_v1::ContentBlock::from("old user")],
path_style,
cx,
);
thread.push_acp_agent_block("old assistant".into(), cx);
thread
.push_acp_user_block(
ClientUserMessageId::new(),
[acp_v2::ContentBlock::from("old user")],
path_style,
cx,
)
.expect("user content should be supported");
thread
.push_acp_agent_block("old assistant".into(), cx)
.expect("agent content should be supported");
});
});
@ -4962,12 +5081,14 @@ mod internal_tests {
cx.update(|cx| {
let path_style = project.read(cx).path_style(cx);
thread.update(cx, |thread, cx| {
thread.push_acp_user_block(
ClientUserMessageId::new(),
[acp_v1::ContentBlock::from("hello from the user")],
path_style,
cx,
);
thread
.push_acp_user_block(
ClientUserMessageId::new(),
[acp_v2::ContentBlock::from("hello from the user")],
path_style,
cx,
)
.expect("user content should be supported");
});
acp_thread.update(cx, |acp_thread, cx| {
acp_thread.set_draft_prompt(
@ -5042,23 +5163,45 @@ mod internal_tests {
#[test]
fn test_qualified_compact_commands_are_not_native_compact() {
let unqualified_blocks = [acp_v1::ContentBlock::from("/compact")];
let unqualified_blocks = [acp_v2::ContentBlock::from("/compact")];
let unqualified = Command::parse(&unqualified_blocks).unwrap();
assert!(unqualified.is_unqualified("compact"));
let mcp_blocks = [acp_v1::ContentBlock::from("/server.compact")];
let mcp_blocks = [acp_v2::ContentBlock::from("/server.compact")];
let mcp_qualified = Command::parse(&mcp_blocks).unwrap();
assert_eq!(mcp_qualified.prompt_name, "compact");
assert_eq!(mcp_qualified.explicit_server_id, Some("server"));
assert!(!mcp_qualified.is_unqualified("compact"));
let skill_blocks = [acp_v1::ContentBlock::from("/:compact")];
let skill_blocks = [acp_v2::ContentBlock::from("/:compact")];
let skill_qualified = Command::parse(&skill_blocks).unwrap();
assert_eq!(skill_qualified.prompt_name, "compact");
assert_eq!(skill_qualified.skill_scope, Some(""));
assert!(!skill_qualified.is_unqualified("compact"));
}
#[test]
fn test_mcp_prompt_content_preserves_annotations() {
let content = json!({
"type": "resource",
"resource": {"uri": "file:///raw%20path.md", "mimeType": "text/markdown"},
"annotations": {"audience": ["user", "assistant"], "priority": 0.75}
});
let block = mcp_message_content_to_acp_content_block(
serde_json::from_value(content).expect("MCP resource should deserialize"),
);
assert_eq!(
serde_json::to_value(block).expect("v2 resource link should serialize"),
json!({
"type": "resource_link",
"name": "file:///raw%20path.md",
"uri": "file:///raw%20path.md",
"mimeType": "text/markdown",
"annotations": {"audience": ["user", "assistant"], "priority": 0.75}
}),
);
}
fn make_project_skill(name: &str, description: &str, worktree: &str) -> Skill {
Skill {
name: name.to_string(),
@ -7348,27 +7491,36 @@ mod internal_tests {
cx.run_until_parked();
assert_eq!(thread_entries(&thread_store, cx), vec![]);
let send = acp_thread.update(cx, |thread, cx| {
thread.send(
vec![
"What does ".into(),
acp_v2::ContentBlock::ResourceLink(acp_v2::ResourceLink::new(
"b.md",
MentionUri::File {
abs_path: path!("/a/b.md").into(),
}
.to_uri()
.to_string(),
)),
" mean?".into(),
],
cx,
)
});
let prompt = vec![
acp_v2::ContentBlock::Text(acp_v2::TextContent::new("What does ").meta(
acp_v2::Meta::from_iter([("source".into(), json!({"nested": [null, "retained"]}))]),
)),
acp_v2::ContentBlock::ResourceLink(
acp_v2::ResourceLink::new(
"b.md",
MentionUri::File {
abs_path: path!("/a/b.md").into(),
}
.to_uri()
.to_string(),
)
.icons(vec![acp_v2::Icon::new("https://example.com/file.svg")])
.annotations(
acp_v2::Annotations::new()
.audience(vec![acp_v2::Role::Other("_future-audience".into())]),
),
),
" mean?".into(),
];
let send = acp_thread.update(cx, |thread, cx| thread.send(prompt.clone(), cx));
let send = cx.foreground_executor().spawn(send);
cx.run_until_parked();
let request = fake.pending_completions_for(&model).pop().unwrap();
let request_text = request_texts_after_system(&request.messages).join("\n");
assert!(request_text.contains("What does "));
assert!(request_text.contains("b.md"));
assert!(request_text.contains(" mean?"));
fake.send_text(&model, &request, "Lorem.");
fake.send_event(
&model,
@ -7390,6 +7542,13 @@ mod internal_tests {
fake.end_stream(&summary_model, &summary_request);
send.await.unwrap();
acp_thread.read_with(cx, |thread, _| {
let Some(acp_thread::AgentThreadEntry::UserMessage(message)) = thread.entries().first()
else {
panic!("native prompt should retain its source");
};
assert_eq!(message.content.source_blocks(), prompt.as_slice());
});
let uri = MentionUri::File {
abs_path: path!("/a/b.md").into(),
}
@ -8381,32 +8540,58 @@ mod internal_tests {
fn mcp_message_content_to_acp_content_block(
content: context_server::types::MessageContent,
) -> acp_v1::ContentBlock {
) -> acp_v2::ContentBlock {
match content {
context_server::types::MessageContent::Text {
text,
annotations: _,
} => text.into(),
context_server::types::MessageContent::Text { text, annotations } => {
acp_v2::ContentBlock::Text(
acp_v2::TextContent::new(text).annotations(mcp_annotations_to_acp(annotations)),
)
}
context_server::types::MessageContent::Image {
data,
mime_type,
annotations: _,
} => acp_v1::ContentBlock::Image(acp_v1::ImageContent::new(data, mime_type)),
annotations,
} => acp_v2::ContentBlock::Image(
acp_v2::ImageContent::new(data, mime_type)
.annotations(mcp_annotations_to_acp(annotations)),
),
context_server::types::MessageContent::Audio {
data,
mime_type,
annotations: _,
} => acp_v1::ContentBlock::Audio(acp_v1::AudioContent::new(data, mime_type)),
annotations,
} => acp_v2::ContentBlock::Audio(
acp_v2::AudioContent::new(data, mime_type)
.annotations(mcp_annotations_to_acp(annotations)),
),
context_server::types::MessageContent::Resource {
resource,
annotations: _,
annotations,
} => {
let mut link =
acp_v1::ResourceLink::new(resource.uri.to_string(), resource.uri.to_string());
acp_v2::ResourceLink::new(resource.uri.to_string(), resource.uri.to_string())
.annotations(mcp_annotations_to_acp(annotations));
if let Some(mime_type) = resource.mime_type {
link = link.mime_type(mime_type);
link = link.mime_type(acp_v2::MediaType::new(mime_type));
}
acp_v1::ContentBlock::ResourceLink(link)
acp_v2::ContentBlock::ResourceLink(link)
}
}
}
fn mcp_annotations_to_acp(
annotations: Option<context_server::types::MessageAnnotations>,
) -> Option<acp_v2::Annotations> {
annotations.map(|annotations| {
acp_v2::Annotations::new()
.audience(annotations.audience.map(|audience| {
audience
.into_iter()
.map(|role| match role {
context_server::types::Role::User => acp_v2::Role::User,
context_server::types::Role::Assistant => acp_v2::Role::Assistant,
})
.collect::<Vec<_>>()
}))
.priority(annotations.priority)
})
}

View file

@ -2726,31 +2726,36 @@ impl Thread {
pub fn push_acp_user_block(
&mut self,
id: ClientUserMessageId,
blocks: impl IntoIterator<Item = acp::ContentBlock>,
blocks: impl IntoIterator<Item = acp_v2::ContentBlock>,
path_style: PathStyle,
cx: &mut Context<Self>,
) {
) -> Result<()> {
let content = blocks
.into_iter()
.map(|block| UserMessageContent::from_content_block(block, path_style))
.collect::<Arc<_>>();
.collect::<Result<Arc<_>>>()?;
self.messages
.push(Arc::new(Message::User(UserMessage { id, content })));
cx.notify();
Ok(())
}
pub fn push_acp_agent_block(&mut self, block: acp::ContentBlock, cx: &mut Context<Self>) {
pub fn push_acp_agent_block(
&mut self,
block: acp_v2::ContentBlock,
cx: &mut Context<Self>,
) -> Result<()> {
let text = match block {
acp::ContentBlock::Text(text_content) => text_content.text,
acp::ContentBlock::Image(_) => "[image]".to_string(),
acp::ContentBlock::Audio(_) => "[audio]".to_string(),
acp::ContentBlock::ResourceLink(resource_link) => resource_link.uri,
acp::ContentBlock::Resource(resource) => match resource.resource {
acp::EmbeddedResourceResource::TextResourceContents(resource) => resource.uri,
acp::EmbeddedResourceResource::BlobResourceContents(resource) => resource.uri,
_ => "[resource]".to_string(),
acp_v2::ContentBlock::Text(text_content) => text_content.text,
acp_v2::ContentBlock::Image(_) => "[image]".to_string(),
acp_v2::ContentBlock::Audio(_) => "[audio]".to_string(),
acp_v2::ContentBlock::ResourceLink(resource_link) => resource_link.uri,
acp_v2::ContentBlock::Resource(resource) => match resource.resource {
acp_v2::EmbeddedResourceResource::TextResourceContents(resource) => resource.uri,
acp_v2::EmbeddedResourceResource::BlobResourceContents(resource) => resource.uri,
_ => anyhow::bail!("Native agent does not support this embedded resource variant"),
},
_ => "[unknown]".to_string(),
_ => anyhow::bail!("Native agent does not support this content block variant"),
};
self.messages.push(Arc::new(Message::Agent(AgentMessage {
@ -2758,6 +2763,7 @@ impl Thread {
..Default::default()
})));
cx.notify();
Ok(())
}
fn run_turn(
@ -6912,15 +6918,32 @@ impl From<String> for UserMessageContent {
}
impl UserMessageContent {
pub fn from_content_block(value: acp::ContentBlock, path_style: PathStyle) -> Self {
pub fn validate_content_block(value: &acp_v2::ContentBlock) -> Result<()> {
match value {
acp::ContentBlock::Text(text_content) => Self::Text(text_content.text),
acp::ContentBlock::Image(image_content) => Self::Image(convert_image(image_content)),
acp::ContentBlock::Audio(_) => {
acp_v2::ContentBlock::Text(_)
| acp_v2::ContentBlock::Image(_)
| acp_v2::ContentBlock::Audio(_)
| acp_v2::ContentBlock::ResourceLink(_) => Ok(()),
acp_v2::ContentBlock::Resource(resource) => match &resource.resource {
acp_v2::EmbeddedResourceResource::TextResourceContents(_)
| acp_v2::EmbeddedResourceResource::BlobResourceContents(_) => Ok(()),
_ => anyhow::bail!("Native agent does not support this embedded resource variant"),
},
_ => anyhow::bail!("Native agent does not support this content block variant"),
}
}
pub fn from_content_block(value: acp_v2::ContentBlock, path_style: PathStyle) -> Result<Self> {
Ok(match value {
acp_v2::ContentBlock::Text(text_content) => Self::Text(text_content.text),
acp_v2::ContentBlock::Image(image_content) => Self::Image(LanguageModelImage {
source: image_content.data.into(),
}),
acp_v2::ContentBlock::Audio(_) => {
// TODO
Self::Text("[audio]".to_string())
}
acp::ContentBlock::ResourceLink(resource_link) => {
acp_v2::ContentBlock::ResourceLink(resource_link) => {
match MentionUri::parse(&resource_link.uri, path_style) {
Ok(uri) => Self::Mention {
uri,
@ -6932,8 +6955,8 @@ impl UserMessageContent {
}
}
}
acp::ContentBlock::Resource(resource) => match resource.resource {
acp::EmbeddedResourceResource::TextResourceContents(resource) => {
acp_v2::ContentBlock::Resource(resource) => match resource.resource {
acp_v2::EmbeddedResourceResource::TextResourceContents(resource) => {
match MentionUri::parse(&resource.uri, path_style) {
Ok(uri) => Self::Mention {
uri,
@ -6951,45 +6974,35 @@ impl UserMessageContent {
}
}
}
acp::EmbeddedResourceResource::BlobResourceContents(_) => {
acp_v2::EmbeddedResourceResource::BlobResourceContents(_) => {
// TODO
Self::Text("[blob]".to_string())
}
other => {
log::warn!("Unexpected content type: {:?}", other);
Self::Text("[unknown]".to_string())
}
_ => anyhow::bail!("Native agent does not support this embedded resource variant"),
},
other => {
log::warn!("Unexpected content type: {:?}", other);
Self::Text("[unknown]".to_string())
}
}
_ => anyhow::bail!("Native agent does not support this content block variant"),
})
}
}
impl From<UserMessageContent> for acp::ContentBlock {
impl From<UserMessageContent> for acp_v2::ContentBlock {
fn from(content: UserMessageContent) -> Self {
match content {
UserMessageContent::Text(text) => text.into(),
UserMessageContent::Image(image) => {
acp::ContentBlock::Image(acp::ImageContent::new(image.source, "image/png"))
acp_v2::ContentBlock::Image(acp_v2::ImageContent::new(image.source, "image/png"))
}
UserMessageContent::Mention { uri, content } => {
acp_v2::ContentBlock::Resource(acp_v2::EmbeddedResource::new(
acp_v2::EmbeddedResourceResource::TextResourceContents(
acp_v2::TextResourceContents::new(content, uri.to_uri().to_string()),
),
))
}
UserMessageContent::Mention { uri, content } => acp::ContentBlock::Resource(
acp::EmbeddedResource::new(acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(content, uri.to_uri().to_string()),
)),
),
}
}
}
fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage {
LanguageModelImage {
source: image_content.data.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;