- 后端
- 前端
- 协同办公
- 即时通讯
- 企业应用
- 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.
AgentSessionQueueResponse是 Macro 的 Agent Harness OpenAPI 类型体系中,唯一描述"某个 Agent 会话当前还有哪些动作在排队"的响应类型。它不是随手包了一层数组,而是 Macro 在"一个会话同时只能跑一个 turn"这一硬约束下,把排队、编辑、抢占、撤销等能力暴露给客户端的关键契约。读完本文,你可以准确写出解析该响应的前端代码、理解每个字段在服务端的真实来源、并掌握与之配套的队列 API 与实时通道行为。
一、类型定义与原文档契约
关联文档给出的定义非常短,但信息密度很高:
type AgentSessionQueueResponse = { entries: QueuedActionDto[]; };原文档对它的描述是两句话,值得原样理解:
- "Response body for a session's queue: everything waiting, oldest first."—— 响应体描述一个会话的队列,包含所有等待中的动作,并按最旧优先排列。
- "A wrapper rather than a bare array so that anything which is about the response rather than about an entry has somewhere to go later without breaking every client."—— 之所以用对象包一层而不是裸数组,是为了将来出现"属于响应整体、而非属于某一条目"的字段时,有地方可放,且不会破坏所有客户端。
这个"包一层"的决定在 Rust 侧与文档完全对应,见 axum_router.rs:
/// Response body for a session's queue: everything waiting, oldest first. /// /// A wrapper rather than a bare array so that anything which is about the /// response rather than about an entry has somewhere to go later without /// breaking every client. #[derive(Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct AgentSessionQueueResponse { /// The waiting actions, in dispatch order. pub entries: Vec<QueuedActionDto>, }几个可直接落到实现层面的结论:
camelCase是硬约束。#[serde(rename_all = "camelCase")]意味着 Rust 的entries字段在 JSON 中就是entries,但它同时约束了未来新增字段的命名风格。客户端应按 camelCase 解析,不要假设 snake_case 兼容。Deserialize也存在。该类型同时派生Serialize与Deserialize,这与QueuedActionDto注释里那句 "Clients deserialize this, so both derives are used."(见 model.rs)是同一设计取向:类型既用于服务端出参,也被服务端/测试侧当入参反序列化,形状不允许两侧漂移。ToSchema让它是 OpenAPI 的一等公民。AgentSessionQueueResponse会出现在生成文档中,并成为GetAgentSessionQueueResponse/GetAgentSessionQueueResponses这类生成类型的源头,见 GetAgentSessionQueueResponses.md:{ 200: AgentSessionQueueResponse }。
二、entries里的每一条:QueuedActionDto的六个字段
AgentSessionQueueResponse.entries的元素类型是QueuedActionDto。原文档对它的定义与字段说明如下:
| 属性 | 类型 | 说明 |
|---|---|---|
actionId | AgentActionId | 该动作被接受时使用的 id |
kind | string | 等待中的动作类型——prompt或compact;只有占用整个 turn 的动作才会入队 |
prompt? | string \| null | prompt 的原始文本,仅 prompt 存在;编辑操作替换的就是它 |
attachments? | PromptAttachment[] | prompt 引用的文件,仅 prompt 有;编辑会保留它,因为编辑只替换文本 |
actorUserId? | string \| null | 入队的用户;当 bot 不代表任何人操作时缺省 |
createdAt | string | 该动作被接受的时间 |
其 Rust 定义在 model.rs,字段名与 JSON 名通过rename_all = "camelCase"一一对应(action_id→actionId,actor_user_id→actorUserId,created_at→createdAt)。
2.1actionId:贯穿控制端点、线协议与折叠消息的同一条 id
actionId的类型是AgentActionId,在生成文档中被定义为string,并附有明确语义(见 AgentActionId.md):它端到端标识一个被接受的AgentAction——由控制端点返回,作为 JSON-RPC request id 写到该动作的线帧上,并在其派生出的折叠消息上以requestId被读回。这是一个v7 uuid,因此 id 本身按生成时间可排序;服务端在接受时生成,或采纳客户端在控制请求中自带("speculate")的 id。
这对客户端有一个非常实际的意义:当你用同一个actionId乐观渲染了一条消息,随后收到queued状态时,队列里的这一条和后续折叠消息里的requestId是同一个值,可以原地"提升"同一条 UI 记录,而不必撤回重建。ControlRequest.action_id的注释也明确写了这一点(见 axum_router.rs)。
2.2kind:为什么队列里只有prompt和compact
原文档明确:kind取值为prompt或compact,"只有占用整个 turn 的动作才会被排队"。这在AgentAction枚举里可以验证——见 action.rs,完整的动作集合是Prompt、SetModel、SetConfigOption、Compact、Stop、RespondElicitation、RespondToPermission;但只有Prompt与Compact需要独占整轮对话。队列模块的文档解释了这个根因(见 queue.rs):
Prompts(以及 compaction)占用整个 agent turn,而 ACP 一次只跑一个 turn —— 所以当一个 turn 在运行时,后续的"占用 turn 的动作"会在这里等待,而不是在线路上交错执行。
kind的取值来自AgentAction的AsRefStr(strum(serialize_all = "snake_case"))。在QueuedActionDto的转换实现中可以看到(见 model.rs):
let (prompt, attachments) = match &queued.action { AgentAction::Prompt(action) => (Some(action.prompt.clone()), action.attachments.clone()), _ => (None, Vec::new()), }; Self { action_id: queued.action_id, kind: queued.action.as_ref().to_owned(), prompt, attachments, actor_user_id: queued.actor.map(|actor| actor.to_string()), created_at: queued.created_at, }由此可以确认三条实现事实:kind是action的 snake_case 判别名;prompt与attachments仅对Prompt赋值,Compact时分别为None与空数组;actorUserId是宏用户 id 的字符串化结果。
2.3prompt与attachments:为什么编辑只会替换文本
原文档写明prompt是"prompt 的原始文本",attachments是"prompt 引用的文件",且"编辑会保留 attachments,因为编辑只替换文本"。这不是文档随便写的约定,而是队列实现的直接结果(见 queue.rs 的edit_prompt):
match &mut entry.action { AgentAction::Prompt(action) => { action.prompt = prompt; entry.actor = actor; Ok(()) } AgentAction::SetModel(_) | AgentAction::SetConfigOption(_) | AgentAction::Compact | AgentAction::Stop | AgentAction::RespondElicitation(_) | AgentAction::RespondToPermission(_) => Err(QueueError::NotEditable), }edit_prompt只覆写action.prompt,action.attachments原封不动,所以QueuedActionDto.attachments在编辑后依然保留在同一actionId下。同时注意entry.actor = actor:编辑会把入队者改成编辑者——edit_prompt的文档说得很直白:"谁改写了文本,谁就是之后 dispatch 时被宣布、被记录的人,编辑不能让别人的名字挂在自己没打的字上。"
attachments的元素是PromptAttachment,其定义见 PromptAttachment.md:{ uri: string; name: string; mimeType?: string | null; size?: number | null }。它"镜像 ACP 的resource_link内容块"——字节不会经过 prompt,只有一个 URI(实践中是静态文件服务 URL)加上足够客户端渲染 chip、agent 决定是否抓取的元数据。这正是为什么编辑文本无需重新上传附件。
2.4actorUserId:可以为空的归属
actorUserId对应 Rust 的Option<String>,语义是"入队者;当 bot 不代表任何人操作时缺省"。同样的措辞出现在StoredQueuedAction.actor(model.rs)与QueuedControl.actor(ports.rs)上。客户端解析时应当把它当作可缺省字段,而不是空串占位。
2.5createdAt:RFC 3339 时间戳
created_at在 Rust 中是DateTime<Utc>,序列化为字符串。SDK 侧的QueuedAction直接把 getter 文档写作 "When the action was accepted, as an RFC 3339 timestamp"(见 queued-action.ts)。它是队列排序语义的显式证据——但客户端不必自己排序,因为服务端已保证顺序(见下节)。
三、服务端如何产出这个响应:GET /agent-sessions/{session_id}/queue
AgentSessionQueueResponse只有一个生产者:获取队列的 GET 端点。它的utoipa::path声明与处理器在 axum_router.rs:
#[utoipa::path( get, path = "/agent-sessions/{session_id}/queue", tag = "agent-sessions", operation_id = "get_agent_session_queue", params(("session_id" = Uuid, Path, description = "ID of the agent session")), responses( (status = 200, body = AgentSessionQueueResponse), (status = 401, body = String), (status = 403, body = String), (status = 500, body = String), ) )] pub async fn get_agent_session_queue_handler<...>( _access: AgentSessionAccessLevelExtractor<ViewAccessLevel, Access, Auth>, State(state): State<AgentSessionControlState<R, Access, Auth>>, caller: MacroAuthorizationExtractor<Auth, UserBotOrHarness>, Path(session_id): Path<Uuid>, ) -> Result<Json<AgentSessionQueueResponse>, AgentSessionApiError> { let session_id = AgentSessionId::new_from_uuid(session_id); ensure_harness_serves_session(&caller.authorization, state.recipient.as_ref(), session_id).await?; let entries = state.recipient.queued_controls(session_id).await?; Ok(Json(AgentSessionQueueResponse { entries: entries.into_iter().map(Into::into).collect(), })) }从这段代码可以读出四个必须知道的运行时约束:
- 读队列只需 View 权限。路由模块文档明确指出:"查看会话、其日志或队列需要
View;控制它——发送,以及编辑或移除排队内容——需要Edit"(见 axum_router.rs)。因此只读的旁观者也能拿到完整队列。 - router 只接受
UserBotOrHarness调用方。授权提取器限制了谁可以读队列。 ensure_harness_serves_session会校验"当前这个副本确实承载该会话"。这意味着队列读取是进程本地语义的:队列是活的运行时状态,不是纯数据库投影。queued_controls是端口方法,返回Vec<QueuedControl>,再通过Into::into逐条转成QueuedActionDto。也就是说DTO 的构造是集中的、唯一的,GET与实时快照共享同一转换。
3.1 端口契约:queued_controls的三条兄弟方法
队列端点不是孤立的,它与编辑、删除、抢占三个同级能力共享同一个端口,定义在 ports.rs:
| 能力 | 端口方法 | 队列中的语义 |
|---|---|---|
| 读取 | queued_controls(id) | 返回等待中的动作,最旧优先 |
| 编辑 | edit_queued_control(id, action_id, prompt, actor) | 已 dispatch →QueuedControlNotFound;无可编辑文本 →QueuedControlNotEditable |
| 移除 | remove_queued_control(id, action_id, actor) | 已 dispatch →QueuedControlNotFound |
| 抢占 | steer_queued_control(id, action_id, actor) | 移到队首,并取消在飞的 turn,使该条在 turn 结束后立刻 dispatch |
对应的 HTTP 路由在 axum_router.rs:
GET /{session_id}/queue PUT /{session_id}/queue/{action_id} # 编辑 DELETE /{session_id}/queue/{action_id} # 移除 POST /{session_id}/queue/{action_id}/steer # 抢占编辑端点的请求体是EditQueuedActionRequest,只有一个字段prompt,且min_length = 1;它的注释解释了为什么不能为空:"一个什么都没说的 prompt 就是一次删除,而删除有专门的端点。"(见 axum_router.rs 与 EditQueuedActionRequest.md)。处理器还会额外做request.prompt.trim().is_empty()检查,空/纯空白直接返回EmptyQueuedPrompt。
3.2 错误码到 HTTP 状态的映射
AgentSessionQueueResponse的 200 分支之外,四个队列相关错误在 axum_router.rs 有确定映射:
| 错误 | HTTP 状态 | 触发场景 |
|---|---|---|
QueuedControlNotFound | 404 | 已 dispatch、已移除,或从未入队 |
QueuedControlNotEditable | 422 | 对该 action 而言队列条目不携带文本(如compact) |
EmptyQueuedPrompt | 422 | 编辑为空或纯空白 |
ControlQueueFull | 422 | 该会话排队动作过多 |
错误文案本身也实现了自解释,见 error.rs:
no queued action with this id; it may already have been dispatched only queued prompts can be edited a queued prompt cannot be edited to say nothing; remove it instead agent session {0} has too many queued actions四、顺序与容量的实现真相
原文档只说 "oldest first",实现里则有两处不容忽视的细节。
4.1 顺序就是 dispatch 顺序,来自VecDeque
队列结构是VecDeque<QueuedEntry>,list直接按迭代顺序映射为QueuedControl(见 queue.rs):
pub fn list(&self, session: AgentSessionId) -> Vec<QueuedControl> { self.queues .get(&session) .map(|queue| queue.iter().map(QueuedControl::from).collect()) .unwrap_or_default() }claim_next用pop_front(),因此entries[0]就是下一条将要 dispatch 的动作。仓储测试 queue/test.rs 用entries_come_back_out_oldest_first把这条不变量钉死:先入队的 id 既排在list()的第一位,也是claim_next()的第一个返回值,且队列排空后list()为空。
注意list()对不存在的会话返回空向量而非错误——所以entries: []是合法的 200 响应,代表队列为空,而不是会话不存在。客户端不要把它当异常处理。
4.2 容量上限是50,且是"防跑飞"的兜底
QUEUE_CAP定义在 queue.rs:
/// How many actions one session may hold queued. Far above any real /// conversation; a backstop against a runaway bot caller, not a product /// limit. const QUEUE_CAP: usize = 50;insert在超过上限时返回QueueError::Full(queue.rs),最终映射为上表的ControlQueueFull→ 422。所以"队列满"是调用控制端点(发送 prompt)时被拒,而不是读队列时报错——读取永远只返回现有内容。
4.3 失败 dispatch 会把条目放回队首
dispatch_next的文档说明了一个对客户端行为很重要的语义(见 domain/service/queue.rs):dispatch 失败时条目"被放回队首,仍在下一轮或下一次 prompt 时最先执行,并在此期间继续可见于队列"。这意味着客户端观察到某条actionId在一次 turn 结束后仍然出现在队列里,是正常的重试路径,不应视为重复入队。同一段注释还说明,该条目的 chip 消息会被记住,"在某次 announce 成功后失败的 dispatch 会重试而不会再发第二条 chip"。
五、与实时通道的字节级一致性
AgentSessionQueueResponse并不只在 REST 上出现。同一个QueuedActionDto数组还会通过 Connection Gateway 以agent_session_queue事件推送。这条"同一份字节"的设计在 connection_gateway_realtime.rs 中说明得最清楚:
/// The body of an [`AGENT_SESSION_QUEUE`] message. /// /// `entries` are [`QueuedActionDto`] - the exact rows `GET .../queue` serves ... /// so a client baselining from the REST endpoint and one following the socket /// are reading the same bytes, so they cannot disagree about what an entry means. pub struct AgentSessionQueueEvent { pub agent_session_id: Uuid, pub entries: Vec<QueuedActionDto>, }并且这个事件永远推送完整队列,绝不推送增量(connection_gateway_realtime.rs):
Always the full queue, never a delta: any one event is a complete, self-sufficient truth, which is what lets a client treat the socket as the only writer once it has heard anything on it — a late
GET .../queueresponse can never carry information the socket will not deliver.
前端手写协议镜像了这一点,见 realtime-protocol.ts:事件常量AGENT_SESSION_QUEUE_EVENT = 'agent_session_queue',类型AgentSessionQueueEvent = { agentSessionId; entries: QueuedActionDto[] },注释明确"最后一个到达的事件无条件胜出(last one wins unconditionally)"。
在服务端,这条推送由publish_queue发出,失败只记 warn,不影响主流程(见 domain/service/queue.rs):
let _ = self.sessions .publish_queue_changed(AgentSessionQueueChanged { agent_session_id: session_id, entries: self.queues.list(session_id), }) .await .inspect_err(|error| tracing::warn!(...));对客户端由此得到一条明确的实现建议:一旦收到过agent_session_queue事件,就以 socket 为唯一真相来源;GET只用于首次基线化,用于打破"没有实时连接"的降级场景。
六、在 TypeScript SDK 中消费这个响应
如果不想直接面对entries,Macro 的 SDK 已经做了封装。AgentSession.queue()会调用生成的getAgentSessionQueue,解包后把每个QueuedActionDto包成QueuedAction对象(见 agent-session.ts):
async queue(): Promise<QueuedAction[]> { const { entries } = unwrap( await this.client.agentHarness.getAgentSessionQueue({ path: { session_id: this.id }, }), ); return entries.map((entry) => QueuedAction.from(this.client, this.id, entry)); }QueuedAction是"列表来源"的包装器而不是按 id 懒加载的实体——因为根本没有GET /queue/{action_id}这种单条读端点,只有整队列快照(见 queued-action.ts)。它暴露actionId、kind、prompt、attachments、actorUserId、createdAt六个 getter,与文档字段严格一一对应,并额外提供edit()、steer()、remove()。
关于edit(),有一个容易踩的细节:编辑端点返回204 No Content,因此 SDK 在本地直接合并prompt字段,而不是从响应体回读(见 queued-action.ts):
await this.client.agentHarness.editQueuedAction({ path: { session_id: this.sessionId, action_id: this.actionId }, body: { prompt }, }); this.dto = { ...this.dto, prompt };它的注释还提示了另一个副作用:edit只更新本地prompt,而服务端会同时把actorUserId改成编辑者。如果你的 UI 依赖入队者展示,应通过 socket 的完整快照刷新,而不是依赖本地合并结果。
发送一侧的衔接点是control()返回的ControlResponse(见 agent-session.ts):返回的actionId与折叠消息的requestId相同,status为queued时表示"有 turn 在跑,动作正在队列中等待"。ControlStatusDto的取值就是"sent" | "queued"(见 ControlStatusDto.md),其中Queued的文档直接引用了队列端点:"在它 dispatch 之前,GET .../queue会列出它,并且可以在它的 action id 下被编辑或移除。"
七、一份可直接对照的响应样例
下面这个响应体完全依据AgentSessionQueueResponse+QueuedActionDto的序列化规则推导(camelCase、可选字段缺省时省略),可用于前端 mock 与契约测试:
{ "entries": [ { "actionId": "0199c3a1-7f2b-7c3d-9e10-2ab4c5d6e7f8", "kind": "prompt", "prompt": "把 email 模块的错误处理统一成 anyhow 上下文", "attachments": [ { "uri": "https://static.example/uploads/spec.pdf", "name": "spec.pdf", "mimeType": "application/pdf", "size": 182734 } ], "actorUserId": "macro|alice@example.com", "createdAt": "2026-11-01T00:01:12.345Z" }, { "actionId": "0199c3a1-8a11-7d55-b001-77c9e2d4a310", "kind": "compact", "createdAt": "2026-11-01T00:02:40.900Z" } ] }观察第二条:compact没有prompt、没有attachments、也没有actorUserId(假设由系统触发)。这与From<QueuedControl> for QueuedActionDto的分支完全吻合——对非Prompt分支,prompt为None、attachments为空向量,而#[serde(skip_serializing_if = "Option::is_none")]与#[serde(skip_serializing_if = "Vec::is_empty")]会让它们从 JSON 中消失,而不是输出null或[]。因此客户端的类型标注应为可选字段,而不是必填可空字段:把prompt写成string | null是安全的宽松写法,但把它当必填会解析失败。
八、落地时的七条实践清单
- 按
entries[0]就是"下一条要跑的动作"来做 UI,不要按createdAt自行重排,否则与steer抢占后的服务端顺序冲突。 - 不要缓存
GET结果与 socket 结果两份真相。收到agent_session_queue后以事件为准,事件是完整快照,天然幂等。 - 把
actionId当主键贯穿全链路:队列条目 →PUT/DELETE /queue/{action_id}→ 折叠消息的requestId。这样"乐观 UI 提升"与"事后审计"共用一条 id。 - 对 404 做显式分支。
QueuedControlNotFound表示"已经跑了或本来就没有",属于正常的竞态结果(用户点了删除,但此刻恰好 dispatch),不应弹系统错误。 - 编辑前先判
kind。非prompt的条目(如compact)调用编辑会拿到 422QueuedControlNotEditable;前端应在 UI 上直接禁用编辑入口。 - 空字符串不是删除。编辑
prompt为空白会得到 422EmptyQueuedPrompt,删除请走DELETE。 steer会取消在飞的 turn。它把条目移到队首并中断当前 turn(见 ports.rs 的注释与处理器文档),因此它是有副作用的破坏性操作,UI 需要二次确认。
九、延伸阅读的仓库入口
- 响应与 DTO 定义:crates/agent_session/src/inbound/axum_router.rs、crates/agent_session/src/domain/model.rs
- 队列端点与错误映射:crates/agent_session/src/inbound/axum_router.rs
- 端口契约:crates/agent_session/src/domain/ports.rs
- 队列实现与不变量测试:crates/agent_harness/src/domain/queue.rs、crates/agent_harness/src/domain/queue/test.rs
- 实时事件与前端镜像:crates/agent_session/src/outbound/connection_gateway_realtime.rs、apps/web/src/lib/queries/agent-session/realtime-protocol.ts
- SDK 封装:packages/sdk/src/entities/agent-sessions/agent-session.ts、packages/sdk/src/entities/agent-sessions/queued-action.ts
- 生成类型索引:packages/sdk-docs/reference/generated/agent-harness/types.gen/README.md
- 后端
- 前端
- 协同办公
- 即时通讯
- 企业应用
- 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.
相关推荐
Macro Agent Harness 的 ControlAgentSessionResponses 类型详解:实时控制会话的响应契约与队列语义
Macro Agent Harness 的 ControlAgentSessionResponses 类型详解:实时控制会话的响应契约与队列语义 Control
后端前端协同办公即时通讯企业应用CRMAI Agent人工智能CreateAgentSessionResponse 详解:Macro Agent Harness 创建 Agent 会话的响应契约
CreateAgentSessionResponse 详解:Macro Agent Harness 创建 Agent 会话的响应契约 在 Macro 的 Age
后端前端协同办公即时通讯企业应用CRMAI Agent人工智能Macro Agent Harness 中的 ListAgentConversationsData:频道内 Agent 会话的读取契约与实现
Macro Agent Harness 中的 ListAgentConversationsData:频道内 Agent 会话的读取契约与实现 在 Macro 的
后端前端协同办公即时通讯企业应用CRMAI Agent人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考