ASTER ACP / 协议原理
ACP 与 Core 的职责边界
按一次客户端请求拆开协议适配、可信宿主、Core 会话和界面事件。
客户端与执行者的职责
客户端接收用户输入并显示运行过程。
aster-acp 接收 ACP 消息,再调用 Core。
Core 接纳执行、调用模型、治理工具并检查完成。
协议适配器把运行事件送回客户端。
- 01从交互进入适配器
客户端构造协议参数。ACP 在入口检查内容类型。
- 02可信宿主选择能力
宿主提供可信配置。工作区配置保留原有信任限制。
- 03Core 处理事实与完成
ACP 的显示事件不会替代内核回执。
turn.rsL26 ↗
例如,用户在终端提交日期导出修复。 终端发送文本,ACP 找到对应会话。 Core 执行任务,ACP 翻译事件。 终端用事件中的原始 ID 更新工具行。
三层结束含义怎样区分
| 层次 | 结束表示什么 | 还要核对什么 |
|---|---|---|
| ACP prompt | 本轮协议请求返回 | Core 的运行与完成依据 |
| Core Run | 原始执行进入终态 | 资源结清与该轮证据 |
| Durable Goal | 原目标满足完成条件 | 全部步骤、等待与切片 |
end_turn 是协议停止原因。
它不能独自证明长期目标已经完成。
客户端需要结合对应的 Core 状态显示结果。
从连接到一轮执行
- 01先协商能力
客户端依据 initialize 响应选择已声明的能力。
- 02再建立或恢复会话
session/new 与 session/load 有不同来源校验。
resume.rsL37 ↗ - 03消费事件直到本轮结束
客户端同时处理消息、工具更新和确认请求。
turn.rsL26 ↗
客户端先调用 initialize。
随后创建或恢复会话,再提交 session/prompt。
运行期间,客户端消费 session/update。
需要确认时,客户端回答原始权限请求。
本轮结束后,客户端读取 prompt 响应。
入口会拒绝哪些内容
当前 ACP prompt 接受文本内容块。 图片、音频、资源和资源链接会返回参数错误。 拒绝发生在模型调用之前。 Core 内部具备某项能力,不代表 ACP 已暴露该能力。
标准 ACP 方法与宿主扩展也要分开。
aster/goal/* 和 aster/review/* 属于宿主扩展。
未知扩展方法返回 method_not_found。
入口:宿主方法闭集 L45 ↗。
哪些信息必须来自可信宿主
| 信息 | 作用 | 工作区文本的边界 |
|---|---|---|
| 模型与服务配置 | 选择实际模型调用路径 | 保留配置层的信任规则 |
| 权限与沙箱 | 判定工具调用 | 提示词不能放宽 |
goal_control | 限制自动续跑 | 工作区声明不能提高上限 |
| 工作策略选择 | 选择已编入的策略 | 客户端不能提交任意策略代码 |
| 完成证明选择 | 安装宿主检查 | 模型不能自行安装证明器 |
每项信息由其原入口准入。 ACP 不应把普通文本解释为额外执行权限。
源码漫游从哪里开始
源码漫游
ACP 请求与事件源码漫游
沿协议入口查看文本校验、事件映射、确认与无人值守结果。
pub(super) fn prompt_text(prompt: &[acp::ContentBlock]) -> acp::Result<String> {let unsupported = |kind: &str| {acp::Error::invalid_params().data(format!("unsupported prompt content block `{kind}`; this agent accepts text only and did not send the request to the model"))};let mut text = Vec::new();for block in prompt {match block {acp::ContentBlock::Text(content) => text.push(content.text.as_str()),acp::ContentBlock::Image(_) => return Err(unsupported("image")),acp::ContentBlock::Audio(_) => return Err(unsupported("audio")),acp::ContentBlock::Resource(_) => return Err(unsupported("resource")),acp::ContentBlock::ResourceLink(_) => return Err(unsupported("resource_link")),_ => return Err(unsupported("unknown")),}}Ok(text.join("\n"))}pub(super) async fn handle_event(&self,session_id: &acp::SessionId,session: &AgentSession,state: &mut TurnState,
pub(super) async fn handle_event(&self,session_id: &acp::SessionId,session: &AgentSession,state: &mut TurnState,event: AgentEvent,) {let observation = state.observer.as_mut().and_then(|observer| observer.event(&event));self.emit_run_observation(observation).await;match event {AgentEvent::TextDelta { text } => {if !text.is_empty() {state.saw_text = true;}state.delta_text.push_str(&text);self.emit_text(session_id, &text).await;}AgentEvent::ReasoningDelta { text } => {self.emit_thought(session_id, &text).await;}AgentEvent::ToolStart { .. } => {// Streaming and the fact runner both announce a call before its// arguments exist. Painting that announcement opens a second// Run row for one execution, and it also paints a twin that// the same-response collapse never runs.}
AgentEvent::ToolStart { .. } => {// Streaming and the fact runner both announce a call before its// arguments exist. Painting that announcement opens a second// Run row for one execution, and it also paints a twin that// the same-response collapse never runs.}AgentEvent::ToolExecutionStart {id,name,args: tool_args,} => match note_tool_row(&mut state.painted_tools, &id) {ToolRowPaint::Open => {self.emit_tool_call(session_id, &id, &name, Some(&tool_args)).await;}ToolRowPaint::Update => {self.emit_tool_call_started(session_id, &id, &name, &tool_args).await;}},AgentEvent::ToolOutputDelta { id, name, delta } => {self.emit_tool_call_output_delta(session_id, &id, &name, &delta).await;}AgentEvent::ToolEnd {id,name,
pub(super) fn tool_permission_ask(session_id: &acp::SessionId,tool_id: &str,tool_name: &str,args: &serde_json::Value,workspace: Option<&Path>,) -> acp::RequestPermissionRequest {let raw_input = prepared_raw_input(tool_name, args);let why = approval_reason(&assess(tool_name, args, workspace));let mut fields = acp::ToolCallUpdateFields::new().title(tool_title(tool_name)).kind(tool_kind_for(tool_name)).status(acp::ToolCallStatus::Pending).content(vec![acp::ToolCallContent::from(acp::ContentBlock::Text(acp::TextContent::new(why),))]);if let Some(raw_input) = raw_input {fields = fields.raw_input(raw_input);}let mut options = vec![acp::PermissionOption::new(acp::PermissionOptionId::new("allow-once"),"Yes, proceed",acp::PermissionOptionKind::AllowOnce,)];let learnable = (tool_name == "bash").then(|| args.get("command").and_then(serde_json::Value::as_str)).flatten().and_then(crate::approval_learning::learnable_rule);if let Some(rule) = learnable {options.push(acp::PermissionOption::new(acp::PermissionOptionId::new("allow-always"),format!("Yes, and don't ask again for `{rule}` in this workspace"),
if failure.is_none() && !progress.has_action_evidence() {progress.mark_evidence_missing();failure = Some(anyhow::anyhow!("A3S Code agent ended without a successful tool execution"));}let (outcome, reason) = match &failure {None => (ExecutionResultOutcomeV1::Succeeded,TerminalReason::AgentEnd,),Some(error) => classify_failure(&progress, error),};if let Some(path) = &unattended.result_file {if let Err(error) = persist_result(path, &progress.report(outcome, reason)).await {tracing::error!(path = %path.display(), "could not write the result record: {error:#}");failure.get_or_insert(error);}}match failure {None => Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)),// These outcomes are recorded in the result file. Returning a// JSON-RPC internal error makes Harbor's summary say only// "Internal error" and hides the reason.Some(_)if matches!(
源码与本文版本一致,保留原始行号。