ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入解析 Macro AgentConfigOptionDto:ACP 会话设置的类型结构、能力发现与下发实践

深入解析 Macro AgentConfigOptionDto:ACP 会话设置的类型结构、能力发现与下发实践
  • 后端
  • 前端
  • 协同办公
  • 即时通讯
  • 企业应用
  • CRM
  • AI Agent
  • 人工智能

【免费下载链接】macro

Macro is a unified workspace for teams: email, chat, docs, tasks, agents, calls, and CRM — @-linked together with shared AI memory.

项目地址:https://gitcode.com/GitHub_Trending/macro3/macro
点击查看免费下载

在 Macro 的 Agent Harness 体系里,不同编码代理(Macro 内置 in-memory agent、用户的 Cursor 账户、配对的 macrod 运行时)都会向客户端“自报”一组可调会话设置,例如模型、思考等级等。AgentConfigOptionDto就是这套设置在对外 SDK 中的统一结构。本文以该类型文档为核心骨架,结合仓库中的 Rust 投影实现、HTTP 适配器、测试用例与 TypeScript 生成代码,逐字段拆解它的类型契约、来源与使用方式,读完后你可以独立完成“发现设置 → 渲染 UI → 下发修改”的完整闭环。

一、类型定位:它是协议里的哪一层

原文档对该类型只有一句定义性描述:

AgentConfigOptionDto=AgentConfigKindDto&{ category?: string | null; description?: string | null; id: string; name: string }

One agent-advertised ACP session setting.

这短短几行包含三个关键信息,值得先展开:

  1. 它是 TypeScript 的交叉类型(intersection type),不是单一接口。AgentConfigOptionDto= 通用外壳字段(id/name/description/category)交叉AgentConfigKindDto(形态相关的type、currentValue,以及 select 专属的options)。因此它同时具备“元数据”和“值状态”两类信息。
  2. 它是 ACP(Agent Client Protocol)语义的投影。仓库中的类型注释直接写明 “One agent-advertisedACPsession setting”,说明这些字段并非 Macro 自己发明的业务概念,而是把 ACP 的SessionConfigOption转换成了稳定的对外词汇。
  3. 它是“被代理声明的”而不是“被 Macro 定义的”。字段注释里反复出现 “supplied by the agent”“advertised by the agent”“Opaque”,意味着 Macro 只做搬运与规范化,不解释具体语义,也不对取值做枚举约束。

AgentConfigOptionDto在仓库中有两处权威来源,互为镜像:

角色文件说明
Rust 定义(真相源)crates/agent_harness/src/inbound/capability_discovery.rs带utoipa::ToSchema的 DTO,用于生成 OpenAPI
TypeScript 定义(生成物)packages/sdk/generated/agent-harness/types.gen.ts由@hey-api/openapi-ts自动生成,文件头即声明自动生成
文档页(本文主体)AgentConfigOptionDto.md由类型定义派生的参考文档

在 Rust 侧,DTO 与领域对象之间还有一层转换:领域层用AgentCapabilities { config_options: Vec<SessionConfigOption> }表达同一份数据(crates/agent_harness/src/domain/capability_discovery.rs),HTTP 层再通过From<AgentCapabilities> for DiscoverAgentCapabilitiesResponse把它映射成AgentConfigOptionDto(同文件)。这条“ACP → 领域 → DTO → SDK”的链路,是理解每个字段含义的最佳切入点。

二、逐字段拆解

2.1 通用外壳字段(交叉部分)

字段类型是否必填原文档描述技术含义
idstring必填Opaque id used to change this setting.修改该设置时回传的不透明标识,对应 ACP 的option.id,最终用于session/set_config_option的configId
namestring必填Display label supplied by the agent.代理给出的展示名,例如Model;UI 直接渲染,不做翻译映射
description?string \| null可选Optional explanatory copy.补充说明文案,可能为空
category?string \| null可选ACP semantic category, such asmodelorthought_level.ACP 语义分类,用于客户端做分组/图标/排序等启发式处理

id的“不透明”(opaque)属性非常关键:文档没有承诺它一定是可枚举常量,虽然后测用例中确实出现了"model",但契约层面应视为字符串。仓库测试就断言了首个选项的id为"model"(crates/agent_harness/src/domain/capability_discovery/test.rs),这属于测试夹具的取值,而非类型保证。

category同样是“语义提示”而不是“值域约束”。原文档只举了model与thought_level两个例子。从源码结构看,它的取值是从 ACP 枚举经serde_json序列化后取字符串得到的:

category: option .category .as_ref() .and_then(|category| serde_json::to_value(category).ok()) .and_then(|value| value.as_str().map(str::to_owned)),

这段代码位于 crates/folds/agent_fold/src/domain/session_config.rs。它意味着:能被序列化成字符串的分类会被保留,无法序列化的情形会退化为None(在 TS 中表现为undefined或null)。因此客户端必须容忍category缺失,不能把category === 'model'当作前提。

2.2 形态字段:AgentConfigKindDto 的两种联合成员

AgentConfigKindDto是一个以type为判别字段的联合类型(AgentConfigKindDto.md):

export type AgentConfigKindDto = { currentValue: string; // Current opaque value. options: Array<AgentConfigSelectOptionDto>; // Ordered values supplied by the agent. type: 'select'; } | { currentValue: boolean; // Current value. type: 'boolean'; };
成员字段类型含义
selecttype'select'单值选择器
selectcurrentValuestring当前选中的不透明值,与options[].value同一取值空间
selectoptionsAgentConfigSelectOptionDto[]由代理给出、保持顺序的候选项
booleantype'boolean'开关型设置
booleancurrentValueboolean当前开/关状态

注意currentValue在两种成员下类型不同:select是string,boolean是boolean。这正是使用type做判别收窄(narrowing)的原因,也是不能把它统一当作字符串处理的根本原因。

2.3 候选项:AgentConfigSelectOptionDto

select.options的元素类型见 AgentConfigSelectOptionDto.md:

字段类型必填含义
valuestring必填被选中时回传给代理的不透明值
namestring必填展示标签
description?string \| null可选该取值的提供方说明
group?string \| null可选提供方给出的分组标题

value与currentValue的关系是“候选项集合中的一员”。文档没有要求currentValue一定出现在options中,也没有要求value全局唯一,因此比较时建议直接用严格相等,而不是依赖索引位置。

group字段的来源值得一提:ACP 允许 select 的选项以“分组”形式下发,Macro 在投影时会把分组拍平为一维数组,同时把组名写入每一项的group:

SessionConfigSelectOptions::Grouped(groups) => groups .iter() .flat_map(|group| { group .options .iter() .map(|option| project_select_option(option, Some(group.name.as_str()))) }) .collect(),

见 crates/folds/agent_fold/src/domain/session_config.rs。因此客户端若要还原分组 UI,应按group字段做稳定分组(保留首次出现顺序),而不是假设数组本身是二维的。未分组场景下group为None。

三、从 ACP 到 DTO:投影过程的三个实现细节

理解投影实现,可以解释文档中那些“看起来没写”的边界行为。

3.1 只保留已识别的形态,未知形态被静默丢弃

session_config_options使用filter_map+project_option:

pub fn session_config_options(options: &[AcpConfigOption]) -> Vec<SessionConfigOption> { options.iter().filter_map(project_option).collect() }

其中project_option对AcpConfigKind做match,Select与Boolean之外的分支直接return None;Select内部若options既不是Ungrouped也不是Grouped,同样返回None(crates/folds/agent_fold/src/domain/session_config.rs)。

结论很明确:configOptions是“Macro 能理解的子集”,而不是“代理宣称的全部”。当你看到数组长度小于代理实际能力时,这不是丢包,而是投影层的有意收敛。这一点在原类型文档中无法直接看出,但对排障非常重要。

3.2 两种“无能力”场景返回空数组而非报错

领域服务提供AgentCapabilities::unsupported(),返回config_options: Vec::new()(crates/agent_harness/src/domain/capability_discovery.rs)。也就是说,某些 provider 天生不提供会话设置时,HTTP 仍然是 200,只是configOptions: []。客户端应把空数组视作正常状态(隐藏设置面板),而不是异常。

3.3 DTO 转换保留了字段顺序与层级

From<AgentCapabilities>的实现把领域结构逐层克隆到 DTO:id、name、description、category原样传递,kind按Select/Boolean分支映射,options逐项映射(crates/agent_harness/src/inbound/capability_discovery.rs)。序列化层面,结构体标了#[serde(rename_all = "camelCase")],枚举内部又用rename_all_fields = "camelCase"并显式rename = "currentValue",这解释了为什么 Rust 侧叫current_value,而对外的 JSON/TS 是currentValue。

四、获取这些选项:POST /agent-capabilities/discover

AgentConfigOptionDto的数组并不是随便就能拿到的,它是能力发现接口响应体DiscoverAgentCapabilitiesResponse.configOptions的元素类型(types.gen.ts)。

4.1 请求结构

请求体DiscoverAgentCapabilitiesRequest(types.gen.ts):

字段类型必填说明
harnessCapabilityHarnessDto必填要探测的 provider
harnessIdstring \| null视情况macrod必填,其它目标禁止传
modelstring \| null可选指定要检查哪个模型下的会话设置

harness的取值来自CapabilityHarnessDto,其枚举标注了#[serde(rename_all = "kebab-case")](crates/agent_harness/src/inbound/capability_discovery.rs),因此线上取值为字符串:

枚举成员线上取值含义
InMemoryin-memoryMacro 进程内 agent
Cursorcursor调用者自己的 Cursor 账户
Macrodmacrod已配对的 macrod 运行时

一个探测 Cursor 账户在特定模型下可调项的最小请求:

POST /agent-capabilities/discover Authorization: Bearer <token> Content-Type: application/json { "harness": "cursor", "model": "claude-sonnet-4-5" }

响应示例(字段形态与文档一致,取值为示例数据):

{ "configOptions": [ { "id": "model", "name": "Model", "description": "The model this session runs on.", "category": "model", "type": "select", "currentValue": "fast", "options": [ { "value": "fast", "name": "Fast" }, { "value": "deep", "name": "Deep", "description": "Slower, more thorough." } ] }, { "id": "auto_approve", "name": "Auto approve tools", "category": null, "description": null, "type": "boolean", "currentValue": false } ] }

注意description与category可以是null;type决定后面是否存在options。

4.2harnessId的强约束

领域服务的分发逻辑对(harness, harness_id)组合做了显式校验(crates/agent_harness/src/domain/capability_discovery.rs):

  • in-memory/cursor目标必须不传harnessId,否则BadRequest;
  • macrod目标必须传harnessId,缺失时报harnessId is required for macrod;
  • macrod目标在探测前会先经过HarnessCapabilityAccess::can_use授权检查,未通过直接Forbidden,不会触发一次无谓的运行时调用。

这点在测试中也有直接覆盖:macrod_authorizes_before_dispatch_and_projects_the_catalog验证了“先授权后分发”的顺序(crates/agent_harness/src/domain/capability_discovery/test.rs)。

4.3 错误码与 DTO 的关系

HTTP 适配器把领域错误映射为状态码(crates/agent_harness/src/inbound/capability_discovery.rs):

状态码触发条件客户端应如何处理
200探测成功(含空数组)正常渲染设置
400harnessId与该 harness 组合非法修正请求,不重试
401未认证重新登录
403配对 harness 对调用者不可见提示无权限
409macrod 运行时或 Cursor 账户未连接引导用户连接后重试
504macrod 探测超时可退避重试
502provider 探测失败记录原始错误文本

其中超时是领域服务的构造参数之一(AgentCapabilitiesServiceImpl::new(..., timeout),同文件),由组合根注入,类型定义本身不承载该语义。

五、下发修改:与 AgentConfigOptionDto 配套的 AgentSetConfigOptionAction

拿到id与候选项value之后,修改设置依靠AgentSetConfigOptionAction(AgentSetConfigOptionAction.md):

AgentSetConfigOptionAction={ configId: string; value: string }

Ask the agent to change one advertised select-style session setting.

字段类型说明
configIdstring由代理声明的不透明 ACP config id,即AgentConfigOptionDto.id
valuestring该选项声明的某个 select 值,即options[].value

这是一个两字段动作,正好与AgentConfigOptionDto的id和options[].value一一对应。原文档特意强调 “select-style”,说明该动作只适用于type: 'select'。boolean型设置没有配套的 DTO 动作类型,不应把布尔值塞进value字符串字段,否则会破坏契约。

在动作联合体中,该结构以type: 'setConfigOption'作为判别值(types.gen.ts):

export type AgentAction = (AgentPromptAction & { type: 'prompt' }) | (AgentSetModelAction & { type: 'setModel' }) | (AgentSetConfigOptionAction & { type: 'setConfigOption' }) | { type: 'compact' } | { type: 'stop' } | (AgentRespondElicitationAction & { type: 'respondElicitation' }) | (AgentPermissionAction & { type: 'respondToPermission' });

请求示例:

{ "type": "setConfigOption", "configId": "model", "value": "deep" }

5.1 服务端如何消费这个动作

Rust 领域层把动作转换为 ACP 的 JSON-RPC 请求。测试changing_the_model_persists_it_and_tells_the_running_agent明确断言了线上方法名为session/set_config_option,并且修改是持久化 + 通知运行中代理双写:

assert!( sent.iter().any(|message| matches!( message, ToRuntimeMessage::Acp(AcpMessage(RawJsonRpcMessage::Request(request))) if request.method.as_ref() == "session/set_config_option" )), "the running agent is told, got {sent:?}" );

见 crates/agent_harness/src/domain/service/test.rs。这条测试原本针对“切换模型”场景,而模型切换在协议层走的正是同一个session/set_config_option通道——这也解释了为什么AgentConfigOptionDto的示例id常常是model。

5.2 队列中的不可编辑性

AgentSetConfigOptionAction进入动作队列后,属于不可编辑的一类。队列实现里对edit_prompt显式拒绝这些动作(crates/agent_harness/src/domain/queue.rs):

AgentAction::SetModel(_) | AgentAction::SetConfigOption(_) | AgentAction::Compact | AgentAction::Stop | AgentAction::RespondElicitation(_) | AgentAction::RespondToPermission(_) => Err(QueueError::NotEditable),

因此 UI 层不应把“设置变更”当作可自由改写的草稿提示词;若要撤销,应重新提交一次相反取值的动作,而不是编辑历史条目。

六、TypeScript 侧的完整用法

生成的 SDK 已经封装了发现接口,方法名为discoverAgentCapabilitiesHandler,使用 Bearer 认证并固定Content-Type: application/json(packages/sdk/generated/agent-harness/sdk.gen.ts)。

6.1 发现并渲染

import type { AgentConfigOptionDto } from './generated/agent-harness/types.gen'; const { data } = await sdk.discoverAgentCapabilitiesHandler({ body: { harness: 'macrod', harnessId: '<paired-harness-uuid>' }, }); const options: AgentConfigOptionDto[] = data.configOptions; // 按 type 收窄后分别渲染 for (const option of options) { if (option.type === 'select') { renderSelect(option.id, option.name, option.currentValue, option.options); } else { // option.type === 'boolean' renderToggle(option.id, option.name, option.currentValue); } }

option.type === 'select'分支内,currentValue会被收窄为string,并且options字段才存在;else分支中currentValue为boolean。这就是为什么文档把AgentConfigKindDto设计成带判别字段的联合类型,而不是可选字段的大对象。

6.2 按group还原分组

function groupOptions(options: AgentConfigSelectOptionDto[]) { const groups = new Map<string, AgentConfigSelectOptionDto[]>(); for (const opt of options) { const key = opt.group ?? ''; // 未分组归入空串分组 (groups.get(key) ?? groups.set(key, []).get(key)!).push(opt); } return groups; }

由于投影层已经把 ACP 的分组拍平(见第 3 节),这里的分组是基于字段重建的,顺序依赖数组本身的有序性。

6.3 提交变更

await sdk.controlAgentSessionHandler({ body: { action: { type: 'setConfigOption', configId: option.id, value: chosen.value }, }, });

上面示例中的方法名与请求外层字段属于调用方视角的示意;真正与本文类型契约绑定的是两点:动作对象必须携带type: 'setConfigOption',并且只包含configId与value两个字段。

七、容易被忽略的六个工程细节

  1. 不要枚举id。id被定义为 “Opaque id”,具体取值由代理决定;测试夹具里的"model"只是夹具。
  2. 不要假设currentValue一定在options里。文档未作此保证;UI 应先查找匹配项,找不到时降级展示原始值。
  3. configOptions可能为空数组。AgentCapabilities::unsupported()就是这种形态,属于正常语义。
  4. 未知形态会被丢弃。投影层只保留select/boolean,见 session_config.rs。
  5. boolean设置没有对应动作类型。AgentSetConfigOptionAction的value是string,文档也只承诺 select-style。
  6. macrod探测先鉴权再分发,因此403与409的语义不同:前者是权限问题,重试无益;后者是连接问题,可引导重连。

八、相关文档与源码索引

  • 主类型文档:AgentConfigOptionDto
  • 形态联合:AgentConfigKindDto
  • 候选项结构:AgentConfigSelectOptionDto
  • 配套动作:AgentSetConfigOptionAction
  • DTO 定义:crates/agent_harness/src/inbound/capability_discovery.rs
  • 领域服务与分发校验:crates/agent_harness/src/domain/capability_discovery.rs
  • ACP 投影实现:crates/folds/agent_fold/src/domain/session_config.rs
  • 领域测试:crates/agent_harness/src/domain/capability_discovery/test.rs
  • 动作下发测试:crates/agent_harness/src/domain/service/test.rs
  • 生成 SDK:packages/sdk/generated/agent-harness/sdk.gen.ts、types.gen.ts
  • 后端
  • 前端
  • 协同办公
  • 即时通讯
  • 企业应用
  • CRM
  • AI Agent
  • 人工智能

【免费下载链接】macro

Macro is a unified workspace for teams: email, chat, docs, tasks, agents, calls, and CRM — @-linked together with shared AI memory.

项目地址:https://gitcode.com/GitHub_Trending/macro3/macro
点击查看免费下载

相关推荐

上一篇:终极指南:bsdiff/bspatch二进制补丁工具完全解析
下一篇:Maple Mono 等宽字体:3 步装好,开启圆角连字与终端图标

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进