news 2026/11/1 1:50:09

深入 Macro Agent Harness 的 AgentPromptAction:提示词、附件与资源链接的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入 Macro Agent Harness 的 AgentPromptAction:提示词、附件与资源链接的完整实现
  • 后端
  • 前端
  • 协同办公
  • 即时通讯
  • 企业应用
  • 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
点击查看免费下载

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的两项特殊语义,二者都在源码中有明确注释与测试:

  1. 它会独占一整轮对话。在 action.rs 中,occupies_turn()只对Prompt和Compact返回true,其余动作(切模型、停止、回答 elicitation、回答权限)都返回false。原因是 ACP 的session/prompt同一时刻只能运行一个,所以提示词必须排队等待当前轮次结束,而控制类动作可以并行发送。
  2. 它是唯一会携带文件引用的动作。其它动作都没有附件字段,附件只属于提示词语义本身。

二、类型定义与字段详解

生成文档给出的类型是:

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: Option<crate::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: Vec<PromptAttachment>, /// Raw user text used for a visible session name when `prompt` is enriched. #[serde(skip)] name_source: Option<String>, }

字段说明如下:

字段类型必填说明
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的元素类型是PromptAttachment:

type 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 Into<String>, name: impl Into<String>) -> Self { /* mime/size 为 None */ } pub fn mime_type(self, mime_type: impl Into<String>) -> 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) -> Option<Self> { /* 仅识别 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与输入一致,并把附件从帧中回读后与原始Vec<PromptAttachment>做相等比较。

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是一个AgentActionId(v7 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 turn's 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?: Record<string, unknown> | null; };
字段说明
actionId?调用方可以预先自己铸造一个 ID,让前端在请求返回前就按最终 ID 打开会话、写历史行或建立引用。省略时由服务端铸造。
context?说明「为什么让 Agent 做这件事」。文档明确:只有转发触发式提示词的 runtime 才能发送;普通用户请求携带它会被拒绝。
type/ 其他被扁平化(flatten)到同一层,AgentPromptAction的prompt、attachments因此与actionId同级。

对应接口:POST /agent-sessions/{session_id}/control,session_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.rs:v7 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。转换器是StaticFileLinks(model.rs):

pub fn prompt_attachment(&self, attachment: &MessageEventAttachment) -> Option<PromptAttachment> { 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.attachments(session_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与空数组。

九、实践清单与边界条件

把上面的实现约束整理成可执行的检查表:

  1. uri必须是 Agent 能抓取的地址。附件只携带链接,不携带字节;如果链接需要凭证或位于内网,Agent 侧无法取到内容,提示词里的「看这张图」就会失效。
  2. 保持用户附加顺序。数组顺序就是resource_link块的顺序,服务端不会重排。多个附件之间的语义(例如「第一张是修改前、第二张是修改后」)依赖这个顺序。
  3. name与mimeType尽量准确。它们用于客户端渲染 chip,也用于 Agent 判断是否值得抓取;渠道自动转换场景下只能给出image/video与通配 MIME,这是数据源限制而非协议限制。
  4. 纯附件提示词是合法的。prompt传空字符串时服务端不会发送空文本块,只发资源链接。
  5. 不要在用户请求里塞context。该字段只允许转发触发式提示词的 runtime 使用,普通用户请求携带会被拒绝。
  6. 需要前端乐观更新时自行传actionId。省略则由服务端铸造;两种情况下返回的actionId都会成为折叠消息上的requestId。
  7. 不要期望排队中的提示词能改附件。编辑接口只替换文本,attachments会原样保留。
  8. /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
  • 后端
  • 前端
  • 协同办公
  • 即时通讯
  • 企业应用
  • 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
点击查看免费下载

相关推荐

上一篇:Agent中断了怎么办?揭秘Agents-Flex持久化运行时的Snapshot快照与版本CAS恢复机制
下一篇:如何后台挂机刷Bing搜索:rewards-farmer 的 Windows 虚拟桌面配置指南

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/1 1:49:53

零基础上手 VideoCaptioner:3 步生成视频字幕并完成中英双语翻译

零基础上手 VideoCaptioner&#xff1a;3 步生成视频字幕并完成中英双语翻译 【免费下载链接】VideoCaptioner &#x1f3ac; 卡卡字幕助手 | VideoCaptioner - 基于 LLM 的智能字幕助手 - 视频字幕生成、断句、校正、字幕翻译全流程处理&#xff01;- A powered tool for easy…

作者头像 李华
网站建设 2026/11/1 1:47:43

DXVK 2.7.1:Direct3D 转 Vulkan 提速实操

DXVK 2.7.1&#xff1a;Direct3D 转 Vulkan 提速实操 【免费下载链接】dxvk Vulkan-based implementation of D3D8, 9, 10 and 11 for Linux / Wine 项目地址: https://gitcode.com/gh_mirrors/dx/dxvk Windows 游戏搬到 Linux&#xff0c;画面撕裂、帧率腰斩、进游戏就…

作者头像 李华
网站建设 2026/11/1 1:46:51

腾讯二面:@Bean 与 @Component 用在同一个类上,会怎么样?

一、面试场景还原&#xff1a;这道题到底在问什么先还原一个典型的腾讯二面现场。面试官已经问完 Spring IOC、循环依赖、Bean 生命周期这些常规问题&#xff0c;你回答得都比较顺。这时候他突然抛出一句&#xff1a;“你说 Component 是类级注解&#xff0c;Bean 是方法级注解…

作者头像 李华