@genui/a3-create 0.1.36
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/README.md +123 -0
- package/dist/index.js +684 -0
- package/package.json +52 -0
- package/template/.cursor/rules/example-app.mdc +9 -0
- package/template/CLAUDE.md +121 -0
- package/template/README.md +20 -0
- package/template/_gitignore +36 -0
- package/template/app/ThemeProvider.tsx +17 -0
- package/template/app/agents/age.ts +25 -0
- package/template/app/agents/greeting.ts +30 -0
- package/template/app/agents/index.ts +57 -0
- package/template/app/agents/onboarding/index.ts +15 -0
- package/template/app/agents/onboarding/prompt.ts +59 -0
- package/template/app/agents/registry.ts +17 -0
- package/template/app/agents/state.ts +10 -0
- package/template/app/api/agui/route.ts +56 -0
- package/template/app/api/chat/route.ts +35 -0
- package/template/app/api/stream/route.ts +57 -0
- package/template/app/apple-icon-dark.png +0 -0
- package/template/app/apple-icon.png +0 -0
- package/template/app/components/atoms/AgentNode.tsx +56 -0
- package/template/app/components/atoms/AppLogo.tsx +44 -0
- package/template/app/components/atoms/ChatContainer.tsx +13 -0
- package/template/app/components/atoms/ChatHeader.tsx +49 -0
- package/template/app/components/atoms/MarkdownRenderer.tsx +134 -0
- package/template/app/components/atoms/MessageBubble.tsx +21 -0
- package/template/app/components/atoms/TransitionEdge.tsx +49 -0
- package/template/app/components/atoms/index.ts +7 -0
- package/template/app/components/molecules/ChatInput.tsx +94 -0
- package/template/app/components/molecules/ChatMessage.tsx +45 -0
- package/template/app/components/molecules/index.ts +2 -0
- package/template/app/components/organisms/AgentGraph.tsx +75 -0
- package/template/app/components/organisms/AguiChat.tsx +133 -0
- package/template/app/components/organisms/Chat.tsx +88 -0
- package/template/app/components/organisms/ChatMessageList.tsx +35 -0
- package/template/app/components/organisms/ExamplePageLayout.tsx +118 -0
- package/template/app/components/organisms/OnboardingChat.tsx +24 -0
- package/template/app/components/organisms/Sidebar.tsx +147 -0
- package/template/app/components/organisms/SidebarLayout.tsx +58 -0
- package/template/app/components/organisms/StateViewer.tsx +126 -0
- package/template/app/components/organisms/StreamChat.tsx +173 -0
- package/template/app/components/organisms/index.ts +10 -0
- package/template/app/constants/chat.ts +52 -0
- package/template/app/constants/paths.ts +1 -0
- package/template/app/constants/ui.ts +61 -0
- package/template/app/examples/agui/page.tsx +26 -0
- package/template/app/examples/chat/page.tsx +26 -0
- package/template/app/examples/page.tsx +106 -0
- package/template/app/examples/stream/page.tsx +26 -0
- package/template/app/favicon-dark.ico +0 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/icon.svg +13 -0
- package/template/app/layout.tsx +36 -0
- package/template/app/lib/actions/restartSession.ts +10 -0
- package/template/app/lib/getAgentGraphData.ts +43 -0
- package/template/app/lib/getGraphLayout.ts +99 -0
- package/template/app/lib/hooks/useRestart.ts +33 -0
- package/template/app/lib/parseTransitionTargets.ts +140 -0
- package/template/app/lib/providers/anthropic.ts +12 -0
- package/template/app/lib/providers/bedrock.ts +12 -0
- package/template/app/lib/providers/openai.ts +10 -0
- package/template/app/onboarding/page.tsx +21 -0
- package/template/app/page.tsx +16 -0
- package/template/app/styled.d.ts +6 -0
- package/template/app/theme.ts +22 -0
- package/template/docs/A3-README.md +121 -0
- package/template/docs/API-REFERENCE.md +85 -0
- package/template/docs/ARCHITECTURE.md +84 -0
- package/template/docs/CORE-CONCEPTS.md +347 -0
- package/template/docs/CUSTOM_LOGGING.md +36 -0
- package/template/docs/CUSTOM_PROVIDERS.md +642 -0
- package/template/docs/CUSTOM_STORES.md +228 -0
- package/template/docs/PROVIDER-ANTHROPIC.md +45 -0
- package/template/docs/PROVIDER-BEDROCK.md +45 -0
- package/template/docs/PROVIDER-OPENAI.md +47 -0
- package/template/docs/PROVIDERS.md +124 -0
- package/template/docs/QUICK-START-EXAMPLES.md +197 -0
- package/template/docs/RESILIENCE.md +226 -0
- package/template/docs/TRANSITIONS.md +245 -0
- package/template/docs/WIDGETS.md +331 -0
- package/template/docs/contributing/LOGGING.md +104 -0
- package/template/docs/designs/a3-gtm-strategy.md +280 -0
- package/template/docs/designs/a3-platform-vision.md +276 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +41 -0
- package/template/public/android-chrome-192x192.png +0 -0
- package/template/public/android-chrome-512x512.png +0 -0
- package/template/public/site.webmanifest +11 -0
- package/template/scripts/dev.mjs +29 -0
- package/template/tsconfig.json +47 -0
|
@@ -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`.
|
|
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'
|
|
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'
|
|
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` |
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Transitions
|
|
2
|
+
|
|
3
|
+
Transitions control how agents hand off to each other in a multi-agent conversation.
|
|
4
|
+
Each agent's `transition` property determines when and how the conversation moves to the next agent.
|
|
5
|
+
|
|
6
|
+
For agent basics, see [Core Concepts](./CORE-CONCEPTS.md#agent).
|
|
7
|
+
|
|
8
|
+
## Three Transition Modes
|
|
9
|
+
|
|
10
|
+
A3 supports three transition modes based on what you assign to the `transition` property: **default** (omitted), **non-deterministic** (array), and **deterministic** (function).
|
|
11
|
+
|
|
12
|
+
### Default (`transition` Omitted)
|
|
13
|
+
|
|
14
|
+
When `transition` is not set, the agent stays active by default.
|
|
15
|
+
The `redirectToAgent` field is technically present in the LLM output schema as `z.string().nullable()`, but the base prompt restricts the LLM to only redirect to agents listed in its SPECIALIST AGENT POOL — which, without a `transition` array, contains only the agent itself.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { z } from 'zod'
|
|
19
|
+
import { Agent, BaseState } from '@genui/a3'
|
|
20
|
+
|
|
21
|
+
interface MyState extends BaseState {
|
|
22
|
+
topic?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const assistantAgent: Agent<MyState> = {
|
|
26
|
+
id: 'assistant',
|
|
27
|
+
description: 'General assistant',
|
|
28
|
+
prompt: 'You are a helpful assistant.',
|
|
29
|
+
outputSchema: z.object({
|
|
30
|
+
topic: z.string().optional(),
|
|
31
|
+
}),
|
|
32
|
+
// transition is omitted:
|
|
33
|
+
// - Agent stays active each turn
|
|
34
|
+
// - No other agents are shown in the SPECIALIST AGENT POOL
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This is the right choice for single-agent setups or leaf agents that should not route anywhere.
|
|
39
|
+
|
|
40
|
+
### Non-Deterministic (`AgentId[]`)
|
|
41
|
+
|
|
42
|
+
When `transition` is an array of agent IDs, `redirectToAgent` is constrained to a `z.enum(...)` of those IDs.
|
|
43
|
+
The LLM must pick from the listed agents.
|
|
44
|
+
If the LLM returns an ID not in the array, Zod validation throws at parse time.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const triageAgent: Agent<MyState> = {
|
|
48
|
+
id: 'triage',
|
|
49
|
+
description: 'Routes users to the appropriate department',
|
|
50
|
+
prompt: `
|
|
51
|
+
You are a triage agent. Based on the user's request, route them to:
|
|
52
|
+
- "billing" for payment or invoice questions
|
|
53
|
+
- "technical-support" for product issues
|
|
54
|
+
- "account" for account management
|
|
55
|
+
`,
|
|
56
|
+
outputSchema: z.object({}),
|
|
57
|
+
// LLM must pick from these three agents via redirectToAgent
|
|
58
|
+
transition: ['billing', 'technical-support', 'account'],
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The LLM output schema for this agent will include:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
redirectToAgent: z.enum(['billing', 'technical-support', 'account']).nullable()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Deterministic (`(state, goalAchieved) => AgentId`)
|
|
69
|
+
|
|
70
|
+
When `transition` is a function, `redirectToAgent` is completely omitted from the LLM output schema.
|
|
71
|
+
Your code controls routing based on state and goal status.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const authAgent: Agent<AppState> = {
|
|
75
|
+
id: 'auth',
|
|
76
|
+
description: 'Verifies user identity',
|
|
77
|
+
prompt: 'Verify the user. Set goalAchieved when done.',
|
|
78
|
+
outputSchema: z.object({
|
|
79
|
+
isAuthenticated: z.boolean(),
|
|
80
|
+
}),
|
|
81
|
+
// Code decides routing — LLM has no say
|
|
82
|
+
transition: (state, goalAchieved) => {
|
|
83
|
+
if (goalAchieved && state.isAuthenticated) return 'main-menu'
|
|
84
|
+
if (state.failedAttempts > 3) return 'escalation'
|
|
85
|
+
return 'auth' // returning own ID = stay here
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The function receives:
|
|
91
|
+
|
|
92
|
+
- `state` -- the session state *after* `setState` has been applied for this turn
|
|
93
|
+
- `goalAchieved` -- the `goalAchieved` boolean from the LLM's response
|
|
94
|
+
|
|
95
|
+
Returning the agent's own ID keeps it active for the next turn.
|
|
96
|
+
|
|
97
|
+
## Choosing a Transition Mode
|
|
98
|
+
|
|
99
|
+
| Scenario | Mode | Why |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| Single agent or leaf agent | Omit `transition` | Agent stays active, no routing exposed |
|
|
102
|
+
| Triage hub with known targets | `AgentId[]` | LLM picks from a bounded set |
|
|
103
|
+
| Strict sequential workflow | `function` | Code controls ordering |
|
|
104
|
+
| Routing depends on state conditions | `function` | Logic in code, not prompt |
|
|
105
|
+
|
|
106
|
+
## Transition Mechanics
|
|
107
|
+
|
|
108
|
+
### Recursive Chaining
|
|
109
|
+
|
|
110
|
+
When an agent transitions to a different agent, `manageFlow` recursively invokes the next agent within the same `send()` call.
|
|
111
|
+
The intermediate agent's `chatbotMessage` becomes `lastAgentUnsentMessage`, which is injected into the next agent's system prompt as transition context.
|
|
112
|
+
The user sees a single response even if multiple agents were involved.
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
User sends message
|
|
116
|
+
→ Agent A responds (chatbotMessage: "Let me transfer you...")
|
|
117
|
+
→ Agent B receives lastAgentUnsentMessage: "Let me transfer you..."
|
|
118
|
+
→ Agent B responds to user (this is what the user sees)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Recursion Limit
|
|
122
|
+
|
|
123
|
+
A3 enforces a maximum number of automatic transitions per `send()` call to prevent infinite loops.
|
|
124
|
+
The default limit is **10**, configurable via `ChatSessionConfig.agentRecursionLimit`.
|
|
125
|
+
Only transitions to a *different* agent count toward the limit (staying on the same agent does not increment the counter).
|
|
126
|
+
|
|
127
|
+
When the limit is reached:
|
|
128
|
+
|
|
129
|
+
- The current agent's response is returned to the user
|
|
130
|
+
- `nextAgentId` in the response points to the blocked target agent
|
|
131
|
+
- A warning is logged
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const session = new ChatSession<MyState>({
|
|
135
|
+
sessionId: 'user-123',
|
|
136
|
+
store: new MemorySessionStore(),
|
|
137
|
+
initialAgentId: 'triage',
|
|
138
|
+
initialState: {},
|
|
139
|
+
provider,
|
|
140
|
+
agentRecursionLimit: 5, // lower limit for tighter control
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Streaming Transitions
|
|
145
|
+
|
|
146
|
+
During streaming, each transition yields an `AgentTransition` event with `{ fromAgentId, toAgentId }` between agents.
|
|
147
|
+
See the [StreamEvent table](./CORE-CONCEPTS.md#streamevent-types) in Core Concepts.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
for await (const event of session.send({ message: 'Help me', stream: true })) {
|
|
151
|
+
if (event.type === 'AgentTransition') {
|
|
152
|
+
console.log(`${event.fromAgentId} → ${event.toAgentId}`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Non-Deterministic Routing Example
|
|
158
|
+
|
|
159
|
+
A complete triage pattern with LLM-driven routing:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { z } from 'zod'
|
|
163
|
+
import { Agent, AgentRegistry, ChatSession, MemorySessionStore, BaseState } from '@genui/a3'
|
|
164
|
+
import { createBedrockProvider } from '@genui/a3-bedrock'
|
|
165
|
+
|
|
166
|
+
interface SupportState extends BaseState {
|
|
167
|
+
category?: string
|
|
168
|
+
resolved: boolean
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Triage agent — LLM picks the target from a constrained set
|
|
172
|
+
const triageAgent: Agent<SupportState> = {
|
|
173
|
+
id: 'triage',
|
|
174
|
+
description: 'Routes users to the correct support department',
|
|
175
|
+
prompt: `
|
|
176
|
+
You are a triage agent. Determine what the user needs and route them:
|
|
177
|
+
- "billing" for payment, invoices, or subscription questions
|
|
178
|
+
- "technical-support" for bugs, errors, or product issues
|
|
179
|
+
- "account" for password resets, profile changes, or access
|
|
180
|
+
Always set redirectToAgent on your first response.
|
|
181
|
+
`,
|
|
182
|
+
outputSchema: z.object({
|
|
183
|
+
category: z.string().optional(),
|
|
184
|
+
}),
|
|
185
|
+
transition: ['billing', 'technical-support', 'account'],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const billingAgent: Agent<SupportState> = {
|
|
189
|
+
id: 'billing',
|
|
190
|
+
description: 'Handles billing and payment questions',
|
|
191
|
+
prompt: `
|
|
192
|
+
You are a billing specialist. Help the user with payment or invoice issues.
|
|
193
|
+
Set goalAchieved to true once the billing question is fully answered.
|
|
194
|
+
`,
|
|
195
|
+
outputSchema: z.object({ resolved: z.boolean() }),
|
|
196
|
+
// Deterministic (function): code decides the next agent.
|
|
197
|
+
transition: (_state, goalAchieved) => goalAchieved ? 'triage' : 'billing',
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const technicalSupportAgent: Agent<SupportState> = {
|
|
201
|
+
id: 'technical-support',
|
|
202
|
+
description: 'Handles technical issues and bugs',
|
|
203
|
+
prompt: `
|
|
204
|
+
You are a technical support specialist. Help the user troubleshoot their issue.
|
|
205
|
+
If the issue is resolved, redirect to triage so the user can raise other concerns.
|
|
206
|
+
If you need more information, stay on this agent to continue troubleshooting.
|
|
207
|
+
`,
|
|
208
|
+
outputSchema: z.object({ resolved: z.boolean() }),
|
|
209
|
+
// Non-deterministic (array): the LLM picks from the listed agents.
|
|
210
|
+
transition: ['triage', 'technical-support'],
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const accountAgent: Agent<SupportState> = {
|
|
214
|
+
id: 'account',
|
|
215
|
+
description: 'Handles account management',
|
|
216
|
+
prompt: `
|
|
217
|
+
You are an account specialist. Help the user manage their account.
|
|
218
|
+
`,
|
|
219
|
+
outputSchema: z.object({ resolved: z.boolean() }),
|
|
220
|
+
// Default (omitted): agent stays active every turn.
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Setup
|
|
224
|
+
const registry = AgentRegistry.getInstance<SupportState>()
|
|
225
|
+
registry.register([triageAgent, billingAgent, technicalSupportAgent, accountAgent])
|
|
226
|
+
|
|
227
|
+
const provider = createBedrockProvider({
|
|
228
|
+
models: ['us.anthropic.claude-sonnet-4-5-20250929-v1:0'],
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const session = new ChatSession<SupportState>({
|
|
232
|
+
sessionId: 'support-001',
|
|
233
|
+
store: new MemorySessionStore(),
|
|
234
|
+
initialAgentId: 'triage',
|
|
235
|
+
initialState: { resolved: false },
|
|
236
|
+
provider,
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Conversation
|
|
240
|
+
const response = await session.send({ message: 'I got charged twice last month' })
|
|
241
|
+
// Triage agent sets redirectToAgent to "billing"
|
|
242
|
+
// → billing agent responds to the user in the same request
|
|
243
|
+
console.log(response.activeAgentId) // 'billing'
|
|
244
|
+
console.log(response.responseMessage) // Billing agent's response
|
|
245
|
+
```
|