mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-13 17:15:16 +00:00
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
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:
parent
f54e09c151
commit
f16f9652ec
2 changed files with 321 additions and 123 deletions
|
|
@ -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, ¶ms.prompt)
|
||||
{
|
||||
return Task::ready(Err(error));
|
||||
}
|
||||
|
||||
if let Some(parsed_command) = Command::parse(¶ms.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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue