深入 Macro Agent Harness 的 AgentPromptAction:提示词、附件与资源链接的完整实现 后端前端协同办公即时通讯企业应用CRMAI Agent人工智能【免费下载链接】macroMacro 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点击查看免费下载AgentPromptAction是 Macro Agent Harness 控制接口中最核心的请求类型之一它把一个自然语言提示词和一组可选附件打包交给已在运行的 Agent 会话执行。本文以 SDK 生成的AgentPromptAction类型文档为基础结合 Rust 运行时协议实现、验证测试与 TypeScript SDK 源码讲清它的字段语义、线上传输形态、排队行为与端到端调用方式。一、AgentPromptAction 在动作模型中的位置AgentPromptAction并不是一个孤立的类型而是判别联合AgentAction的一个成员。AgentAction的type字段决定具体分支而AgentPromptAction对应的分支值是prompt。完整的联合定义见 AgentAction 文档type值携带的类型语义promptAgentPromptAction让 Agent 处理一段提示词可带附件setModelAgentSetModelAction切换会话后续使用的模型setConfigOptionAgentSetConfigOptionAction修改 Agent 声明的某个 select 型会话配置compact无字段压缩当前上下文stop无字段中断 Agent 正在做的事respondElicitationAgentRespondElicitationAction回答 Agent 发出的 elicitation 请求respondToPermissionAgentPermissionAction回答 Agent 发出的权限请求这个位置决定了AgentPromptAction的两项特殊语义二者都在源码中有明确注释与测试它会独占一整轮对话。在 action.rs 中occupies_turn()只对Prompt和Compact返回true其余动作切模型、停止、回答 elicitation、回答权限都返回false。原因是 ACP 的session/prompt同一时刻只能运行一个所以提示词必须排队等待当前轮次结束而控制类动作可以并行发送。它是唯一会携带文件引用的动作。其它动作都没有附件字段附件只属于提示词语义本身。二、类型定义与字段详解生成文档给出的类型是type AgentPromptAction { attachments?: PromptAttachment[]; prompt: string; };对应的 Rust 定义在 action.rs序列化统一使用 camelCase#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all camelCase)] pub struct AgentPromptAction { /// What to tell the agent. pub prompt: String, /// Trusted system events, authored only by the harness. #[cfg_attr(feature utoipa, schema(ignore))] #[serde(default, skip_serializing_if Option::is_none)] pub notifications: Optioncrate::domain::notification::NotificationInput, /// Files the prompt refers to, in the order the user attached them. #[serde(default, skip_serializing_if Vec::is_empty)] pub attachments: VecPromptAttachment, /// Raw user text used for a visible session name when prompt is enriched. #[serde(skip)] name_source: OptionString, }字段说明如下字段类型必填说明promptstring是要告诉 Agent 的内容。在服务端还可能被增强例如注入隐藏的执行上下文但原始用户文本始终是该块的第一段。attachmentsPromptAttachment[]否提示词引用的文件顺序即用户附加顺序在文本之后逐个以resource_link内容块送达。notifications内部结构否「受信任的系统事件」只允许 harness 自己写入。它在生成的 SDK schema 中被schema(ignore)排除不会出现在前端类型里。name_sourcestring否私有字段#[serde(skip)]不参与序列化用于在prompt被增强后仍能取到原始文本来推导可见的会话名称。由此可以推出一个对调用方很重要的规则普通的prompt请求只应关心prompt与attachments两个字段。notifications属于服务端内部通道外部调用方不应也无法借此注入系统事件。AgentPromptAction还提供了两个便捷构造函数AgentAction::prompt(fix the flaky test); // 无附件 AgentAction::prompt_with_attachments(看图, vec![screenshot]); // 带附件前者实际是后者传入空Vec的包装因此两条路径产生的线上结构完全一致。三、PromptAttachment只传 URI不传字节attachments的元素类型是PromptAttachmenttype PromptAttachment { mimeType?: string | null; name: string; size?: number | null; uri: string; };字段类型必填含义uristring是Agent 可以抓取该文件的地址。实践中是静态文件服务的 URL。namestring是展示名称通常是原始文件名。mimeTypestring \| null否媒体类型已知时填写。sizenumber \| null否字节数已知时填写Rust 侧为i64。这个设计的核心约束写在 action.rs 的文档注释里字节永远不随提示词传输只有 URI 加上足够渲染一个附件 chip、并让 Agent 自行决定是否抓取的元数据。它刻意对齐 ACP 的resource_link内容块原因是每个 Agent 都必须接受该内容块harness 因此无需「解析」文件本身只需做一次字段翻译。Rust 侧的实现提供了双向转换impl PromptAttachment { pub fn new(uri: impl IntoString, name: impl IntoString) - Self { /* mime/size 为 None */ } pub fn mime_type(self, mime_type: impl IntoString) - Self { /* builder */ } pub fn size(self, size: i64) - Self { /* builder */ } pub fn to_content_block(self) - ContentBlock { /* - ResourceLink */ } pub fn from_content_block(block: ContentBlock) - OptionSelf { /* 仅识别 ResourceLink */ } }to_content_block把四个字段映射到 ACP 的ResourceLink::new(name, uri).mime_type(...).size(...)from_content_block则从已记录的帧里把同样的字段读回来「fold」对话记录折叠因此不需要第二个数据源就能渲染附件。这也解释了为什么字段形状必须与 ACP 原生一致写出去和读回来必须是同一组字段。四、线上形态文本在前资源链接在后AgentPromptAction最终被翻译为一条 ACP 的session/promptJSON-RPC 请求。翻译逻辑在to_runtime()中关键规则有三条。4.1 顺序文本块在前附件逐个在后// Text first, then one link per attachment: agents read the // prompt in order, and the text is what the links are about. let text (!action.prompt.is_empty()).then(|| ContentBlock::from(action.prompt.clone())); let blocks text .into_iter() .chain(action.attachments.iter().map(PromptAttachment::to_content_block)) .collect();对应的一次请求长这样示例结构字段名遵循 ACP 的 camelCase{ jsonrpc: 2.0, id: harness:prompt:1, method: session/prompt, params: { sessionId: acp-abc, prompt: [ { type: text, text: what is wrong in this screenshot? }, { type: resource_link, name: screenshot.png, uri: https://static.example/file/11111111-1111-4111-8111-111111111111, mimeType: image/png, size: 2048 } ] } }测试a_prompt_with_attachments_sends_text_then_one_resource_link_each断言了parsed.prompt.len() 3、第一个块是文本、第二个块是ResourceLink且uri/name/mime_type/size与输入一致并把附件从帧中回读后与原始VecPromptAttachment做相等比较。4.2 纯附件发送不产生空文本块如果用户只拖了文件没写文字服务端不会发射一个空text块let text (!action.prompt.is_empty()).then(|| ContentBlock::from(action.prompt.clone()));注释解释得很直接空文本块会被某些 runtime 当作「空提示词」而 fold 又会把它丢掉那样线上帧和对话记录就会不一致。测试a_file_only_prompt_carries_no_text_block_at_all覆盖了这个场景并断言此时prompt数组长度为 1 且唯一元素是ResourceLink。4.3 隐藏的执行上下文与轮次 ID当这条请求的 JSON-RPCid是一个AgentActionIdv7 UUID时服务端会在prompt数组末尾追加一个带_meta的隐藏文本块{ type: text, text: m-agent-context{\version\:1,\text\:\invocation turn_id\\\...\\\Use this turn_id for ExecuteCode calls originating in this prompt. Never substitute another turns ID for a stale call./invocation\}/m-agent-context, _meta: { macro/invocation-context: true } }这意味着附件块与用户文本的相对顺序不受影响隐藏上下文永远排在最后同时每个提示词位携带自己的turn_id测试each_prompt_carries_its_own_hidden_execution_turn_without_replacing_user_content验证了连续两轮 prompt 各自持有独立 ID且不会覆盖用户原文。4.4 可选的attachments与向后兼容attachments在 Serde 上带#[serde(default, skip_serializing_if Vec::is_empty)]因此老客户端发送{type:prompt,prompt:hi}依然合法无附件的提示词序列化后不会多出一个空数组字段。测试attachments_are_optional_on_the_wire同时断言了这两种行为并给出带附件时的完整 JSON 形状{ type: prompt, prompt: look, attachments: [{ uri: https://x/file/1, name: a.png, mimeType: image/png }] }4.5 一个容易踩的边界/compact与附件AgentAction::Compact在线上同样表现为一条session/prompt内容是纯文本/compact。反向识别control_from_runtime要求该请求的prompt数组恰好只有一个文本块只要同时存在resource_link它就被当作「带附件的真实提示词」而不是压缩指令。测试a_compact_command_with_attachments_is_still_a_plain_prompt专门锁定了这一点避免把用户附件在识别为控制指令时丢掉。五、通过 control 端点提交提示词AgentPromptAction不会直接裸发到 HTTP 接口而是作为ControlRequest的一部分提交。请求体的公开形状见 ControlRequest 文档路径见 ControlAgentSessionData 文档type ControlRequest AgentAction { actionId?: AgentActionId | null; context?: Recordstring, unknown | null; };字段说明actionId?调用方可以预先自己铸造一个 ID让前端在请求返回前就按最终 ID 打开会话、写历史行或建立引用。省略时由服务端铸造。context?说明「为什么让 Agent 做这件事」。文档明确只有转发触发式提示词的 runtime 才能发送普通用户请求携带它会被拒绝。type/ 其他被扁平化flatten到同一层AgentPromptAction的prompt、attachments因此与actionId同级。对应接口POST /agent-sessions/{session_id}/controlsession_id是目标 Agent 会话 ID。这个「扁平化」行为有单独的回归测试。在 axum_router/test.rs 中{ type: prompt, prompt: hello, actionId: 01a0acab-5eff-72d6-91ca-16997a26d13a }被解析为ControlRequest { action_id: Some(..), action: AgentAction::prompt(hello) }证明带附件/不带附件的提示词与调用方指定的 ID 可以共存而不会被枚举吞掉同时{type:stop}这类无 ID 请求序列化后不会出现actionId: null。返回的actionId与服务端在折叠消息上记录的requestId是同一个值——这正是AgentActionId的设计目的见 action.rsv7 UUID 按铸造时间可排序服务端是运行时帧的唯一写入方。六、排队语义什么时候提示词会被「挂起」因为prompt会占据一整轮control的返回状态可能是「已接受但排队」。排队之后队列快照由AgentSessionQueueResponse返回entries按派发顺序从旧到新排列每条记录是QueuedActionDto其中kind只可能是prompt或compact因为只有「占轮次」的动作才会进入队列QueuedActionDto.attachments只对提示词存在并且文档明确编辑队列中的提示词只替换文本附件会被保留。这条规则在 Rust 侧的类型转换中可以直接核对agent_session 领域模型let (prompt, attachments) match queued.action { AgentAction::Prompt(action) (Some(action.prompt.clone()), action.attachments.clone()), _ (None, Vec::new()), };因此如果你的产品允许「排队中修改提示词」界面必须把附件 chip 当作不可编辑的既有内容展示而不是随文本一起清空。七、从渠道消息到 PromptAttachment附件是怎么被自动生成的除了调用方手动构造Macro 内部还会把渠道消息里的文件自动转成PromptAttachment。转换器是StaticFileLinksmodel.rspub fn prompt_attachment(self, attachment: MessageEventAttachment) - OptionPromptAttachment { let (kind, mime_type) match attachment.entity_type.as_str() { Self::STATIC_IMAGE (image, image/*), Self::STATIC_VIDEO (video, video/*), _ return None, }; let uri format!({}/file/{}, self.base_url, attachment.entity_id); Some(PromptAttachment::new(uri, kind).mime_type(mime_type)) }这段实现解释了若干细节URI 形态为{base_url}/file/{entity_id}其中base_url来自部署配置由 composition root 注入构造时会去掉结尾多余斜杠所以本模块只做纯翻译、不读环境变量。name不是原文件名而是image/video。因为渠道行只记录「这是图片/视频」这一级信息不记录原始文件名。mimeType使用通配范围image/*/video/*同样是因为渠道侧只知道大类。文档类附件被排除document类型返回None。源码注释说明文档通过 mention 机制到达 Agent不作为可匿名抓取的文件链接。新会话由 mention 触发时附件被存进MentionOrigin.attachmentssession_origin.rs开题时经into_action()交给prompt_with_attachments任务指派TaskAssignment则只有纯文本提示词。集成侧测试a_mention_with_attached_files_opens_with_them_as_prompt_attachments与a_managed_channel_message_with_files_delivers_them_as_prompt_attachments覆盖了这两条路径并验证「文档不产生文件链接、附件数量为 2」。八、TypeScript SDK 中的实际用法SDK 把 control 端点封装成实体方法agent-session.ts// 通用入口任何 AgentAction await session.control({ type: prompt, prompt: hello }); // 语法糖只发提示词可选附件 await session.prompt(what is wrong in this screenshot?, [ { uri: https://static.example/file/11111111-1111-4111-8111-111111111111, name: screenshot.png, mimeType: image/png, size: 2048, }, ]);prompt()的实现有一个值得注意的细节return this.control({ type: prompt, prompt: text, ...(attachments attachments.length 0 ? { attachments } : {}), });它只在附件非空时才展开attachments键与服务端Vec空值不序列化的行为保持一致避免线上出现attachments: []这种多余字段。读取排队中的动作时QueuedAction实体会把可能缺失的数组归一成空数组/** Files the prompt refers to; empty for a compact. Kept through an edit. */ get attachments(): PromptAttachment[] { return this.dto.attachments ?? []; }见 queued-action.ts。这让调用方在渲染队列时无需区分undefined与空数组。九、实践清单与边界条件把上面的实现约束整理成可执行的检查表uri必须是 Agent 能抓取的地址。附件只携带链接不携带字节如果链接需要凭证或位于内网Agent 侧无法取到内容提示词里的「看这张图」就会失效。保持用户附加顺序。数组顺序就是resource_link块的顺序服务端不会重排。多个附件之间的语义例如「第一张是修改前、第二张是修改后」依赖这个顺序。name与mimeType尽量准确。它们用于客户端渲染 chip也用于 Agent 判断是否值得抓取渠道自动转换场景下只能给出image/video与通配 MIME这是数据源限制而非协议限制。纯附件提示词是合法的。prompt传空字符串时服务端不会发送空文本块只发资源链接。不要在用户请求里塞context。该字段只允许转发触发式提示词的 runtime 使用普通用户请求携带会被拒绝。需要前端乐观更新时自行传actionId。省略则由服务端铸造两种情况下返回的actionId都会成为折叠消息上的requestId。不要期望排队中的提示词能改附件。编辑接口只替换文本attachments会原样保留。/compact与附件互斥于控制识别。带资源链接的/compact文本会被当作普通提示词处理这是刻意的保护行为。十、延伸阅读类型定义AgentPromptAction、PromptAttachment、AgentAction请求包装ControlRequest、ControlAgentSessionData排队结构AgentSessionQueueResponse、QueuedActionDto运行时实现crates/agent_runtime_protocol/src/domain/action.rs行为验证crates/agent_runtime_protocol/src/domain/action/test.rs渠道附件翻译crates/agent_harness/src/domain/model.rs、crates/agent_harness/src/domain/model/session_origin.rs客户端封装packages/sdk/src/entities/agent-sessions/agent-session.ts、packages/sdk/src/entities/agent-sessions/queued-action.ts赞分享后端前端协同办公即时通讯企业应用CRMAI Agent人工智能【免费下载链接】macroMacro 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点击查看免费下载相关推荐markdownlint 列表解析边界场景详解段落中的数字为何触发 MD029/MD030/MD032/MD004markdownlint 列表解析边界场景详解段落中的数字为何触发 MD029/MD030/MD032/MD004 markdownlint 是 Node.j后端前端协同办公即时通讯企业应用CRMAI Agent人工智能Macro Agent Harness 源码解析Cursor 云代理的仓库选择系统提示词与决策链路Macro Agent Harness 源码解析Cursor 云代理的仓库选择系统提示词与决策链路 在 Macro 的统一工作空间中每次发起 coding后端前端协同办公即时通讯企业应用CRMAI Agent人工智能Macro Agent 任务指派提示词详解从 prompt.md 到会话指令的完整实现链路Macro Agent 任务指派提示词详解从 prompt.md 到会话指令的完整实现链路 Macro 允许用户把任务直接指派给 AI Agent。当指派发生后端前端协同办公即时通讯企业应用CRMAI Agent人工智能上一篇Agent中断了怎么办揭秘Agents-Flex持久化运行时的Snapshot快照与版本CAS恢复机制下一篇如何后台挂机刷Bing搜索rewards-farmer 的 Windows 虚拟桌面配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考