Commit b5006bc
feat(sdk): add discriminated errors, type guards, toJSON, withRetry
The April 2026 SDK DX consultation called out `instanceof Run402Error`
as fragile across SDK copies and realms — duplicate npm installs,
bundler chunk splits, ESM/CJS interop, and V8-isolate boundaries all
silently break identity-based checks. Coding agents run in environments
they don't control, so a failed `instanceof` shows up as "my error
handler doesn't catch the right errors" with no diagnostic.
This change adds value-based discriminators alongside the existing
class hierarchy. All additions are non-breaking — `instanceof` keeps
working for single-copy single-realm callers.
New surface (exported from @run402/sdk and @run402/sdk/node):
- Run402ErrorKind: union of "payment_required" | "project_not_found"
| "unauthorized" | "api_error" | "network_error" | "local_error"
| "deploy_error".
- Run402Error.kind: stable string discriminator on every subclass.
- Run402Error.isRun402Error: structural brand for cross-copy detection.
- Run402Error.toJSON(): canonical envelope (name/kind/message/status/
code/category/retryable/safeToRetry/mutationState/traceId/context/
details/nextActions/body). Run402DeployError extends with phase/
resource/operationId/planId/fix/logs/rolledBack. JSON.stringify on
any subclass now produces a populated object instead of "{}".
- Type guards: isRun402Error, isPaymentRequired, isProjectNotFound,
isUnauthorized, isApiError, isNetworkError, isLocalError,
isDeployError, isRetryableRun402Error.
- withRetry(fn, opts?): exponential-backoff helper using
isRetryableRun402Error as the default policy. Defaults: 3 attempts,
250ms base, 5s cap. Throws the LAST error after exhausting attempts
so the caller's catch handler sees the original structured envelope.
Pair with idempotencyKey baked into the closure for safe mutation
retries.
Fixed two anonymous Run402Error subclasses in
sdk/src/namespaces/projects.ts (which TypeScript correctly rejected
once `kind` became abstract) — replaced with LocalError, which is
the right class for local-config-missing errors.
Docs: rewrote the "Errors" sections in sdk/README.md and
sdk/llms-sdk.txt to lead with the type-guard pattern, document the
Run402ErrorKind union, and show the canonical `withRetry` recipe paired
with idempotencyKey. The CI gate (sdk-docs-fidelity) verifies every
TS-fenced example compiles against the new types.
Tests: 45 new unit tests in sdk/src/errors.test.ts (kind discriminators,
brand, each guard's narrowing, isRetryableRun402Error policy across
status codes / flags / kinds, toJSON envelope shape including subclass
fields, instanceof back-compat) and sdk/src/retry.test.ts (happy path,
retry on retryable failures, non-retryable short-circuit, last-error
preservation, custom retryIf, onRetry callback ordering and buggy-logger
isolation, exponential backoff timing capped by maxDelayMs, closure
idempotency-key passthrough).
Side fix (npm test glob): the root package.json's test script had
unquoted globs (`sdk/src/**/*.test.ts`), which the shell expanded
without globstar — silently dropping every depth-1 test in
core/src, sdk/src, and src. Local `npm test` reported 266; CI's
inline quoted-glob command saw 566. Quoted the globs in
package.json so local matches CI. Net effect: 300 previously-hidden
tests now run on every local `npm test`.
Spec at run402-private/openspec/changes/sdk-error-discrimination/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 5caa09c commit b5006bc
10 files changed
Lines changed: 1016 additions & 46 deletions
File tree
- sdk
- src
- namespaces
- node
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
146 | | - | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
147 | 166 | | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
157 | 174 | | |
158 | | - | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
159 | 202 | | |
160 | 203 | | |
161 | 204 | | |
162 | 205 | | |
163 | | - | |
164 | | - | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
165 | 209 | | |
166 | 210 | | |
167 | 211 | | |
168 | 212 | | |
169 | 213 | | |
170 | 214 | | |
171 | 215 | | |
172 | | - | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
173 | 225 | | |
174 | | - | |
175 | | - | |
176 | | - | |
177 | | - | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
178 | 231 | | |
179 | 232 | | |
180 | 233 | | |
181 | 234 | | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
182 | 241 | | |
183 | 242 | | |
184 | 243 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
104 | 104 | | |
105 | 105 | | |
106 | 106 | | |
107 | | - | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
108 | 124 | | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
| 125 | + | |
118 | 126 | | |
119 | 127 | | |
120 | 128 | | |
121 | 129 | | |
122 | | - | |
123 | | - | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
124 | 133 | | |
125 | 134 | | |
126 | 135 | | |
127 | 136 | | |
128 | 137 | | |
129 | 138 | | |
130 | 139 | | |
131 | | - | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
132 | 149 | | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
137 | 155 | | |
138 | 156 | | |
139 | 157 | | |
140 | 158 | | |
141 | 159 | | |
142 | 160 | | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
143 | 179 | | |
144 | 180 | | |
145 | 181 | | |
| |||
0 commit comments