@genui-a3/create 0.1.7 → 0.1.9
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.
- package/dist/index.js +268 -80
- package/package.json +3 -1
- package/template/.cursor/rules/example-app.mdc +9 -0
- package/template/.cursorrules +112 -0
- package/template/CLAUDE.md +112 -0
- package/template/README.md +4 -0
- package/template/app/agents/age.ts +1 -0
- package/template/app/agents/greeting.ts +1 -0
- package/template/app/lib/providers/anthropic.ts +1 -1
- package/template/docs/CUSTOM_LOGGING.md +36 -0
- package/template/docs/CUSTOM_PROVIDERS.md +642 -0
- package/template/docs/LOGGING.md +104 -0
- package/template/docs/PROVIDERS.md +217 -0
- package/template/docs/QUICK-START-EXAMPLES.md +197 -0
- package/template/docs/RESILIENCE.md +226 -0
- package/template/package.json +2 -2
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Resilience
|
|
2
|
+
|
|
3
|
+
A3 providers include built-in resilience: automatic retries with backoff, per-request and total timeouts, and model fallback.
|
|
4
|
+
When a request fails, the provider retries against the same model before falling back to the next model in your list — all with zero configuration required.
|
|
5
|
+
|
|
6
|
+
## How it works
|
|
7
|
+
|
|
8
|
+
When you call `sendRequest` or `sendRequestStream`, the provider delegates to `executeWithFallback`, which runs this loop:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
for each model (in priority order):
|
|
12
|
+
for each attempt (1 … 1 + maxAttempts):
|
|
13
|
+
1. Check total timeout — abort if exceeded
|
|
14
|
+
2. Build an AbortSignal combining per-request timeout + total timeout
|
|
15
|
+
3. Call the provider action with (model, signal)
|
|
16
|
+
4. On success → return result
|
|
17
|
+
5. On failure:
|
|
18
|
+
a. Record the error
|
|
19
|
+
b. If retryable and attempts remain → backoff delay → retry same model
|
|
20
|
+
c. Otherwise → move to next model
|
|
21
|
+
|
|
22
|
+
All models exhausted → throw A3ResilienceError
|
|
23
|
+
Total timeout exceeded → throw A3TimeoutError
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Defaults
|
|
27
|
+
|
|
28
|
+
With zero configuration, every provider gets:
|
|
29
|
+
|
|
30
|
+
| Setting | Default |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `retry.maxAttempts` | `2` (3 total attempts per model) |
|
|
33
|
+
| `retry.retryOn` | `'transient'` |
|
|
34
|
+
| `backoff.strategy` | `'exponential'` |
|
|
35
|
+
| `backoff.baseDelayMs` | `500` |
|
|
36
|
+
| `backoff.maxDelayMs` | `30000` |
|
|
37
|
+
| `backoff.jitter` | `true` |
|
|
38
|
+
| `timeout.requestTimeoutMs` | `undefined` (SDK default) |
|
|
39
|
+
| `timeout.totalTimeoutMs` | `undefined` (no limit) |
|
|
40
|
+
| `isRetryableError` | Built-in classifier |
|
|
41
|
+
|
|
42
|
+
These defaults are exported as `DEFAULT_RESILIENCE_CONFIG` from `@genui-a3/core`.
|
|
43
|
+
|
|
44
|
+
## Configuration examples
|
|
45
|
+
|
|
46
|
+
Pass a `resilience` object when creating a provider.
|
|
47
|
+
All fields are optional — unspecified fields keep their defaults.
|
|
48
|
+
|
|
49
|
+
### Minimal — just increase retries
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const provider = createBedrockProvider({
|
|
53
|
+
models: ['us.anthropic.claude-sonnet-4-5-20250929-v1:0'],
|
|
54
|
+
resilience: {
|
|
55
|
+
retry: { maxAttempts: 3 },
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Medium — linear backoff with timeouts
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const provider = createOpenAIProvider({
|
|
64
|
+
models: ['gpt-4o', 'gpt-4o-mini'],
|
|
65
|
+
resilience: {
|
|
66
|
+
retry: { maxAttempts: 2 },
|
|
67
|
+
backoff: { strategy: 'linear', baseDelayMs: 1000 },
|
|
68
|
+
timeout: {
|
|
69
|
+
requestTimeoutMs: 30_000,
|
|
70
|
+
totalTimeoutMs: 90_000,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Maximum — full control
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const provider = createBedrockProvider({
|
|
80
|
+
models: [
|
|
81
|
+
'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
|
82
|
+
'us.anthropic.claude-haiku-4-5-20251001-v1:0',
|
|
83
|
+
],
|
|
84
|
+
resilience: {
|
|
85
|
+
retry: { maxAttempts: 5, retryOn: 'all' },
|
|
86
|
+
backoff: {
|
|
87
|
+
strategy: 'exponential',
|
|
88
|
+
baseDelayMs: 200,
|
|
89
|
+
maxDelayMs: 10_000,
|
|
90
|
+
jitter: true,
|
|
91
|
+
},
|
|
92
|
+
timeout: {
|
|
93
|
+
requestTimeoutMs: 15_000,
|
|
94
|
+
totalTimeoutMs: 120_000,
|
|
95
|
+
},
|
|
96
|
+
isRetryableError: (error) => {
|
|
97
|
+
// Custom logic — retry everything except auth errors
|
|
98
|
+
return !error.message.includes('401')
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Disable retries entirely
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const provider = createBedrockProvider({
|
|
108
|
+
models: ['us.anthropic.claude-sonnet-4-5-20250929-v1:0'],
|
|
109
|
+
resilience: {
|
|
110
|
+
retry: false,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
With `retry: false`, each model is attempted exactly once with no backoff.
|
|
116
|
+
Model fallback still applies if you provide multiple models.
|
|
117
|
+
|
|
118
|
+
## Backoff strategies
|
|
119
|
+
|
|
120
|
+
| Strategy | Formula | Example (baseDelay=500ms) |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `'exponential'` (default) | `baseDelay * 2^attempt` | 500ms, 1000ms, 2000ms, 4000ms… |
|
|
123
|
+
| `'linear'` | `baseDelay * (attempt + 1)` | 500ms, 1000ms, 1500ms, 2000ms… |
|
|
124
|
+
| `'fixed'` | `baseDelay` | 500ms, 500ms, 500ms, 500ms… |
|
|
125
|
+
|
|
126
|
+
All strategies are capped at `maxDelayMs`.
|
|
127
|
+
When `jitter` is `true`, the actual delay is randomized between `0` and the calculated value.
|
|
128
|
+
|
|
129
|
+
## Error classification
|
|
130
|
+
|
|
131
|
+
The built-in `isRetryableError` classifier determines which errors are transient (safe to retry) and which are permanent (skip to next model).
|
|
132
|
+
|
|
133
|
+
**Retryable (transient):**
|
|
134
|
+
|
|
135
|
+
- Network errors: `ECONNRESET`, `ECONNREFUSED`, `ECONNABORTED`, `ETIMEDOUT`, `ENETUNREACH`, `EPIPE`, `EHOSTUNREACH`
|
|
136
|
+
- Throttling: messages containing `throttl`, `rate limit`, `too many requests`, `request limit`, `quota`
|
|
137
|
+
- Timeouts: messages containing `timeout` or `timed out`
|
|
138
|
+
- HTTP status codes: `408` (Request Timeout), `429` (Too Many Requests), `500+` (Server Errors, including `529` Overloaded)
|
|
139
|
+
- AWS SDK v3: reads `$metadata.httpStatusCode` automatically
|
|
140
|
+
|
|
141
|
+
**Not retryable:**
|
|
142
|
+
|
|
143
|
+
- `AbortError` / `TimeoutError` (intentional cancellation by the timeout system)
|
|
144
|
+
- `400` Bad Request, `401` Unauthorized, `403` Forbidden, `404` Not Found
|
|
145
|
+
- Any error not matching the patterns above
|
|
146
|
+
|
|
147
|
+
When `retryOn` is set to `'all'`, the classifier is bypassed and every error triggers a retry (up to `maxAttempts`).
|
|
148
|
+
|
|
149
|
+
## Error handling
|
|
150
|
+
|
|
151
|
+
When all models and retries are exhausted, A3 throws typed errors that preserve the full failure history.
|
|
152
|
+
|
|
153
|
+
### A3ResilienceError
|
|
154
|
+
|
|
155
|
+
Thrown when every model has failed.
|
|
156
|
+
Contains an `errors` array with one entry per failed attempt.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { A3ResilienceError } from '@genui-a3/core'
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await provider.sendRequest(request)
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (err instanceof A3ResilienceError) {
|
|
165
|
+
for (const entry of err.errors) {
|
|
166
|
+
console.log(`${entry.model} attempt ${entry.attempt}: ${entry.error.message}`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Each `ResilienceErrorEntry` contains:
|
|
173
|
+
|
|
174
|
+
- `model` — the model identifier that was attempted
|
|
175
|
+
- `attempt` — 1-based attempt number within that model's retry cycle
|
|
176
|
+
- `error` — the original `Error` object
|
|
177
|
+
|
|
178
|
+
### A3TimeoutError
|
|
179
|
+
|
|
180
|
+
Thrown when `totalTimeoutMs` is exceeded.
|
|
181
|
+
Extends `A3ResilienceError`, so it includes the same `errors` array and can be caught by either type.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { A3TimeoutError, A3ResilienceError } from '@genui-a3/core'
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await provider.sendRequest(request)
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err instanceof A3TimeoutError) {
|
|
190
|
+
console.log('Total timeout exceeded')
|
|
191
|
+
} else if (err instanceof A3ResilienceError) {
|
|
192
|
+
console.log('All models exhausted')
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Custom error classifier
|
|
198
|
+
|
|
199
|
+
Override the built-in classification by providing an `isRetryableError` function.
|
|
200
|
+
When provided, this replaces the default classifier entirely.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const provider = createBedrockProvider({
|
|
204
|
+
models: ['us.anthropic.claude-sonnet-4-5-20250929-v1:0'],
|
|
205
|
+
resilience: {
|
|
206
|
+
isRetryableError: (error) => {
|
|
207
|
+
// Only retry rate-limit errors
|
|
208
|
+
return error.message.includes('429') || error.message.includes('Too Many Requests')
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The custom classifier is ignored when `retryOn` is set to `'all'`.
|
|
215
|
+
|
|
216
|
+
## Key files
|
|
217
|
+
|
|
218
|
+
| File | Description |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `src/types/resilience.ts` | `ResilienceConfig`, `RetryConfig`, `BackoffConfig`, `TimeoutConfig`, `ResolvedResilienceConfig` |
|
|
221
|
+
| `src/utils/resilience/defaults.ts` | `DEFAULT_RESILIENCE_CONFIG`, `resolveResilienceConfig()` |
|
|
222
|
+
| `src/utils/resilience/errorClassification.ts` | `isRetryableError()` — built-in error classifier |
|
|
223
|
+
| `src/errors/resilience.ts` | `A3ResilienceError`, `A3TimeoutError`, `ResilienceErrorEntry` |
|
|
224
|
+
| `providers/utils/executeWithFallback.ts` | `executeWithFallback()` — core execution loop |
|
|
225
|
+
| `providers/utils/backoff.ts` | `calculateBackoff()`, `sleep()` |
|
|
226
|
+
| `src/index.ts` | Public exports: `resolveResilienceConfig`, `DEFAULT_RESILIENCE_CONFIG`, `isRetryableError`, `A3ResilienceError`, `A3TimeoutError` |
|
package/template/package.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"@ag-ui/encoder": "0.0.45",
|
|
12
12
|
"@emotion/react": "11.14.0",
|
|
13
13
|
"@emotion/styled": "11.14.1",
|
|
14
|
-
"@genui-a3/core": "^0.1.
|
|
15
|
-
"@genui-a3/providers": "^0.0.
|
|
14
|
+
"@genui-a3/core": "^0.1.18",
|
|
15
|
+
"@genui-a3/providers": "^0.0.6",
|
|
16
16
|
"@mui/icons-material": "7.3.7",
|
|
17
17
|
"@mui/material": "7.3.7",
|
|
18
18
|
"next": "16.1.6",
|