- 后端
- 前端
- 协同办公
- 即时通讯
- 企业应用
- 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.
AgentPromptAction是 Macro Agent Harness 控制接口中最核心的请求类型之一:它把一个自然语言提示词和一组可选附件打包,交给已在运行的 Agent 会话执行。本文以 SDK 生成的AgentPromptAction类型文档为基础,结合 Rust 运行时协议实现、验证测试与 TypeScript SDK 源码,讲清它的字段语义、线上传输形态、排队行为与端到端调用方式。
一、AgentPromptAction 在动作模型中的位置
AgentPromptAction并不是一个孤立的类型,而是判别联合AgentAction的一个成员。AgentAction的type字段决定具体分支,而AgentPromptAction对应的分支值是"prompt"。完整的联合定义见 AgentAction 文档:
type值 | 携带的类型 | 语义 |
|---|---|---|
prompt | AgentPromptAction | 让 Agent 处理一段提示词(可带附件) |
setModel | AgentSetModelAction | 切换会话后续使用的模型 |
setConfigOption | AgentSetConfigOptionAction | 修改 Agent 声明的某个 select 型会话配置 |
compact | 无字段 | 压缩当前上下文 |
stop | 无字段 | 中断 Agent 正在做的事 |
respondElicitation | AgentRespondElicitationAction | 回答 Agent 发出的 elicitation 请求 |
respondToPermission | AgentPermissionAction | 回答 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: 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>, }字段说明如下:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
prompt | string | 是 | 要告诉 Agent 的内容。在服务端还可能被增强(例如注入隐藏的执行上下文),但原始用户文本始终是该块的第一段。 |
attachments | PromptAttachment[] | 否 | 提示词引用的文件,顺序即用户附加顺序;在文本之后逐个以resource_link内容块送达。 |
notifications | 内部结构 | 否 | 「受信任的系统事件」,只允许 harness 自己写入。它在生成的 SDK schema 中被schema(ignore)排除,不会出现在前端类型里。 |
name_source | string | 否 | 私有字段,#[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; };| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
uri | string | 是 | Agent 可以抓取该文件的地址。实践中是静态文件服务的 URL。 |
name | string | 是 | 展示名称,通常是原始文件名。 |
mimeType | string \| null | 否 | 媒体类型,已知时填写。 |
size | number \| 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与空数组。
九、实践清单与边界条件
把上面的实现约束整理成可执行的检查表:
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
- 后端
- 前端
- 协同办公
- 即时通讯
- 企业应用
- 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.
相关推荐
markdownlint 列表解析边界场景详解:段落中的数字为何触发 MD029/MD030/MD032/MD004
markdownlint 列表解析边界场景详解:段落中的数字为何触发 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人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考