ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Macro Agent Harness SDK 错误契约详解:GetAgentSessionChangesError 与变更接口的 401/403/500 处理

Macro Agent Harness SDK 错误契约详解:GetAgentSessionChangesError 与变更接口的 401/403/500 处理
  • 后端
  • 前端
  • 协同办公
  • 即时通讯
  • 企业应用
  • 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 TypeScript SDK 中,GetAgentSessionChangesError描述了“读取某个 agent session 最新捕获改动”这一接口可能返回的全部错误分支。它是把 HTTP 错误码映射为调用方可处理类型的最后一环。本文将从这个三行类型别名出发,结合 OpenAPI 规范、Rust 服务端实现与 SDK 测试,说明这些错误在什么条件下产生、错误体是什么形态,以及如何在代码中正确地捕获与降级处理。

一、类型定义本身:一个“按状态码索引”的联合类型

关联文档给出的定义非常精简:

type GetAgentSessionChangesError = GetAgentSessionChangesErrors[keyof GetAgentSessionChangesErrors]

它引用的是同一目录下的 GetAgentSessionChangesErrors,其结构为:

type GetAgentSessionChangesErrors = { 401: string; 403: string; 500: string; }

这里的写法是 TypeScript 的索引访问类型(indexed access type):keyof GetAgentSessionChangesErrors的取值是401 | 403 | 500,因此最终得到的GetAgentSessionChangesError等价于:

type GetAgentSessionChangesError = string; // 401 | 403 | 500 三个分支的值同为 string

需要注意三点,这些细节直接影响错误处理代码的写法:

  1. 键是数字字面量,值是string,不是字面量联合。生成器没有把错误体建模成{ message: string }之类的结构化对象,说明该接口的错误响应体在 OpenAPI 中被声明为text/plain的纯字符串。在 packages/sdk/specs/agent-harness.json 的/agent-sessions/{session_id}/changes定义里,401、403、500 三个响应的content均为text/plain+schema: { type: string }。
  2. 只覆盖 401/403/500,没有 404。这一点与相邻的 patch 接口不同(后者的错误类型额外包含404),后文会解释原因。
  3. 该类型描述的是“错误响应的数据部分”,不包含 HTTP 状态码本身。状态码由调用层的response.status携带,SDK 会把它放进抛出异常对象里。

对应的原始生成代码位于 packages/sdk/generated/agent-harness/types.gen.ts:

export type GetAgentSessionChangesErrors = { 401: string; 403: string; 500: string; }; export type GetAgentSessionChangesError = GetAgentSessionChangesErrors[keyof GetAgentSessionChangesErrors];

二、这个错误类型属于哪个请求

GetAgentSessionChangesError与GetAgentSessionChangesData、GetAgentSessionChangesResponses、GetAgentSessionChangesResponse是同一组生成产物,共同描述一次 HTTP 调用:

export type GetAgentSessionChangesData = { body?: never; path: { /** ID of the agent session */ session_id: string; }; query?: never; url: '/agent-sessions/{session_id}/changes'; }; export type GetAgentSessionChangesResponses = { 200: AgentSessionChangesResponse; };

也就是说,它对应:

GET /agent-sessions/{session_id}/changes
  • 路径参数只有session_id(UUID 字符串),没有请求体,也没有查询参数;
  • 成功时返回200与 AgentSessionChangesResponse,其字段为changeset?: ChangesetDto | null、attempt?: CaptureAttemptDto | null、capturing: boolean;
  • 失败时返回 401/403/500 三者之一,响应体是纯文本字符串,这正是GetAgentSessionChangesErrors中三个string的由来。

在生成的客户端类中,该方法只做一层薄封装,见 packages/sdk/generated/agent-harness/sdk.gen.ts:

/** * The latest captured changes of an agent session: the changed files with * statuses and line counts, and how the latest capture attempt went. */ public getAgentSessionChanges<ThrowOnError extends boolean = false>( options: Options<GetAgentSessionChangesData, ThrowOnError> ): RequestResult<GetAgentSessionChangesResponses, GetAgentSessionChangesErrors, ThrowOnError> { return (options.client ?? this.client).get< GetAgentSessionChangesResponses, GetAgentSessionChangesErrors, ThrowOnError >({ url: '/agent-sessions/{session_id}/changes', ...options }); }

泛型参数ThrowOnError决定了失败时的行为:为false(默认)时返回{ data, error, response }结果对象;为true时直接抛出异常。而GetAgentSessionChangesError正是结果对象里error字段的类型来源。

三、三个状态码分别代表什么

要判断错误该不该重试、该提示用户做什么,必须知道每个码在服务端的产生条件。这一层信息不在生成的 Markdown 里,但可以从 Rust 实现中确认。

3.1 路由与处理函数

/changes相关路由由agent_changescrate 的 Axum router 提供,被挂载进 agent harness 服务(见 services/agent_harness_service/src/api.rs 中对agent_changes_router的合并)。处理函数签名位于 crates/agent_changes/src/inbound/axum_router.rs:

#[utoipa::path( get, path = "/agent-sessions/{session_id}/changes", tag = "agent-sessions", operation_id = "get_agent_session_changes", params(("session_id" = Uuid, Path, description = "ID of the agent session")), responses( (status = 200, body = AgentSessionChangesResponse), (status = 401, body = String), (status = 403, body = String), (status = 500, body = String), ) )] pub async fn get_agent_session_changes_handler<...>( access: AgentSessionAccessLevelExtractor<ViewAccessLevel, Access, Auth>, State(state): State<AgentChangesRouterState<Changes, Access, Auth>>, ) -> Result<Json<AgentSessionChangesResponse>, AgentChangesApiError> { let changes = state.service.changes(&access.entity_access_receipt).await?; Ok(Json(changes.into())) }

可以看到两个关键信息:

  • 参数提取器是AgentSessionAccessLevelExtractor<ViewAccessLevel, ...>,即该接口要求调用方对目标 session 具备查看(view)级别的访问权限;
  • utoipa注解声明的响应集合恰好是200/401/403/500,与 TypeScript 侧的GetAgentSessionChangesErrors完全一致——这就是“类型别名只有三个键”的根因。

3.2 错误枚举到 HTTP 状态的映射

AgentChangesApiError的IntoResponse实现把领域错误翻译为线上状态,见 crates/agent_changes/src/inbound/axum_router.rs:

match self { Self::Domain(ChangesError::Forbidden) => (StatusCode::FORBIDDEN, "forbidden").into_response(), Self::Domain(error @ (ChangesError::NoChangeset | ChangesError::PatchMissing)) => (StatusCode::NOT_FOUND, error.to_string()).into_response(), Self::Domain(ChangesError::Session(AgentSessionError::Forbidden)) => (StatusCode::FORBIDDEN, "forbidden").into_response(), Self::Domain(error) => { tracing::error!(error = ?error, "agent session changes request failed"); (StatusCode::INTERNAL_SERVER_ERROR, "internal error").into_response() } }

结合领域错误定义 crates/agent_changes/src/domain/error.rs,可以整理出下表:

状态码触发条件响应体
401请求未通过认证(缺少或无效的凭据)纯文本字符串
403调用方对 session 没有 view 权限;ChangesError::Forbidden;或会话读取本身返回AgentSessionError::Forbidden字面量forbidden
500存储层失败(ChangesError::Storage)、读取会话出错,或其它未归类领域错误字面量internal error,服务端同时记录 error 日志

补充说明两点:

  • 为什么/changes没有 404:ChangesError::NoChangeset(尚未捕获过任何改动)与ChangesError::PatchMissing(元数据存在但 blob 丢失)只在读取 diff 内容时才会触发,它们被映射为 404,只出现在/changes/patch的响应集合中。因此 prompt 的GetAgentSessionChangesPatchErrors比本类型多一个404键;而/changes在“还没捕获成功”时依然返回200,只是changeset为null。
  • 500 不泄露内部细节:响应体固定是internal error,真正的错误只进tracing::error!日志。因此客户端无法从500的 body 判断根因,只能依赖重试或联系服务端排查。

四、在 SDK 中如何捕获这个错误

生成的sdk.gen.ts是底层客户端;日常使用更推荐上层的实体封装,它会把错误统一成可判定的异常。相关链路如下:

  1. packages/sdk/src/entities/agent-sessions/agent-session.ts 中的changes()调用getAgentSessionChanges,并用unwrap解包:
/** Read the latest captured GitHub pull request changes and capture status. */ async changes(): Promise<AgentSessionChangesResponse> { return unwrap( await this.client.agentHarness.getAgentSessionChanges({ path: { session_id: this.id }, }), ); }
  1. unwrap在 packages/sdk/src/utils/unwrap.ts 中把{ data, error, response }转成“成功返回值或抛异常”:
export function unwrap<TData, TError>(res: { data?: TData; error?: TError; response?: Response; }): TData { if (res.error !== undefined) throw new MacroApiError(res.response?.status ?? 0, res.error); return res.data as TData; }
  1. MacroApiError同时携带status与data(即错误响应体,本接口下就是那段纯文本),见 packages/sdk/src/utils/unwrap.ts。

因此,GetAgentSessionChangesError在运行时表现为MacroApiError.data,类型上就是string。下面是一个完整的调用与分类处理示例(Macro客户端的构造方式与仓库测试保持一致):

import { Macro } from '@macro-inc/sdk'; import { MacroApiError } from '@macro-inc/sdk/utils/unwrap'; const macro = new Macro({ token: process.env.MACRO_TOKEN!, // 用户访问令牌 hosts: { 'agent-harness': 'https://agent.example.test' }, }); const session = macro.agentSessions.byId(sessionId); try { const changes = await session.changes(); if (changes.capturing) { // 捕获正在进行,changeset 可能还是上一轮的旧值 } if (!changes.changeset) { // 200 但从未成功捕获过:属于“暂无数据”,不是错误 } } catch (err) { if (err instanceof MacroApiError) { switch (err.status) { case 401: // 令牌缺失/过期,需要重新认证,重试无意义 break; case 403: // 对 session 没有 view 权限,或会话已不可读 // err.data 通常是 'forbidden' break; case 500: // 存储层等内部故障,可退避重试 // err.data 为 'internal error' break; default: // 其它状态码(例如网络层错误可能为 0) break; } } throw err; }

如果你直接使用生成的底层客户端并希望以异常风格调用,可以把throwOnError设为true,此时GetAgentSessionChangesErrors同样会作为错误信息类型参与推断:

const client = macro.agentHarness; // 或自行构造 Sdk 实例 try { const res = await client.getAgentSessionChanges({ path: { session_id: sessionId }, throwOnError: true, }); console.log(res.data.changeset?.files); } catch (err) { // 这里的错误来自 GetAgentSessionChangesError 分支 }

五、测试如何验证这套错误契约

仓库中的 SDK 测试对“403 必须原样保留”有明确断言,见 packages/sdk/tests/agent-sessions.test.ts:

test('preserves access errors from session result endpoints', async () => { globalThis.fetch = (async (_input) => Response.json({ message: 'Forbidden' }, { status: 403 })) as typeof fetch; const macro = new Macro({ token: 'user-token', hosts: { 'agent-harness': 'https://agent.example.test' }, }); const agent = macro.agentSessions.byId(sessionId); for (const read of [ () => agent.changes(), () => agent.changesPatch(), () => agent.refreshChanges(), () => agent.codeExecutions .byId('0198a4cc-e138-7670-a308-a6b766602702') .status(), ]) { await expect(read()).rejects.toMatchObject({ name: 'MacroApiError', status: 403, data: { message: 'Forbidden' }, }); } });

这条用例说明了两点工程约束:

  • 状态码必须保留:上层封装不能把 401/403/500 吞掉或统一改写,否则调用方无法做区分处理;
  • 错误体必须透传:data字段原样保留服务端返回的内容。测试里用 JSON 体模拟响应以便断言结构,而真实服务端对该接口返回的是纯文本(forbidden/internal error),这也解释了为什么生成类型把错误值标为string而不是结构化对象。

同文件的另一条用例(agent-sessions.test.ts)则验证了成功路径:changes()走GET /changes、changesPatch()走GET /changes/patch、refreshChanges()走POST /changes/refresh,且三者都带Authorization: Bearer <token>头,与401的语义形成对照。

六、与相邻错误类型的边界

理解了GetAgentSessionChangesError,就能理解它周围几个类型的差异,避免在错误处理里写错分支:

类型对应操作状态码键典型差异
GetAgentSessionChangesErrorGET /changes401 / 403 / 500无 404;未捕获成功时返回 200 +changeset: null
GetAgentSessionChangesPatchErrorGET /changes/patch401 / 403 /404/ 500404 表示NoChangeset或PatchMissing
RefreshAgentSessionChangesErrorPOST /changes/refresh401 / 403 / 500返回202与当前状态,捕获在后台继续

这种“只看状态码不够、还要看是哪个端点”的设计,是排查变更类问题时的常见坑:同一个500在/changes上意味着读取失败,而在/changes/refresh上还可能是触发捕获失败;同一个403在/changes上是 view 权限问题,在 patch 上同样是 view 权限问题,但排错方向应优先确认 session 的共享配置而非 diff 存储。

七、实战排查清单

当GetAgentSessionChangesError真的出现时,可以按下面的顺序定位:

  1. 先区分 401 与 403。两者都表现为“无法访问”,但 401 是凭据层面的问题(令牌缺失、过期、host 配错导致打到错误服务),403 是授权层面的问题(当前用户对该 session 无 view 权限)。仓库测试要求两者都被保留为MacroApiError.status,因此你可以直接根据status分流,而不必解析文案。
  2. 403 时检查 session 的共享设置。服务端在AgentSessionAccessLevelExtractor<ViewAccessLevel, ...>处就完成了判定,权限不足根本不会进入业务逻辑,因此修改重试策略没有意义。可以先用session.sharePermissions()查看当前授权(见 agent-session.ts)。
  3. 500 时优先退避重试,同时保留上下文。由于响应体固定为internal error,真正信息在服务端日志(agent session changes request failed这条 tracing 事件里),客户端应记录session_id与时间点以便对齐。
  4. 不要把“没有改动”当成错误。如果任务刚启动、尚未完成任何捕获,/changes会返回200且changeset为null,attempt可能处于进行中(capturing: true)。此时正确做法是等待或调用refreshChanges(),而不是把响应当作失败处理。
  5. 需要 diff 文本时再请求 patch。changes()只返回文件清单与统计(ChangedFileDto的additions/deletions/kind/binary/patchOmitted等),真正的统一 diff 通过changesPatch()获取;只有在确认changeset存在后才调用它,才能避开 patch 端点独有的 404。

小结

GetAgentSessionChangesError看起来只是一个三行的生成类型,但它精确地固化了GET /agent-sessions/{session_id}/changes的错误契约:错误体是纯文本字符串,状态码只有 401、403、500,且 404 被有意排除。Rust 侧的AgentChangesApiError把这个契约落实到领域错误的每一次映射,TypeScript 侧的unwrap与MacroApiError则保证状态码与错误体在调用链中原样传递。掌握这条链路之后,变更读取相关的失败就不再是“一个报错”,而是可以按认证、授权、服务端故障三类分别处理的确定性分支。

  • 后端
  • 前端
  • 协同办公
  • 即时通讯
  • 企业应用
  • 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
点击查看免费下载

相关推荐

上一篇:从请求到响应:gh_mirrors/ht/http-kernel的高性能消息处理指南
下一篇:Hello-Agents 部署:3 条命令在本地跑起多智能体科研助手

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

RELATED READING

延伸阅读

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