@sema-agent/sdk 0.1.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +17 -0
  2. package/dist/client.d.ts +1 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +1 -1
  5. package/dist/client.js.map +1 -1
  6. package/dist/errors.d.ts +97 -28
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +249 -117
  9. package/dist/errors.js.map +1 -1
  10. package/dist/events.d.ts +11 -22
  11. package/dist/events.d.ts.map +1 -1
  12. package/dist/events.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +10 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/normalize.d.ts +17 -4
  18. package/dist/normalize.d.ts.map +1 -1
  19. package/dist/normalize.js +16 -9
  20. package/dist/normalize.js.map +1 -1
  21. package/dist/resources/approvals.d.ts +2 -7
  22. package/dist/resources/approvals.d.ts.map +1 -1
  23. package/dist/resources/approvals.js +3 -3
  24. package/dist/resources/approvals.js.map +1 -1
  25. package/dist/resources/assistant.d.ts +1 -1
  26. package/dist/resources/assistant.js +1 -1
  27. package/dist/resources/attachments.d.ts.map +1 -1
  28. package/dist/resources/attachments.js +3 -4
  29. package/dist/resources/attachments.js.map +1 -1
  30. package/dist/resources/fleet.d.ts +4 -4
  31. package/dist/resources/fleet.d.ts.map +1 -1
  32. package/dist/resources/fleet.js +2 -3
  33. package/dist/resources/fleet.js.map +1 -1
  34. package/dist/resources/images.d.ts +4 -4
  35. package/dist/resources/images.d.ts.map +1 -1
  36. package/dist/resources/images.js +7 -6
  37. package/dist/resources/images.js.map +1 -1
  38. package/dist/resources/leader.d.ts +2 -3
  39. package/dist/resources/leader.d.ts.map +1 -1
  40. package/dist/resources/leader.js +2 -3
  41. package/dist/resources/leader.js.map +1 -1
  42. package/dist/resources/memory.d.ts +11 -53
  43. package/dist/resources/memory.d.ts.map +1 -1
  44. package/dist/resources/memory.js +2 -51
  45. package/dist/resources/memory.js.map +1 -1
  46. package/dist/resources/models.d.ts +4 -4
  47. package/dist/resources/models.js +1 -1
  48. package/dist/resources/ops.d.ts +3 -3
  49. package/dist/resources/ops.d.ts.map +1 -1
  50. package/dist/resources/ops.js +3 -1
  51. package/dist/resources/ops.js.map +1 -1
  52. package/dist/resources/runs.d.ts.map +1 -1
  53. package/dist/resources/runs.js +1 -2
  54. package/dist/resources/runs.js.map +1 -1
  55. package/dist/resources/session-sync.d.ts +1 -2
  56. package/dist/resources/session-sync.d.ts.map +1 -1
  57. package/dist/resources/session-sync.js +4 -13
  58. package/dist/resources/session-sync.js.map +1 -1
  59. package/dist/resources/sessions.d.ts +4 -4
  60. package/dist/resources/sessions.d.ts.map +1 -1
  61. package/dist/resources/sessions.js +9 -5
  62. package/dist/resources/sessions.js.map +1 -1
  63. package/dist/resources/tasks.d.ts.map +1 -1
  64. package/dist/resources/tasks.js +1 -2
  65. package/dist/resources/tasks.js.map +1 -1
  66. package/dist/resources/workflows.d.ts.map +1 -1
  67. package/dist/resources/workflows.js +1 -2
  68. package/dist/resources/workflows.js.map +1 -1
  69. package/dist/resources/workspace.d.ts.map +1 -1
  70. package/dist/resources/workspace.js +8 -9
  71. package/dist/resources/workspace.js.map +1 -1
  72. package/dist/sse.d.ts.map +1 -1
  73. package/dist/sse.js +7 -5
  74. package/dist/sse.js.map +1 -1
  75. package/dist/transport.d.ts +25 -0
  76. package/dist/transport.d.ts.map +1 -1
  77. package/dist/transport.js +37 -2
  78. package/dist/transport.js.map +1 -1
  79. package/dist/types.d.ts +108 -55
  80. package/dist/types.d.ts.map +1 -1
  81. package/openapi.yaml +184 -219
  82. package/package.json +2 -2
package/dist/errors.js CHANGED
@@ -5,36 +5,96 @@
5
5
  */
6
6
  export class APIError extends Error {
7
7
  status;
8
- code;
8
+ errorCode;
9
9
  /** `true` = the server said this failure is RETRIABLE: the durable park is still pending, so re-fetching and
10
10
  * deciding/resuming again is meaningful (server sets `retriable: true` on the /decide + resume legs).
11
11
  *
12
12
  * 🔴 `undefined` means "the server did not say", NOT "not retriable" — it is a CONDITIONAL field server-side
13
13
  * (absent ⇒ key omitted), so reading its absence as an explicit `false` over-reads the wire.
14
14
  *
15
- * ⚠️ 为什么它不是 `readonly` / 不是构造参数:`retriable` 可以伴随**任何**分支的错误一起回来,而这里已有 25 个
15
+ * ⚠️ 为什么它不是 `readonly` / 不是构造参数:`retriable` 可以伴随**任何**分支的错误一起回来,而这里已有三十多个
16
16
  * 子类、其中好几个有自己的位置化构造签名(retryAfterMs / allowlist / relation …)。做成第 4 个构造参数就得逐个
17
17
  * 子类穿线,漏一个就是静默丢信号;做成 mapper 末尾的**单点盖章**则天然覆盖全部返回路径。这是刻意取舍。 */
18
18
  retriable;
19
- constructor(status, code, message) {
19
+ constructor(status, errorCode, message) {
20
20
  super(message);
21
21
  this.status = status;
22
- this.code = code;
22
+ this.errorCode = errorCode;
23
23
  this.name = new.target.name;
24
24
  }
25
25
  }
26
- /** 401 / 503 — fail-closed auth (missing/bad SERVICE_AUTH_TOKEN). */
26
+ /** `auth.*` 前缀族 + 401/503 状态兜底 —— fail-closed auth(缺/错 SERVICE_AUTH_TOKEN、非属主、
27
+ * operator-only、bake-runner-only、principal 头缺席…)。
28
+ *
29
+ * 🔴 键位优先于状态(SDK 1.1.0):此前这个类**只**按 `status === 401 || status === 503` 分,于是
30
+ * `auth.forbidden` / `auth.operator_only` / `auth.bake_runner_only` / `auth.ingest_secret_invalid`
31
+ * 四个 403 全塌成裸 APIError,而同一条 503 分支还错吞了 `state.*`(见 {@link ServiceStateError})。
32
+ * 现在先按 `auth.` 前缀分,状态兜底留作**没有机器码时**的退路。 */
27
33
  export class AuthError extends APIError {
28
34
  }
35
+ // ── 附录 A 判据三:9 个稳定前缀族的兜底 typed 面(SDK 1.1.0 × server 3.0.0)────────────────────────
36
+ // 服务端契约原文(ASSISTANT-WIRE-CONTRACT 附录 A 判据三):「前缀族(`auth.` / `request.` /
37
+ // `not_found.` / `conflict.` / `limit.` / `capability.` / `feature.` / `internal.` / `state.`)是稳定的
38
+ // **粗分支面** —— 按前缀兜底(未知码 → 看前缀 → 再不行看状态码)永远安全。」
39
+ //
40
+ // 🔴 为什么这不是「把码抄进 SDK」这么轻:补齐之前,`capability.run_store_required`(**换部署形态**才可能有)
41
+ // 与 `feature.elicitation_disabled`(**开个开关**就行)都是 501、都塌成裸 `APIError` —— 消费端拿不到任何
42
+ // 可分支的东西,而这两者给用户的行动建议正好相反。契约套件第一批把这件事做成了可执行断言
43
+ // (`COLLAPSED_BASELINE` 棘轮 = 25 行塌型),本批销账。
44
+ //
45
+ // 两族**复用已有类,不新铸**:`auth.*` → {@link AuthError};`conflict.*` → {@link ConflictError}
46
+ // (它已经携带 `activeTaskId`,正是 `conflict.session_active_run` 要的那个字段)。
47
+ //
48
+ // ⚠️ 前缀兜底是**开集**的:server 明天新增一个 `capability.xxx_required`,今天这份 SDK 就已经把它
49
+ // 铸成 `CapabilityUnavailableError` —— 未知码的降级仍然是有信息的(与 `stop.*` / `decide.*` 同款设计)。
50
+ /** `request.*` 前缀族 —— **请求本身有问题,改请求即可**(400 为主,另有 413 / 422 / 4xx 兜底粗码)。
51
+ * 一律 NOT retryable verbatim:同一个 body 重发永远同一个结果。子类 {@link PayloadTooLargeError} 专指
52
+ * 「某个字段/整体太大」这一支(处置更具体:缩短后重发)。 */
53
+ export class BadRequestError extends APIError {
54
+ }
55
+ /** `not_found.*` 前缀族 —— 资源不存在。⚠️ 也包含**属主门**:server 对非属主一律回 404 同码同文
56
+ * (不给存在性 oracle),所以「404」不等于「东西真不在」,只等于「你看不到它」。 */
57
+ export class NotFoundError extends APIError {
58
+ }
59
+ /** `limit.*` 前缀族 —— 配额 / 限流 / 保留窗(429 限流与成本配额、416 续读点过保留窗)。
60
+ * `retryAfterMs` 来自 `Retry-After` 头(由 transport 喂入)。
61
+ * ⚠️ 429 那两个成员码({@link RateLimitedError} 起)走更具体的子类,见下。 */
62
+ export class LimitExceededError extends APIError {
63
+ retryAfterMs;
64
+ constructor(status, errorCode, message, retryAfterMs) {
65
+ super(status, errorCode, message);
66
+ this.retryAfterMs = retryAfterMs;
67
+ }
68
+ }
69
+ /** `capability.*` 前缀族 —— **本部署没接这个面**(501)。重试无意义、开开关也没用:要么换部署形态
70
+ * (接上 store / 打开 fleet 接线),要么在 UI 上**别提供这个入口**。
71
+ * 🔴 与 {@link FeatureDisabledError} 的区别正是 P0-1 的全部后果:同为 501,处置相反。 */
72
+ export class CapabilityUnavailableError extends APIError {
73
+ }
74
+ /** `feature.*` 前缀族 —— **面存在,但开关是关的**(501;`feature.metrics_disabled` 是 404)。
75
+ * 处置:提示管理员打开对应开关(`RESOURCE_SUSPEND` / 三个 LIVE HITL 开关 / metrics)——不需要换部署形态。 */
76
+ export class FeatureDisabledError extends APIError {
77
+ }
78
+ /** `internal.*` 前缀族 —— 服务端自己出错(500 / 502)。细节不上 wire。
79
+ * 部分成员**明确可重试**(`internal.cancel_not_terminalized` = 重试 cancel;
80
+ * `internal.snapshot_blobs_missing` = 重试或换快照),按 `.errorCode` 分。 */
81
+ export class InternalServerError extends APIError {
82
+ }
83
+ /** `state.*` 前缀族 —— 服务端**当前状态**不接受这个请求(503):roster 还没拉到、没有可用的
84
+ * sandbox-base。**稍后重试**是主处置。
85
+ * 🔴 修的是一条真缝:这一族此前撞进 `status === 503` 那条分支,被铸成了 {@link AuthError} —— 一个
86
+ * 「稍后重试」的信号被读成了「你没有权限」。 */
87
+ export class ServiceStateError extends APIError {
88
+ }
29
89
  /** 413 `workspace_file_too_large`(#3 workspace file 面,[1894]①)——结构化超限信封的 typed 形:
30
90
  * `sizeBytes`=文件真实大小、`limit`=部署帽(capabilities.workspace.maxFileBytes)。web 用它渲
31
91
  * 「文件过大({sizeBytes}),下载查看」卡;archive 端点是退路。由 WorkspaceResource.file 铸
32
- * (面局部判别,不进全局 classify——code 是这个面独有的)。 */
92
+ * (面局部判别,不进全局 classify——errorCode 是这个面独有的)。 */
33
93
  export class WorkspaceFileTooLargeError extends APIError {
34
94
  sizeBytes;
35
95
  limit;
36
- constructor(status, code, message, sizeBytes, limit) {
37
- super(status, code, message);
96
+ constructor(status, errorCode, message, sizeBytes, limit) {
97
+ super(status, errorCode, message);
38
98
  this.sizeBytes = sizeBytes;
39
99
  this.limit = limit;
40
100
  }
@@ -46,40 +106,39 @@ export class WorkspaceFileTooLargeError extends APIError {
46
106
  * dead).**Retryable against the replacement instance**; honor `retryAfterMs` (from `retry-after`). */
47
107
  export class DrainingError extends APIError {
48
108
  retryAfterMs;
49
- constructor(status, code, message, retryAfterMs) {
50
- super(status, code, message);
109
+ constructor(status, errorCode, message, retryAfterMs) {
110
+ super(status, errorCode, message);
51
111
  this.retryAfterMs = retryAfterMs;
52
112
  }
53
113
  }
54
- export class RateLimitedError extends APIError {
55
- retryAfterMs;
56
- constructor(status, code, message, retryAfterMs) {
57
- super(status, code, message);
58
- this.retryAfterMs = retryAfterMs;
59
- }
114
+ /** 429 —— per-principal 限流。`retryAfterMs` 来自 `Retry-After`(SDK 重试时遵守它)。
115
+ * 自 SDK 1.1.0 起继承 {@link LimitExceededError}(`limit.*` 族的成员码 `limit.rate_exceeded` /
116
+ * `limit.cost_quota_exceeded` 就是从这里下来的)——**additive**:既有 `instanceof RateLimitedError`
117
+ * 一字不改,同时 `catch (e instanceof LimitExceededError)` 也能一把接住整族。 */
118
+ export class RateLimitedError extends LimitExceededError {
60
119
  }
61
- /** 429 `code:"quota_exhausted"` — the principal's usage quota (token windows / budget) is exhausted
120
+ /** 429 `errorCode:"quota_exhausted"` — the principal's usage quota (token windows / budget) is exhausted
62
121
  * (E4 double-rail, service ≥1.150.0). Extends RateLimitedError so existing 429 handling still matches;
63
122
  * carries the structured deny detail so the RENDER layer can produce a human message (人话归壳 — the wire
64
123
  * stays structured, no display strings here). NOT usefully retryable inside the deny window. */
65
124
  export class QuotaExhaustedError extends RateLimitedError {
66
125
  detail;
67
- constructor(status, code, message, retryAfterMs, detail) {
68
- super(status, code, message, retryAfterMs);
126
+ constructor(status, errorCode, message, retryAfterMs, detail) {
127
+ super(status, errorCode, message, retryAfterMs);
69
128
  this.detail = detail;
70
129
  }
71
130
  }
72
- /** 400 `code:"scenario_not_allowed"` — the request's explicit `body.scenario` is outside the principal's
131
+ /** 400 `errorCode:"scenario_not_allowed"` — the request's explicit `body.scenario` is outside the principal's
73
132
  * assigned allowlist (scenario governance, service ≥1.152.0). Carries the service's
74
133
  * `allowlist` VERBATIM (pure scenario names) so the RENDER layer can offer the valid choices (人话归壳 —
75
134
  * the wire stays structured, no display strings here). NOT retryable verbatim: pick a scenario from
76
135
  * `allowlist`, or omit `scenario` entirely to run on the assigned default. */
77
136
  export class ScenarioNotAllowedError extends APIError {
78
137
  allowlist;
79
- constructor(status, code, message,
138
+ constructor(status, errorCode, message,
80
139
  /** The principal's allowed scenario names (may be empty on an older service / defensive drop). */
81
140
  allowlist) {
82
- super(status, code, message);
141
+ super(status, errorCode, message);
83
142
  this.allowlist = allowlist;
84
143
  }
85
144
  }
@@ -89,8 +148,8 @@ export class BudgetExceededError extends APIError {
89
148
  /** 409 — session already has an active run, or an idempotency conflict / session CAS conflict. */
90
149
  export class ConflictError extends APIError {
91
150
  activeTaskId;
92
- constructor(status, code, message, activeTaskId) {
93
- super(status, code, message);
151
+ constructor(status, errorCode, message, activeTaskId) {
152
+ super(status, errorCode, message);
94
153
  this.activeTaskId = activeTaskId;
95
154
  }
96
155
  }
@@ -98,15 +157,15 @@ export class ConflictError extends APIError {
98
157
  * lose destination history: the relation classified to `fork` or `stale` and the caller did NOT pass
99
158
  * `{ resolution: "overwrite-dst" }`. 409, errorCode `conflict`, with the classified `relation` carrying the
100
159
  * exclusive entry sets (mirrors the service SyncConflictError). DISTINCT from the session-CAS ConflictError
101
- * (same status+code, but THIS one carries a `relation` — that's how the mapper tells them apart). Client action:
160
+ * (same status+errorCode, but THIS one carries a `relation` — that's how the mapper tells them apart). Client action:
102
161
  * surface the keep-local / keep-cloud divergence to the user; to override, re-push with `resolution:"overwrite-dst"`.
103
162
  * NEVER auto-overwrite (a silent clobber is exactly the data loss the classifier exists to prevent). */
104
163
  export class SyncConflictError extends ConflictError {
105
164
  relation;
106
- constructor(status, code, message,
165
+ constructor(status, errorCode, message,
107
166
  /** The §7 `fork`/`stale` relation with the exclusive entry sets (untyped at the JSON boundary). */
108
167
  relation) {
109
- super(status, code, message);
168
+ super(status, errorCode, message);
110
169
  this.relation = relation;
111
170
  }
112
171
  }
@@ -115,8 +174,8 @@ export class SyncConflictError extends ConflictError {
115
174
  * and from session-CAS 409. Client action: this pending is gone → REFETCH the inbox; do not retry the decide. */
116
175
  export class ApprovalStaleError extends APIError {
117
176
  terminal;
118
- constructor(status, code, message, terminal) {
119
- super(status, code, message);
177
+ constructor(status, errorCode, message, terminal) {
178
+ super(status, errorCode, message);
120
179
  this.terminal = terminal;
121
180
  }
122
181
  }
@@ -125,8 +184,8 @@ export class ApprovalStaleError extends APIError {
125
184
  * Client action: REFETCH the PendingCheckpoint, RE-PRESENT it to the human, NEVER auto-retry. */
126
185
  export class ApprovalBindingMismatchError extends APIError {
127
186
  field;
128
- constructor(status, code, message, field) {
129
- super(status, code, message);
187
+ constructor(status, errorCode, message, field) {
188
+ super(status, errorCode, message);
130
189
  this.field = field;
131
190
  }
132
191
  }
@@ -136,18 +195,43 @@ export class ApprovalBindingMismatchError extends APIError {
136
195
  * again (retryable). */
137
196
  export class ApprovalConcurrentlyReopenedError extends APIError {
138
197
  }
198
+ /** `steering.*` 族的基类(SDK 1.1.0 —— 从**逐码枚举**改成**前缀分派**,与 `stop.*` / `decide.*` 同形)。
199
+ *
200
+ * 🔴 改之前是这样的:server 真能发 5 个 `steering.*` 码,SDK 只 `===` 认了 2 个,另 3 个
201
+ * (`ambiguous_label` / `ambiguous_target` / `still_running`)塌成通用 409 `ConflictError` —— 一个**开集**
202
+ * 被当成闭集写。契约套件第一批用「钉版 server 产物扫码 → 逐码喂 mapper」把这 3 条塌型做成了
203
+ * `STEERING_COLLAPSED_BASELINE = 3` 的棘轮读数,本批销账(基线降到 0)。
204
+ *
205
+ * ⚠️ 前缀开集的代价必须说清楚:那 3 个码此前偶然是 `ConflictError`(因为它们是 409),现在**不再是**了。
206
+ * 这是有意的 —— 「409」是传输层的巧合,「steer 目标有歧义」才是消费端要分支的东西;按 [clean-cut] 硬切,
207
+ * 不留双继承之类的兼容层。整族一把接:`catch (e instanceof SteeringError)`。 */
208
+ export class SteeringError extends APIError {
209
+ }
139
210
  /** design/80 D-A — you steered a run that is NOT running (it is durable-`suspended`, terminal, or unknown).
140
211
  * Steer is at-most-once and NOT queued. Client action: surface "task isn't running"; if suspended, the path
141
212
  * forward is the approval decide, not a steer. */
142
- export class SteeringNotRunningError extends APIError {
213
+ export class SteeringNotRunningError extends SteeringError {
143
214
  }
144
215
  /** design/80 D-A — the steer text carried a control-plane escape (e.g. `</system-reminder>`) and core rejected
145
216
  * it (runtask.ts:489). Client action: it's an injection attempt or a malformed steer — surface it; don't retry
146
217
  * verbatim. (The BFF also fences steer text defensively; this is core's authoritative reject.) */
147
- export class SteeringInvalidContentError extends APIError {
218
+ export class SteeringInvalidContentError extends SteeringError {
219
+ }
220
+ /** `steering.ambiguous_target`(子代 steer/resume 面,server `http/routes/runs.ts`)/
221
+ * `steering.ambiguous_label`(workflow agent steer 面,`http/routes/workflows.ts`)—— **这一个 run 里有
222
+ * 不止一个活体匹配你给的 target/label**,server 拒绝替你猜。409。
223
+ * Client action:两个码同一个处置 —— **换一个唯一的定位方式**(子代面用 `parentToolCallId`;workflow 面
224
+ * 用更具体的 label/句柄),原样重试永远同一个结果。`.errorCode` 区分是哪条轴上歧义。 */
225
+ export class SteeringAmbiguousTargetError extends SteeringError {
226
+ }
227
+ /** `steering.still_running` —— 你对一个**还在飞**的子代调了 `resume`(revive 只在 settle 之后合法;
228
+ * 也可能是上一次 resume 还没落地)。409。
229
+ * Client action:要么改调 steer(给在跑的它递话),要么等它 settle 再 resume。**与
230
+ * {@link SteeringNotRunningError} 正好互为反面**,两个码塌在一起会让 UI 说反话。 */
231
+ export class SteeringStillRunningError extends SteeringError {
148
232
  }
149
233
  /** `413` + `steer.content_too_large` / `reason_too_large` —— 请求里某个**自由文本**字段超过 server 的字符上限。
150
- * 两个成员码同族同处置,所以共用一个类型(`err.code` 保留具体是哪一个):
234
+ * 两个成员码同族同处置,所以共用一个类型(`err.errorCode` 保留具体是哪一个):
151
235
  * · `steer.content_too_large`(server 1.279.2):steer 入参上限 256 KiB。脱敏门是同步的,一次超大 steer 卡住的
152
236
  * 是整个副本,所以服务端**拒**而不是先截 —— 先截会让引擎的 `[+N chars]` 披露低报。
153
237
  * · `reason_too_large`(server 1.284.0):审批决定的 `reason` 上限 4096 字符,`/decide` 与 plan_review 两道门同值。
@@ -160,7 +244,7 @@ export class PayloadTooLargeError extends APIError {
160
244
  * `stop.not_local`(run 在别的副本,无本地进程可 kill)/ `stop.not_landed`(kill 尝试了未落地)/
161
245
  * `stop.parked`(行在 parked,无活跃进程)/ `stop.park_resume_won`(stop 与「审批落地→恢复」竞态,
162
246
  * 恢复赢了)/ `stop.park_arbiter_unreachable`(仲裁店不可达,真相未知——core 1.397 三分)。
163
- * 开集:未来新 `stop.*` 码同样落到本类(`code` 原样携带),消费方 switch 要留 default。 */
247
+ * 开集:未来新 `stop.*` 码同样落到本类(`errorCode` 原样携带),消费方 switch 要留 default。 */
164
248
  export class TaskStopConflictError extends ConflictError {
165
249
  }
166
250
  /** design/80 D-G (direct-connect door, service [84]) — the signed principal JWT (`X-Approval-Principal-Token`)
@@ -205,8 +289,8 @@ export class ResumeOutcomeInvalidError extends APIError {
205
289
  * longer admissible. Not a race: retrying as-is stays blocked until the policy or the request changes. */
206
290
  export class ResumeBlockedByPolicyError extends APIError {
207
291
  blockedBy;
208
- constructor(status, code, message, blockedBy) {
209
- super(status, code, message);
292
+ constructor(status, errorCode, message, blockedBy) {
293
+ super(status, errorCode, message);
210
294
  this.blockedBy = blockedBy;
211
295
  }
212
296
  }
@@ -220,7 +304,7 @@ export class ResumeBlockedByPolicyError extends APIError {
220
304
  // "re-check the pending list"). These types are what makes it branchable without string-matching a message.
221
305
  /** 409 `resume.*`(dot 前缀;design/122 D2)—— subagent revive 的合同拒绝族:still_running(等 settle)/
222
306
  * retain_off(父 run 没开 retainSubagentSessions)/evicted(留存被逐)/cap(留存上限)/
223
- * session_not_found。前缀开集:core 未来新码至少落到本类(`.code` 判别),不塌匿名 409。
307
+ * session_not_found。前缀开集:core 未来新码至少落到本类(`.errorCode` 判别),不塌匿名 409。
224
308
  * ⚠️ 与下划线形的 resume_outcome_invalid / resume_blocked_by_policy 是**两族**(那两个在上面,先判)。 */
225
309
  export class SubagentResumeConflictError extends ConflictError {
226
310
  }
@@ -229,7 +313,7 @@ export class SubagentResumeConflictError extends ConflictError {
229
313
  export class WakeGatePendingError extends ConflictError {
230
314
  }
231
315
  /** 409 `compact.not_running` / `detach.not_running` —— replica-local live-verb 族:run 已终态,或活在
232
- * 另一副本(手动 compact/detach 只对本副本的活流有效)。`.code` 判别是哪个 verb。 */
316
+ * 另一副本(手动 compact/detach 只对本副本的活流有效)。`.errorCode` 判别是哪个 verb。 */
233
317
  export class RunNotLiveError extends ConflictError {
234
318
  }
235
319
  /** Base of the `decide.*` family — catch this to handle the whole decide door in one place.
@@ -239,10 +323,10 @@ export class RunNotLiveError extends ConflictError {
239
323
  export class DecideError extends APIError {
240
324
  taskId;
241
325
  rollback;
242
- constructor(status, code, message, taskId,
326
+ constructor(status, errorCode, message, taskId,
243
327
  /** revive legs only — the claim-rollback disposition the server reported. */
244
328
  rollback) {
245
- super(status, code, message);
329
+ super(status, errorCode, message);
246
330
  this.taskId = taskId;
247
331
  this.rollback = rollback;
248
332
  }
@@ -280,110 +364,124 @@ function classifyApiError(status, body, retryAfterMs) {
280
364
  // the typed code) instead of crashing scrub() INSIDE the error path (a throw here would mask the real HTTP
281
365
  // failure with a TypeError).
282
366
  // ⚠️ 纠正(2026-07-25):这段注释原先说「scenario-detail 的 404 body 就是 `{error:{code:…}}`」——已不成立,
283
- // server [939] 把那一路统一成了「字符串 `error` + 顶层 `errorCode`」(亲读 `http/server.ts:1517-1527` 确认)。
367
+ // server [939] 把那一路统一成了「字符串 `error` + 顶层 `errorCode`」(亲读 `src/http/routes/capabilities.ts` 确认)。
284
368
  // 本分支**保留**,但定位从「适配某条已知路由」改为「防御任何对象形 error body」:它不该是任何路由的期望形,
285
369
  // 而是错误路径上不许崩的兜底。真形走下面 `b.errorCode` 那条(优先级更高)。
286
370
  const errField = body?.error;
287
371
  const nestedCode = typeof errField === "object" && errField !== null && typeof errField.code === "string"
288
372
  ? errField.code
289
373
  : undefined;
290
- // 🔴 `?? b.code` 是 2026-07-25 补的兜底,补的是一个真缺陷:server 在 `/decide` 上把 CheckpointError 家族的码
291
- // 发在 **`code`** 字段(`http/server.ts` 的 `body: { error: e.message, code: e.code, … }`),而这里此前只看
292
- // `errorCode` ⇒ 那一族(`checkpoint.*`/`resume.*`/`wake.*`)的 `APIError.code` **恒为 undefined**,并且下面
293
- // 两条既有映射(checkpoint.already_resolved / checkpoint.reopened_concurrently)在那条路上**不可达**。
294
- // 优先级刻意是 errorCode > nested > code:server 在 `checkpoint.invalid_outcome` 上正是「`code` 留原码、
295
- // `errorCode` 另铸一个 gate-appropriate 码」,重铸必须压过原码,否则那次刻意区分就被这条兜底抹平了。
296
- // 机器枚举过所有用裸 `code:` 发的 HTTP 错误体(memory_sync_* / resume_blocked_by_policy / quota_exhausted /
297
- // CheckpointError 族),无一与本兜底冲突;`event: error` 流帧那套(WORKER_DOWN 等)不经本函数。
298
- const code = b.errorCode ?? nestedCode ?? b.code;
374
+ // 🔴 单键(SDK 1.0.0 / server 3.0.0 [1989][1990]):wire 上**唯一**的机器码键位是 `errorCode`。
375
+ // 历史留档:2026-07-25 这里曾有一条 `?? b.code` 兜底,补的是 server 在 `/decide` 上把 CheckpointError
376
+ // 家族的码发在裸 `code` 字段这个真缺陷(后果:`checkpoint.*`/`resume.*`/`wake.*` 一族的机器码恒为
377
+ // undefined,两条既有 typed 映射在那条路上不可达)。server 3.0.0 已在**发端**根治(`http/send.ts` 的
378
+ // `sendError(res,status,errorCode,message,extra)` + `/decide` 归一臂),兜底随之删除 —— 留着它只会让
379
+ // 「对端还在发老键」这件事被静默吸收、下游测不出来。`event: error` 流帧那套不经本函数(见 sse.ts)。
380
+ const errorCode = b.errorCode ?? nestedCode;
299
381
  const rawMsg = b.errorMessage ?? (typeof errField === "string" ? errField : undefined);
300
382
  const msg = scrub(rawMsg ?? nestedCode ?? `HTTP ${status}`);
301
- // scenario governance — the service 400 keys the typed code on `code` (same posture as the E4 quota
302
- // 429; the worker envelope's `errorCode` stays untouched for every other route). The allowlist passes
303
- // through with a per-entry type guard (a malformed entry is dropped, never crashes the error path); a bare
304
- // code with no/bad allowlist still maps to ScenarioNotAllowedError with an empty list (generic-fallback).
305
- // (`code` 已由上面的通用兜底吸收了 `b.code`,所以这里不再需要 `code ?? b.code` 的特判形 —— 2026-07-25 收掉,
306
- // 免得读者以为有两套取码机制在并行。)
307
- if (code === "scenario_not_allowed") {
383
+ // scenario governance — server 侧 `HttpError(400, msg, { code: "scenario_not_allowed", extra:{allowlist} })`
384
+ // 经 `sendError` 落到 wire 的 `errorCode` 单键。The allowlist passes through with a per-entry type guard
385
+ // (a malformed entry is dropped, never crashes the error path); a bare code with no/bad allowlist still maps
386
+ // to ScenarioNotAllowedError with an empty list (generic-fallback contract).
387
+ if (errorCode === "scenario_not_allowed") {
308
388
  const allowlist = Array.isArray(b.allowlist) ? b.allowlist.filter((x) => typeof x === "string" && x.length > 0) : [];
309
389
  return new ScenarioNotAllowedError(status, "scenario_not_allowed", msg, allowlist);
310
390
  }
311
- if (code === "budget.precall" || code === "budget.exceeded")
312
- return new BudgetExceededError(status, code, msg);
391
+ if (errorCode === "budget.precall" || errorCode === "budget.exceeded")
392
+ return new BudgetExceededError(status, errorCode, msg);
313
393
  // server ≥1.309:drain 503 有机器码;旧 server 只发 error:"draining" 字面(冻结契约)——两形同映射,
314
394
  // 消费端 instanceof DrainingError 即可,不再抄文案。retryAfterMs 由调用方从 retry-after 头喂入(见 transport)。
315
- if (code === "draining" || (status === 503 && errField === "draining"))
395
+ if (errorCode === "draining" || (status === 503 && errField === "draining"))
316
396
  return new DrainingError(status, "draining", msg, retryAfterMs);
317
397
  // design/80 D-1 — keyed on errorCode, NOT status (both are independent of the session-CAS 409 ConflictError).
318
398
  // [1833] G13:taskStop 409 五形(stop.* 前缀开集)→ typed(壳此前手抄 code 自判,engineTaskHandleWire.ts:186-197)
319
- if (typeof code === "string" && code.startsWith("stop."))
320
- return new TaskStopConflictError(status, code, msg);
321
- if (code === "approval_stale" || code === "checkpoint.already_resolved")
322
- return new ApprovalStaleError(status, code, msg, b.terminal);
323
- if (code === "checkpoint.reopened_concurrently")
324
- return new ApprovalConcurrentlyReopenedError(status, code, msg); // D-1 transient (core 1.101.0)
325
- if (code === "approval_binding_mismatch" || code === "checkpoint.invalid_outcome")
326
- return new ApprovalBindingMismatchError(status, code, msg, b.field);
399
+ if (typeof errorCode === "string" && errorCode.startsWith("stop."))
400
+ return new TaskStopConflictError(status, errorCode, msg);
401
+ if (errorCode === "approval_stale" || errorCode === "checkpoint.already_resolved")
402
+ return new ApprovalStaleError(status, errorCode, msg, b.terminal);
403
+ if (errorCode === "checkpoint.reopened_concurrently")
404
+ return new ApprovalConcurrentlyReopenedError(status, errorCode, msg); // D-1 transient (core 1.101.0)
405
+ if (errorCode === "approval_binding_mismatch" || errorCode === "checkpoint.invalid_outcome")
406
+ return new ApprovalBindingMismatchError(status, errorCode, msg, b.field);
327
407
  // ASSISTANT-WIRE-CONTRACT §4b/§4c — 错门守卫, 键于 errorCode(都是 409 但语义 ≠ session-CAS ConflictError, 不能塌进通用 409)。
328
- if (code === "gate_not_resumable")
329
- return new GateNotResumableError(status, code, msg); // §4b resume 撞 human/policy_ask
330
- if (code === "gate_not_plan_review")
331
- return new GateNotPlanReviewError(status, code, msg); // §4c plan_review 撞非 plan_review
332
- if (code === "steering.not_running")
333
- return new SteeringNotRunningError(status, code, msg); // design/80 D-A
334
- if (code === "steering.invalid_content")
335
- return new SteeringInvalidContentError(status, code, msg); // design/80 D-A (422)
408
+ if (errorCode === "gate_not_resumable")
409
+ return new GateNotResumableError(status, errorCode, msg); // §4b resume 撞 human/policy_ask
410
+ if (errorCode === "gate_not_plan_review")
411
+ return new GateNotPlanReviewError(status, errorCode, msg); // §4c plan_review 撞非 plan_review
412
+ // `steering.*` —— 前缀分派(SDK 1.1.0,与 `stop.*` / `decide.*` 同形;见 SteeringError 顶注)。
413
+ // 未来 server 新铸的 steering 码至少落到 SteeringError 基类,不再塌成匿名 409。
414
+ if (errorCode?.startsWith("steering.")) {
415
+ if (errorCode === "steering.not_running")
416
+ return new SteeringNotRunningError(status, errorCode, msg); // design/80 D-A
417
+ if (errorCode === "steering.invalid_content")
418
+ return new SteeringInvalidContentError(status, errorCode, msg); // design/80 D-A (422)
419
+ // 两个「歧义」码同一个处置(换唯一定位方式)⇒ 共用一个类,`.errorCode` 保留是哪条轴。
420
+ if (errorCode === "steering.ambiguous_target" || errorCode === "steering.ambiguous_label") {
421
+ return new SteeringAmbiguousTargetError(status, errorCode, msg);
422
+ }
423
+ if (errorCode === "steering.still_running")
424
+ return new SteeringStillRunningError(status, errorCode, msg);
425
+ return new SteeringError(status, errorCode, msg);
426
+ }
336
427
  // 自由文本超上限族(413):两个成员码同处置 —— 缩短后重发,重试原文无意义。见 PayloadTooLargeError 顶注。
337
- if (code === "steer.content_too_large" || code === "reason_too_large")
338
- return new PayloadTooLargeError(status, code, msg);
428
+ if (errorCode === "steer.content_too_large" || errorCode === "reason_too_large")
429
+ return new PayloadTooLargeError(status, errorCode, msg);
430
+ // 413 `workspace_file_too_large` —— **铸造点从 WorkspaceResource.file 挪到这里**(S2 收编,SDK 1.1.0)。
431
+ // 原先的理由是「errorCode 是这个面独有的,面局部判别就够了」;实况证否了它:面局部的手写分支恰恰是
432
+ // 唯一没有任何 mapper 测试保护的地方,S2 就死在那里(读裸 `code`,typed 类真机零触发)。
433
+ // 两个结构化字段都在才铸(缺一个 ⇒ web 的卡拿到 undefined,不如老实降级成通用 413)。
434
+ if (errorCode === "workspace_file_too_large" && typeof b.sizeBytes === "number" && typeof b.limit === "number") {
435
+ return new WorkspaceFileTooLargeError(status, errorCode, msg, b.sizeBytes, b.limit);
436
+ }
339
437
  // design/80 D-G (direct-connect door, service [84]) — service-locked wire codes; forward-draft until M2 BFF signer.
340
- if (code === "principal_unverified")
341
- return new PrincipalUnverifiedError(status, code, msg);
342
- if (code === "principal_unbound")
343
- return new PrincipalUnboundError(status, code, msg); // cnf.bnd ≠ SHA256(token) = token-lift
344
- if (code === "approval_mac_required")
345
- return new ApprovalMacRequiredError(status, code, msg);
346
- if (code === "approval_mac_invalid")
347
- return new ApprovalMacInvalidError(status, code, msg);
438
+ if (errorCode === "principal_unverified")
439
+ return new PrincipalUnverifiedError(status, errorCode, msg);
440
+ if (errorCode === "principal_unbound")
441
+ return new PrincipalUnboundError(status, errorCode, msg); // cnf.bnd ≠ SHA256(token) = token-lift
442
+ if (errorCode === "approval_mac_required")
443
+ return new ApprovalMacRequiredError(status, errorCode, msg);
444
+ if (errorCode === "approval_mac_invalid")
445
+ return new ApprovalMacInvalidError(status, errorCode, msg);
348
446
  // resume 侧两个 server 刻意铸出来的码(见各自类注释)。
349
- if (code === "resume_outcome_invalid")
350
- return new ResumeOutcomeInvalidError(status, code, msg);
351
- if (code === "resume_blocked_by_policy") {
352
- return new ResumeBlockedByPolicyError(status, code, msg, typeof b.blockedBy === "string" ? b.blockedBy : undefined);
447
+ if (errorCode === "resume_outcome_invalid")
448
+ return new ResumeOutcomeInvalidError(status, errorCode, msg);
449
+ if (errorCode === "resume_blocked_by_policy") {
450
+ return new ResumeBlockedByPolicyError(status, errorCode, msg, typeof b.blockedBy === "string" ? b.blockedBy : undefined);
353
451
  }
354
452
  // 2026-07-28 补齐批:三小族(此前裸串)。resume.* 是 dot 前缀开集——必须排在上面两个下划线码之后。
355
- if (code?.startsWith("resume."))
356
- return new SubagentResumeConflictError(status, code, msg);
357
- if (code === "wake.gate_pending")
358
- return new WakeGatePendingError(status, code, msg);
359
- if (code === "compact.not_running" || code === "detach.not_running")
360
- return new RunNotLiveError(status, code, msg);
453
+ if (errorCode?.startsWith("resume."))
454
+ return new SubagentResumeConflictError(status, errorCode, msg);
455
+ if (errorCode === "wake.gate_pending")
456
+ return new WakeGatePendingError(status, errorCode, msg);
457
+ if (errorCode === "compact.not_running" || errorCode === "detach.not_running")
458
+ return new RunNotLiveError(status, errorCode, msg);
361
459
  // decide.* 家族 —— 前缀分派,按「调用方该做什么」分组(见 DecideError 上方那段注释)。
362
460
  // 前缀式而非逐码穷举:server 新增一个 decide.* 码时,它至少会落到 DecideError 基类(带 taskId,可被
363
461
  // `catch (e instanceof DecideError)` 接住),而不是塌成一个匿名 409 —— 未知码的降级仍然是有信息的。
364
- if (code?.startsWith("decide.")) {
462
+ if (errorCode?.startsWith("decide.")) {
365
463
  const taskId = typeof b.taskId === "string" ? b.taskId : undefined;
366
464
  const rollback = typeof b.rollback === "string" ? b.rollback : undefined;
367
465
  // 重查 pending 列表后再决定
368
- if (code === "decide.not_parked" || code === "decide.gate_moved" || code === "decide.claim_lost") {
369
- return new DecideParkMovedError(status, code, msg, taskId, rollback);
466
+ if (errorCode === "decide.not_parked" || errorCode === "decide.gate_moved" || errorCode === "decide.claim_lost") {
467
+ return new DecideParkMovedError(status, errorCode, msg, taskId, rollback);
370
468
  }
371
469
  // 换 decide 形(原样重试无意义)
372
- if (code === "decide.parked_answer_unsupported" || code === "decide.parked_question_unsupported" || code === "decide.parked_remember_unsupported") {
373
- return new DecideUnsupportedError(status, code, msg, taskId, rollback);
470
+ if (errorCode === "decide.parked_answer_unsupported" || errorCode === "decide.parked_question_unsupported" || errorCode === "decide.parked_remember_unsupported") {
471
+ return new DecideUnsupportedError(status, errorCode, msg, taskId, rollback);
374
472
  }
375
473
  // 原样重试
376
- if (code === "decide.store_unreachable")
377
- return new DecideStoreUnreachableError(status, code, msg, taskId, rollback);
474
+ if (errorCode === "decide.store_unreachable")
475
+ return new DecideStoreUnreachableError(status, errorCode, msg, taskId, rollback);
378
476
  // 其余(row_unrevivable / revive_failed / revive_rejected)= 交给人看
379
- return new DecideError(status, code, msg, taskId, rollback);
477
+ return new DecideError(status, errorCode, msg, taskId, rollback);
380
478
  }
381
479
  if (status === 429) {
382
- // E4 quota deny — the SERVICE 429 body keys the typed code on `code` (NOT `errorCode`; the
383
- // worker envelope's errorCode stays untouched for every other route). Structured fields pass through
384
- // VERBATIM with type guards (a malformed field is dropped, never crashes the error path); a bare code
385
- // with no fields still maps to QuotaExhaustedError with an empty detail (generic-fallback contract).
386
- if (code === "quota_exhausted") { // 同上:通用兜底已覆盖 `b.code` 这个键位
480
+ // E4 quota deny — the SERVICE 429 body keys the typed code on `errorCode` (`sendError(res, 429,
481
+ // "quota_exhausted", …)`, server 3.0 单键). Structured fields pass through VERBATIM with type guards
482
+ // (a malformed field is dropped, never crashes the error path); a bare code with no fields still maps
483
+ // to QuotaExhaustedError with an empty detail (generic-fallback contract).
484
+ if (errorCode === "quota_exhausted") {
387
485
  const detail = {
388
486
  ...(b.windowType === "fiveHour" || b.windowType === "weekly" || b.windowType === "budget" ? { windowType: b.windowType } : {}),
389
487
  ...(typeof b.remaining === "number" && Number.isFinite(b.remaining) ? { remaining: b.remaining } : {}),
@@ -392,20 +490,54 @@ function classifyApiError(status, body, retryAfterMs) {
392
490
  ...(typeof b.pool === "string" ? { pool: b.pool } : {}),
393
491
  ...(typeof b.retryAfterSec === "number" && Number.isFinite(b.retryAfterSec) ? { retryAfterSec: b.retryAfterSec } : {}),
394
492
  };
395
- return new QuotaExhaustedError(status, code, msg, retryAfterMs, detail);
493
+ return new QuotaExhaustedError(status, errorCode, msg, retryAfterMs, detail);
494
+ }
495
+ // `limit.rate_exceeded` / `limit.cost_quota_exceeded` 也从这里出去 —— RateLimitedError 是
496
+ // LimitExceededError 的子类,所以走这条更具体的腿**不会**让 `limit.*` 族的兜底落空,反而多带了
497
+ // `retryAfterMs`。⚠️ 正因如此,下面的 `limit.` 前缀分派必须排在 429 之后(排前面会把这条腿吃掉)。
498
+ return new RateLimitedError(status, errorCode, msg, retryAfterMs);
499
+ }
500
+ // ── 附录 A 判据三:9 个稳定前缀族的兜底 ────────────────────────────────────────────────────────
501
+ // 位置:所有**具名码**的精确分派之后(具体优先于粗分支)、状态码兜底之前(键位优先于状态)。
502
+ // 🔴 中间那句不是可有可无的排序偏好:`state.*` 全是 503,若让 `status === 503` 先手,它们会被铸成
503
+ // AuthError —— 「稍后重试」读成「你没有权限」。所以前缀族**必须**排在状态兜底之前。
504
+ if (errorCode !== undefined) {
505
+ if (errorCode.startsWith("auth."))
506
+ return new AuthError(status, errorCode, msg); // 401/403/503 三种状态同一族
507
+ if (errorCode.startsWith("request.")) {
508
+ // 族内唯一分岔:「太大了」的处置更具体(缩短后重发),与 steer/reason 两个扁平码同类。
509
+ if (errorCode === "request.payload_too_large")
510
+ return new PayloadTooLargeError(status, errorCode, msg);
511
+ return new BadRequestError(status, errorCode, msg);
396
512
  }
397
- return new RateLimitedError(status, code, msg, retryAfterMs);
513
+ if (errorCode.startsWith("not_found."))
514
+ return new NotFoundError(status, errorCode, msg);
515
+ // `conflict.*` 复用 ConflictError —— 它本来就携带 `activeTaskId`,正是 `conflict.session_active_run`
516
+ // 要消费的那个字段。⚠️ 裸码 `conflict`(session-sync 的扁平码)**不**以 `conflict.` 开头,不受影响,
517
+ // 仍走下面 SyncConflictError / 409 那两条腿。
518
+ if (errorCode.startsWith("conflict."))
519
+ return new ConflictError(status, errorCode, msg, b.activeTaskId);
520
+ if (errorCode.startsWith("limit."))
521
+ return new LimitExceededError(status, errorCode, msg, retryAfterMs);
522
+ if (errorCode.startsWith("capability."))
523
+ return new CapabilityUnavailableError(status, errorCode, msg);
524
+ if (errorCode.startsWith("feature."))
525
+ return new FeatureDisabledError(status, errorCode, msg);
526
+ if (errorCode.startsWith("internal."))
527
+ return new InternalServerError(status, errorCode, msg);
528
+ if (errorCode.startsWith("state."))
529
+ return new ServiceStateError(status, errorCode, msg);
398
530
  }
399
531
  if (status === 401 || status === 503)
400
- return new AuthError(status, code, msg);
532
+ return new AuthError(status, errorCode, msg);
401
533
  // 2c session-sync: a 409 `conflict` that CARRIES a fork/stale `relation` is the SyncConflictError (distinct from
402
534
  // the session-CAS ConflictError, which has no `relation`). Keyed on the relation's presence + tag, not status alone.
403
- if (status === 409 && code === "conflict" && (b.relation?.relation === "fork" || b.relation?.relation === "stale")) {
404
- return new SyncConflictError(status, code, msg, b.relation);
535
+ if (status === 409 && errorCode === "conflict" && (b.relation?.relation === "fork" || b.relation?.relation === "stale")) {
536
+ return new SyncConflictError(status, errorCode, msg, b.relation);
405
537
  }
406
538
  if (status === 409)
407
- return new ConflictError(status, code, msg, b.activeTaskId);
408
- return new APIError(status, code, msg);
539
+ return new ConflictError(status, errorCode, msg, b.activeTaskId);
540
+ return new APIError(status, errorCode, msg);
409
541
  }
410
542
  // ── control plane (sema-registry /api/config/*) typed errors ────────────────────────────────────────────
411
543
  // The control plane is a DIFFERENT backend from the worker /v1 plane and uses a DIFFERENT error envelope: