@genui-a3/core 0.1.0 → 0.1.1
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 +672 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1 +1,672 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @genui-a3/core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@genui-a3/core)
|
|
4
|
+
[](https://github.com/generalui/a3/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
**A TypeScript framework for building multi-agent chat applications.**
|
|
7
|
+
|
|
8
|
+
Define focused agents.
|
|
9
|
+
Register them.
|
|
10
|
+
Let A3 route conversations dynamically.
|
|
11
|
+
No graphs.
|
|
12
|
+
No state machines.
|
|
13
|
+
|
|
14
|
+
## Feature Highlights
|
|
15
|
+
|
|
16
|
+
- **Multi-agent orchestration** -- agents route to each other dynamically based on conversation context
|
|
17
|
+
- **Shared state** -- a single state object flows across all agents in a session
|
|
18
|
+
- **Structured output** -- Zod schemas validate every LLM response at runtime
|
|
19
|
+
- **Streaming** -- real-time token streaming via `sendStream` with AG-UI-compatible event types
|
|
20
|
+
- **Pluggable session stores** -- swap in-memory, AWS AgentCore, Redis, or your own store
|
|
21
|
+
- **Pluggable providers** -- ships with AWS Bedrock; designed for additional providers
|
|
22
|
+
- **TypeScript-native** -- full type safety from agent definitions to response handling
|
|
23
|
+
- **Dual ESM/CJS** -- works in any Node.js environment
|
|
24
|
+
|
|
25
|
+
## Table of Contents
|
|
26
|
+
|
|
27
|
+
- [Quick Start](#quick-start)
|
|
28
|
+
- [Architecture at a Glance](#architecture-at-a-glance)
|
|
29
|
+
- [Core Concepts](#core-concepts)
|
|
30
|
+
- [Agent](#agent)
|
|
31
|
+
- [AgentRegistry](#agentregistry)
|
|
32
|
+
- [ChatSession](#chatsession)
|
|
33
|
+
- [State](#state)
|
|
34
|
+
- [Output Schemas](#output-schemas)
|
|
35
|
+
- [Routing](#routing)
|
|
36
|
+
- [Session Stores](#session-stores)
|
|
37
|
+
- [Providers](#providers)
|
|
38
|
+
- [Streaming](#streaming)
|
|
39
|
+
- [Multi-Agent Example](#multi-agent-example)
|
|
40
|
+
- [API Reference](#api-reference)
|
|
41
|
+
- [Comparison](#comparison)
|
|
42
|
+
- [Roadmap](#roadmap)
|
|
43
|
+
- [Requirements](#requirements)
|
|
44
|
+
- [Contributing](#contributing)
|
|
45
|
+
- [License](#license)
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install @genui-a3/core
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Define an agent
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { z } from 'zod'
|
|
59
|
+
import { Agent, simpleAgentResponse, BaseState } from '@genui-a3/core'
|
|
60
|
+
|
|
61
|
+
interface State extends BaseState {
|
|
62
|
+
userName?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const greetingAgent: Agent<State> = {
|
|
66
|
+
id: 'greeting',
|
|
67
|
+
name: 'Greeting Agent',
|
|
68
|
+
description: 'Greets the user and collects their name',
|
|
69
|
+
promptGenerator: async () => `
|
|
70
|
+
You are a friendly greeting agent. Your goal is to greet the user
|
|
71
|
+
and learn their name. Once you have their name, set goalAchieved to true.
|
|
72
|
+
`,
|
|
73
|
+
outputSchema: z.object({
|
|
74
|
+
userName: z.string().optional(),
|
|
75
|
+
}),
|
|
76
|
+
generateAgentResponse: simpleAgentResponse,
|
|
77
|
+
fitDataInGeneralFormat: (data, state) => ({ ...state, ...data }),
|
|
78
|
+
nextAgentSelector: (_state, goalAchieved) =>
|
|
79
|
+
goalAchieved ? 'end' : 'greeting',
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Register and run
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { AgentRegistry, ChatSession, MemorySessionStore } from '@genui-a3/core'
|
|
87
|
+
|
|
88
|
+
const registry = AgentRegistry.getInstance<State>()
|
|
89
|
+
registry.register(greetingAgent)
|
|
90
|
+
|
|
91
|
+
const session = new ChatSession<State>({
|
|
92
|
+
sessionId: 'demo',
|
|
93
|
+
store: new MemorySessionStore(),
|
|
94
|
+
initialAgentId: 'greeting',
|
|
95
|
+
initialState: { userName: undefined },
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const response = await session.send('Hi there!')
|
|
99
|
+
console.log(response.responseMessage)
|
|
100
|
+
// => "Hello! I'd love to get to know you. What's your name?"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
That's it.
|
|
104
|
+
One agent, one session, one function call.
|
|
105
|
+
|
|
106
|
+
## Architecture at a Glance
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
110
|
+
│ Your Application │
|
|
111
|
+
└─────────────────────────┬────────────────────────────────────┘
|
|
112
|
+
│ ▲
|
|
113
|
+
.send(message) ChatResponse
|
|
114
|
+
│ { responseMessage,
|
|
115
|
+
│ state, goalAchieved }
|
|
116
|
+
▼ │
|
|
117
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
118
|
+
│ ChatSession │
|
|
119
|
+
│ │
|
|
120
|
+
│ 1. Load session from store 6. Save updated session │
|
|
121
|
+
│ 2. Append user message 5. Append bot message │
|
|
122
|
+
│ │
|
|
123
|
+
└───────────┬──────────────────────────────────────────────────┘
|
|
124
|
+
│ ▲
|
|
125
|
+
│ manageFlow({ agent, │ { responseMessage,
|
|
126
|
+
│ sessionData }) │ newState,
|
|
127
|
+
│ │ nextAgentId }
|
|
128
|
+
▼ │
|
|
129
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
130
|
+
│ ChatFlow │
|
|
131
|
+
│ │
|
|
132
|
+
│ Looks up active agent, delegates, checks routing │
|
|
133
|
+
│ │
|
|
134
|
+
│ If nextAgent ≠ activeAgent: │
|
|
135
|
+
│ ┌──────────────────────────────────────────┐ │
|
|
136
|
+
│ │ Recursive call to manageFlow │ │
|
|
137
|
+
│ │ with new agent + updated state │ │
|
|
138
|
+
│ └──────────────────────────────────────────┘ │
|
|
139
|
+
│ │
|
|
140
|
+
└───────────┬──────────────────────────────────────────────────┘
|
|
141
|
+
│ ▲
|
|
142
|
+
│ generateAgentResponse │ { chatbotMessage,
|
|
143
|
+
│ ({ agent, sessionData }) │ newState,
|
|
144
|
+
│ │ nextAgentId }
|
|
145
|
+
▼ │
|
|
146
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
147
|
+
│ Active Agent │
|
|
148
|
+
│ │
|
|
149
|
+
│ • Builds system prompt (promptGenerator) │
|
|
150
|
+
│ • Defines output schema (Zod) │
|
|
151
|
+
│ • Determines next agent (nextAgentSelector) │
|
|
152
|
+
│ │
|
|
153
|
+
└───────────┬──────────────────────────────────────────────────┘
|
|
154
|
+
│ ▲
|
|
155
|
+
│ prompt + schema │ structured JSON
|
|
156
|
+
▼ │
|
|
157
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
158
|
+
│ Provider │
|
|
159
|
+
│ (Bedrock) │
|
|
160
|
+
│ │
|
|
161
|
+
│ • Converts Zod → JSON Schema │
|
|
162
|
+
│ • Merges message history │
|
|
163
|
+
│ • Model fallback on error │
|
|
164
|
+
│ │
|
|
165
|
+
└───────────┬──────────────────────────────────────────────────┘
|
|
166
|
+
│ ▲
|
|
167
|
+
│ API request │ API response
|
|
168
|
+
▼ │
|
|
169
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
170
|
+
│ LLM │
|
|
171
|
+
└──────────────────────────────────────────────────────────────┘
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**How it flows:**
|
|
175
|
+
|
|
176
|
+
1. Your app calls `session.send(message)` with the user's input
|
|
177
|
+
1. **ChatSession** loads session data (history, state) from the configured store and appends the user message
|
|
178
|
+
1. **ChatFlow** looks up the active agent and calls `generateAgentResponse`
|
|
179
|
+
1. The **Agent** builds a system prompt, defines its Zod output schema, and delegates to the provider
|
|
180
|
+
1. The **Provider** sends the request to the LLM and returns structured JSON
|
|
181
|
+
1. The **Agent** extracts state updates and a routing decision (`nextAgentId`) from the response
|
|
182
|
+
1. If the next agent differs from the active agent, ChatFlow **recursively calls `manageFlow`** with the new agent and updated state
|
|
183
|
+
1. **ChatSession** appends the bot message, saves the updated session, and returns a `ChatResponse` to your app
|
|
184
|
+
|
|
185
|
+
Agents route dynamically.
|
|
186
|
+
There is no fixed graph.
|
|
187
|
+
Each agent decides whether to continue or hand off based on the conversation.
|
|
188
|
+
|
|
189
|
+
## Core Concepts
|
|
190
|
+
|
|
191
|
+
### Agent
|
|
192
|
+
|
|
193
|
+
An agent is the fundamental building block.
|
|
194
|
+
Each agent has a focused responsibility and defines how it generates responses, what structured data it extracts, and when to hand off to another agent.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { z } from 'zod'
|
|
198
|
+
import { Agent, simpleAgentResponse, BaseState } from '@genui-a3/core'
|
|
199
|
+
|
|
200
|
+
interface MyState extends BaseState {
|
|
201
|
+
userName?: string
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const greetingAgent: Agent<MyState> = {
|
|
205
|
+
// Identity
|
|
206
|
+
id: 'greeting',
|
|
207
|
+
name: 'Greeting Agent',
|
|
208
|
+
description: 'Greets the user and collects their name',
|
|
209
|
+
|
|
210
|
+
// Prompt: instructions for the LLM
|
|
211
|
+
promptGenerator: async () => `
|
|
212
|
+
You are a friendly greeting agent.
|
|
213
|
+
Ask the user for their name, then greet them.
|
|
214
|
+
Set goalAchieved to true once you know their name.
|
|
215
|
+
`,
|
|
216
|
+
|
|
217
|
+
// Output schema: Zod schema for structured data extraction
|
|
218
|
+
outputSchema: z.object({
|
|
219
|
+
userName: z.string().optional(),
|
|
220
|
+
}),
|
|
221
|
+
|
|
222
|
+
// Response generator: how to process the LLM response
|
|
223
|
+
generateAgentResponse: simpleAgentResponse,
|
|
224
|
+
|
|
225
|
+
// State mapper: merge extracted data into global state
|
|
226
|
+
fitDataInGeneralFormat: (data, state) => ({
|
|
227
|
+
...state,
|
|
228
|
+
...data,
|
|
229
|
+
}),
|
|
230
|
+
|
|
231
|
+
// Routing: decide the next agent after each turn
|
|
232
|
+
nextAgentSelector: (state, goalAchieved) => {
|
|
233
|
+
return goalAchieved ? 'next-agent' : 'greeting'
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Agent properties:**
|
|
239
|
+
|
|
240
|
+
| Property | Required | Description |
|
|
241
|
+
|---|---|---|
|
|
242
|
+
| `id` | Yes | Unique identifier for the agent |
|
|
243
|
+
| `name` | Yes | Human-readable display name |
|
|
244
|
+
| `description` | Yes | What this agent does (used in agent pool prompts) |
|
|
245
|
+
| `promptGenerator` | Yes | Async function returning the system prompt for this agent |
|
|
246
|
+
| `outputSchema` | Yes | Zod schema defining structured data to extract from LLM responses |
|
|
247
|
+
| `generateAgentResponse` | Yes | Function that orchestrates the full response cycle |
|
|
248
|
+
| `fitDataInGeneralFormat` | Yes | Maps extracted LLM data into the shared state object |
|
|
249
|
+
| `nextAgentSelector` | No | Determines the next agent based on state and goal status |
|
|
250
|
+
| `transitionsTo` | No | Array of agent IDs this agent is allowed to redirect to |
|
|
251
|
+
| `filterHistoryStrategy` | No | Custom function to filter conversation history before sending to the LLM |
|
|
252
|
+
| `modelId` | No | Override the default model for this agent |
|
|
253
|
+
|
|
254
|
+
### AgentRegistry
|
|
255
|
+
|
|
256
|
+
A singleton registry where all agents are registered before use.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { AgentRegistry } from '@genui-a3/core'
|
|
260
|
+
|
|
261
|
+
const registry = AgentRegistry.getInstance<MyState>()
|
|
262
|
+
|
|
263
|
+
// Register one or many agents
|
|
264
|
+
registry.register(greetingAgent)
|
|
265
|
+
registry.register([authAgent, mainAgent, wrapUpAgent])
|
|
266
|
+
|
|
267
|
+
// Query the registry
|
|
268
|
+
registry.has('greeting') // true
|
|
269
|
+
registry.get('greeting') // Agent object
|
|
270
|
+
registry.getAll() // All registered agents
|
|
271
|
+
registry.getDescriptions() // { greeting: 'Greets the user...' }
|
|
272
|
+
registry.count // 4
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### ChatSession
|
|
276
|
+
|
|
277
|
+
The primary interface your application uses to interact with A3.
|
|
278
|
+
Create a session, send messages, get responses.
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { ChatSession, MemorySessionStore } from '@genui-a3/core'
|
|
282
|
+
|
|
283
|
+
const session = new ChatSession<MyState>({
|
|
284
|
+
sessionId: 'user-123',
|
|
285
|
+
store: new MemorySessionStore(), // pluggable persistence
|
|
286
|
+
initialAgentId: 'greeting',
|
|
287
|
+
initialState: { userName: undefined },
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// Send a message and get a structured response
|
|
291
|
+
const result = await session.send('Hello!')
|
|
292
|
+
|
|
293
|
+
result.responseMessage // "Hi there! What's your name?"
|
|
294
|
+
result.activeAgentId // 'greeting'
|
|
295
|
+
result.nextAgentId // 'greeting'
|
|
296
|
+
result.state // { userName: undefined }
|
|
297
|
+
result.goalAchieved // false
|
|
298
|
+
result.sessionId // 'user-123'
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### State
|
|
302
|
+
|
|
303
|
+
A3 uses a shared global state object that flows across all agents in a session.
|
|
304
|
+
Define your state by extending `BaseState`.
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { BaseState } from '@genui-a3/core'
|
|
308
|
+
|
|
309
|
+
interface AppState extends BaseState {
|
|
310
|
+
userName?: string
|
|
311
|
+
isAuthenticated: boolean
|
|
312
|
+
currentStep: string
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Each agent's `fitDataInGeneralFormat` merges its extracted data into this shared state.
|
|
317
|
+
When agents switch, the full state carries over.
|
|
318
|
+
|
|
319
|
+
### Output Schemas
|
|
320
|
+
|
|
321
|
+
Every agent defines a Zod schema for the structured data it needs to extract from LLM responses.
|
|
322
|
+
A3 merges this with base fields (`chatbotMessage`, `goalAchieved`, `redirectToAgent`) and validates the LLM output.
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Static schema
|
|
326
|
+
const schema = z.object({
|
|
327
|
+
userName: z.string().optional(),
|
|
328
|
+
sentiment: z.enum(['positive', 'neutral', 'negative']),
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Dynamic schema (based on current session state)
|
|
332
|
+
const dynamicSchema = (sessionData) => z.object({
|
|
333
|
+
userName: z.string().describe(`Current: ${sessionData.state.userName ?? 'unknown'}`),
|
|
334
|
+
})
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Schemas serve two purposes:
|
|
338
|
+
|
|
339
|
+
1. **Instruct the LLM** on what data to extract (field names and descriptions become part of the prompt)
|
|
340
|
+
1. **Validate the response** at runtime so your application always receives well-typed data
|
|
341
|
+
|
|
342
|
+
### Routing
|
|
343
|
+
|
|
344
|
+
Agents route to each other in three ways:
|
|
345
|
+
|
|
346
|
+
**1. `nextAgentSelector`** -- Choose the next agent programmatically after each turn:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
nextAgentSelector: (state, goalAchieved) => {
|
|
350
|
+
if (goalAchieved) return 'main-menu'
|
|
351
|
+
if (state.failedAttempts > 3) return 'escalation'
|
|
352
|
+
return 'auth' // stay on current agent
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**2. `redirectToAgent`** -- The LLM itself can request a redirect via the base response schema.
|
|
357
|
+
The agent's prompt and agent pool listing tell the LLM which agents are available.
|
|
358
|
+
|
|
359
|
+
**3. `transitionsTo`** -- Constrain which agents the LLM can redirect to:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
const agent: Agent<MyState> = {
|
|
363
|
+
id: 'triage',
|
|
364
|
+
transitionsTo: ['billing', 'support', 'account'],
|
|
365
|
+
// LLM can only redirect to these three agents
|
|
366
|
+
// ...
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
When a redirect happens, **ChatFlow recursively invokes the next agent** in the same request.
|
|
371
|
+
The user sees a single response, even if multiple agents were involved.
|
|
372
|
+
|
|
373
|
+
### Session Stores
|
|
374
|
+
|
|
375
|
+
A3 uses pluggable session stores for persistence.
|
|
376
|
+
Any object implementing the `SessionStore` interface works.
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
interface SessionStore<TState extends BaseState> {
|
|
380
|
+
load(sessionId: string): Promise<SessionData<TState> | null>
|
|
381
|
+
save(sessionId: string, data: SessionData<TState>): Promise<void>
|
|
382
|
+
delete?(sessionId: string): Promise<void>
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Built-in stores:**
|
|
387
|
+
|
|
388
|
+
| Store | Use case |
|
|
389
|
+
|---|---|
|
|
390
|
+
| `MemorySessionStore` | Development and testing (sessions lost on restart) |
|
|
391
|
+
| `AgentCoreMemoryStore` | AWS Bedrock AgentCore integration for persistent storage |
|
|
392
|
+
|
|
393
|
+
Custom stores are straightforward to implement for Redis, DynamoDB, PostgreSQL, or any other backend.
|
|
394
|
+
|
|
395
|
+
### Providers
|
|
396
|
+
|
|
397
|
+
Providers handle communication with LLM backends.
|
|
398
|
+
A3 ships with an AWS Bedrock provider and is designed so additional providers can be added.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { sendChatRequest } from '@genui-a3/core'
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The Bedrock provider:
|
|
405
|
+
|
|
406
|
+
- Sends structured requests via the AWS Bedrock Converse API
|
|
407
|
+
- Uses tool-based JSON extraction for reliable structured output
|
|
408
|
+
- Supports model fallback (primary model fails, falls back to secondary)
|
|
409
|
+
- Merges sequential same-sender messages for API compatibility
|
|
410
|
+
- Applies agent-specific history filtering before sending
|
|
411
|
+
|
|
412
|
+
### Streaming
|
|
413
|
+
|
|
414
|
+
A3 supports real-time token streaming via `sendStream`.
|
|
415
|
+
Instead of waiting for a complete response, your application receives events as they happen.
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
for await (const event of session.sendStream('Hello!')) {
|
|
419
|
+
switch (event.type) {
|
|
420
|
+
case 'TextMessageContent':
|
|
421
|
+
process.stdout.write(event.delta) // real-time token output
|
|
422
|
+
break
|
|
423
|
+
case 'AgentTransition':
|
|
424
|
+
console.log(`${event.fromAgentId} → ${event.toAgentId}`)
|
|
425
|
+
break
|
|
426
|
+
case 'RunFinished':
|
|
427
|
+
console.log('Final state:', event.response.state)
|
|
428
|
+
break
|
|
429
|
+
case 'RunError':
|
|
430
|
+
console.error('Error:', event.error)
|
|
431
|
+
break
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**StreamEvent types:**
|
|
437
|
+
|
|
438
|
+
| Event Type | Key Fields | Description |
|
|
439
|
+
|---|---|---|
|
|
440
|
+
| `RunStarted` | `runId`, `threadId` | Stream has begun |
|
|
441
|
+
| `TextMessageStart` | `messageId` | A new text message is starting |
|
|
442
|
+
| `TextMessageContent` | `delta`, `agentId` | A text chunk from the active agent |
|
|
443
|
+
| `TextMessageEnd` | `messageId` | Text message complete |
|
|
444
|
+
| `ToolCallStart` | `toolCallId`, `toolCallName` | Tool/function call initiated |
|
|
445
|
+
| `ToolCallArgs` | `toolCallId`, `delta` | Tool argument chunk |
|
|
446
|
+
| `ToolCallEnd` | `toolCallId` | Tool call complete |
|
|
447
|
+
| `ToolCallResult` | `data`, `agentId` | Tool execution result with extracted data |
|
|
448
|
+
| `AgentTransition` | `fromAgentId`, `toAgentId` | Agent handoff occurred |
|
|
449
|
+
| `RunFinished` | `response` | Stream complete with final `ChatResponse` |
|
|
450
|
+
| `RunError` | `error`, `agentId` | Error during stream |
|
|
451
|
+
|
|
452
|
+
## Multi-Agent Example
|
|
453
|
+
|
|
454
|
+
Three agents routing between each other, demonstrating state flowing across agent boundaries and automatic agent chaining.
|
|
455
|
+
|
|
456
|
+
### Define the agents
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { z } from 'zod'
|
|
460
|
+
import { Agent, simpleAgentResponse, BaseState } from '@genui-a3/core'
|
|
461
|
+
|
|
462
|
+
interface AppState extends BaseState {
|
|
463
|
+
userName?: string
|
|
464
|
+
isAuthenticated: boolean
|
|
465
|
+
issueCategory?: string
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Agent 1: Greeting -- collects the user's name, then routes to auth
|
|
469
|
+
const greetingAgent: Agent<AppState> = {
|
|
470
|
+
id: 'greeting',
|
|
471
|
+
name: 'Greeting Agent',
|
|
472
|
+
description: 'Greets the user and collects their name',
|
|
473
|
+
promptGenerator: async () => `
|
|
474
|
+
Greet the user warmly. Ask for their name.
|
|
475
|
+
Once you have it, set goalAchieved to true.
|
|
476
|
+
`,
|
|
477
|
+
outputSchema: z.object({ userName: z.string().optional() }),
|
|
478
|
+
generateAgentResponse: simpleAgentResponse,
|
|
479
|
+
fitDataInGeneralFormat: (data, state) => ({ ...state, ...data }),
|
|
480
|
+
nextAgentSelector: (_state, goalAchieved) =>
|
|
481
|
+
goalAchieved ? 'auth' : 'greeting',
|
|
482
|
+
transitionsTo: ['auth'],
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Agent 2: Auth -- verifies identity, then routes to support
|
|
486
|
+
const authAgent: Agent<AppState> = {
|
|
487
|
+
id: 'auth',
|
|
488
|
+
name: 'Auth Agent',
|
|
489
|
+
description: 'Verifies user identity',
|
|
490
|
+
promptGenerator: async ({ sessionData }) => `
|
|
491
|
+
The user's name is ${sessionData.state.userName}.
|
|
492
|
+
Ask them to confirm their email to verify identity.
|
|
493
|
+
Set goalAchieved to true once verified.
|
|
494
|
+
`,
|
|
495
|
+
outputSchema: z.object({ isAuthenticated: z.boolean() }),
|
|
496
|
+
generateAgentResponse: simpleAgentResponse,
|
|
497
|
+
fitDataInGeneralFormat: (data, state) => ({ ...state, ...data }),
|
|
498
|
+
nextAgentSelector: (_state, goalAchieved) =>
|
|
499
|
+
goalAchieved ? 'support' : 'auth',
|
|
500
|
+
transitionsTo: ['support'],
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Agent 3: Support -- handles the user's issue
|
|
504
|
+
const supportAgent: Agent<AppState> = {
|
|
505
|
+
id: 'support',
|
|
506
|
+
name: 'Support Agent',
|
|
507
|
+
description: 'Helps resolve user issues',
|
|
508
|
+
promptGenerator: async ({ sessionData }) => `
|
|
509
|
+
The user ${sessionData.state.userName} is authenticated.
|
|
510
|
+
Help them with their issue. Categorize it.
|
|
511
|
+
Set goalAchieved when resolved.
|
|
512
|
+
`,
|
|
513
|
+
outputSchema: z.object({
|
|
514
|
+
issueCategory: z.string().optional(),
|
|
515
|
+
}),
|
|
516
|
+
generateAgentResponse: simpleAgentResponse,
|
|
517
|
+
fitDataInGeneralFormat: (data, state) => ({ ...state, ...data }),
|
|
518
|
+
nextAgentSelector: (_state, goalAchieved) =>
|
|
519
|
+
goalAchieved ? 'end' : 'support',
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Wire them up
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
import { AgentRegistry, ChatSession, MemorySessionStore } from '@genui-a3/core'
|
|
527
|
+
|
|
528
|
+
const registry = AgentRegistry.getInstance<AppState>()
|
|
529
|
+
registry.register([greetingAgent, authAgent, supportAgent])
|
|
530
|
+
|
|
531
|
+
const session = new ChatSession<AppState>({
|
|
532
|
+
sessionId: 'user-456',
|
|
533
|
+
store: new MemorySessionStore(),
|
|
534
|
+
initialAgentId: 'greeting',
|
|
535
|
+
initialState: { isAuthenticated: false },
|
|
536
|
+
})
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Conversation flow
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
// Turn 1: User greets, greeting agent responds
|
|
543
|
+
await session.send('Hello!')
|
|
544
|
+
// => Greeting agent asks for name
|
|
545
|
+
|
|
546
|
+
// Turn 2: User provides name, greeting agent completes and chains to auth
|
|
547
|
+
await session.send("I'm Alex")
|
|
548
|
+
// => Auth agent asks for email verification
|
|
549
|
+
// (greeting → auth happened automatically in one request)
|
|
550
|
+
|
|
551
|
+
// Turn 3: User verifies, auth completes and chains to support
|
|
552
|
+
await session.send('alex@example.com')
|
|
553
|
+
// => Support agent asks how it can help
|
|
554
|
+
// State now: { userName: 'Alex', isAuthenticated: true }
|
|
555
|
+
|
|
556
|
+
// Turn 4: Support agent handles the issue
|
|
557
|
+
await session.send('I need help with my billing')
|
|
558
|
+
// => Support agent resolves the issue
|
|
559
|
+
// State: { userName: 'Alex', isAuthenticated: true, issueCategory: 'billing' }
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Notice that:
|
|
563
|
+
|
|
564
|
+
- **State persists across agents**: `userName` set by the greeting agent is available to auth and support
|
|
565
|
+
- **Agent chaining is automatic**: when greeting completes, auth starts in the same request
|
|
566
|
+
- **Each agent has its own prompt and schema**: they extract different data but share the same state
|
|
567
|
+
|
|
568
|
+
## API Reference
|
|
569
|
+
|
|
570
|
+
### Core Exports
|
|
571
|
+
|
|
572
|
+
| Export | Type | Description |
|
|
573
|
+
|---|---|---|
|
|
574
|
+
| `ChatSession` | Class | Primary interface for sending messages and managing conversations |
|
|
575
|
+
| `AgentRegistry` | Class | Singleton registry for agent registration and lookup |
|
|
576
|
+
| `simpleAgentResponse` | Function | Default response generator for agents |
|
|
577
|
+
| `getAgentResponse` | Function | Low-level agent response pipeline (prompt, schema, LLM call, validation) |
|
|
578
|
+
| `manageFlow` | Function | Recursive chat flow orchestration with automatic agent chaining |
|
|
579
|
+
| `manageFlowStream` | Function | Streaming variant of `manageFlow` yielding `StreamEvent`s |
|
|
580
|
+
| `createFullOutputSchema` | Function | Merges agent schema with base response fields |
|
|
581
|
+
| `sendChatRequest` | Function | Sends structured requests to the LLM provider |
|
|
582
|
+
| `MemorySessionStore` | Class | In-memory session store for development and testing |
|
|
583
|
+
| `AgentCoreMemoryStore` | Class | AWS Bedrock AgentCore session store |
|
|
584
|
+
|
|
585
|
+
### ChatSession Methods
|
|
586
|
+
|
|
587
|
+
| Method | Returns | Description |
|
|
588
|
+
|---|---|---|
|
|
589
|
+
| `send(message)` | `Promise<ChatResponse<TState>>` | Send a user message and get the agent's response |
|
|
590
|
+
| `sendStream(message)` | `AsyncGenerator<StreamEvent<TState>>` | Send a message and stream the response as events |
|
|
591
|
+
| `getSessionData()` | `Promise<SessionData<TState> \| null>` | Load current session state without sending a message |
|
|
592
|
+
| `getOrCreateSessionData()` | `Promise<SessionData<TState>>` | Load session or create with initial values if none exists |
|
|
593
|
+
| `upsertSessionData(updates)` | `Promise<void>` | Merge partial updates into the current session |
|
|
594
|
+
| `getHistory()` | `Promise<Message[]>` | Retrieve conversation history |
|
|
595
|
+
| `clear()` | `Promise<void>` | Delete the session from the store |
|
|
596
|
+
|
|
597
|
+
### AgentRegistry Methods
|
|
598
|
+
|
|
599
|
+
| Method | Returns | Description |
|
|
600
|
+
|---|---|---|
|
|
601
|
+
| `getInstance()` | `AgentRegistry<TState>` | Get the singleton instance |
|
|
602
|
+
| `resetInstance()` | `void` | Reset the singleton (for testing) |
|
|
603
|
+
| `register(agents)` | `void` | Register one or more agents (throws on duplicate ID) |
|
|
604
|
+
| `unregister(id)` | `boolean` | Remove an agent by ID |
|
|
605
|
+
| `get(id)` | `Agent<TState> \| undefined` | Look up an agent by ID |
|
|
606
|
+
| `getAll()` | `Agent<TState>[]` | Get all registered agents |
|
|
607
|
+
| `has(id)` | `boolean` | Check if an agent is registered |
|
|
608
|
+
| `getDescriptions()` | `Record<string, string>` | Map of agent IDs to their descriptions |
|
|
609
|
+
| `clear()` | `void` | Remove all registered agents (for testing) |
|
|
610
|
+
| `count` | `number` | Number of registered agents (getter) |
|
|
611
|
+
|
|
612
|
+
### ChatResponse Fields
|
|
613
|
+
|
|
614
|
+
| Field | Type | Description |
|
|
615
|
+
|---|---|---|
|
|
616
|
+
| `responseMessage` | `string` | The agent's text response to the user |
|
|
617
|
+
| `activeAgentId` | `string \| null` | The agent that generated this response |
|
|
618
|
+
| `nextAgentId` | `string \| null` | The agent that will handle the next message |
|
|
619
|
+
| `state` | `TState` | Updated session state after this turn |
|
|
620
|
+
| `goalAchieved` | `boolean` | Whether the agent considers its goal complete |
|
|
621
|
+
| `sessionId` | `string` | Session identifier |
|
|
622
|
+
| `widgets` | `object \| undefined` | Optional widget data for rich UI rendering |
|
|
623
|
+
|
|
624
|
+
## Comparison
|
|
625
|
+
|
|
626
|
+
| Capability | GenUI A3 | LangGraph | CrewAI | AutoGen |
|
|
627
|
+
|---|---|---|---|---|
|
|
628
|
+
| **Setup complexity** | Minimal | Moderate | Moderate | High |
|
|
629
|
+
| **Routing model** | Dynamic (agent-driven) | Static graph | Role-based | Conversation-based |
|
|
630
|
+
| **State management** | Shared global state | Graph state | Shared memory | Message passing |
|
|
631
|
+
| **TypeScript-native** | Yes | Python-first | Python-only | Python-first |
|
|
632
|
+
| **Structured output** | Zod schemas | Custom parsers | Pydantic | Custom parsers |
|
|
633
|
+
| **Session persistence** | Pluggable stores | Custom | Custom | Custom |
|
|
634
|
+
|
|
635
|
+
## Roadmap
|
|
636
|
+
|
|
637
|
+
- **Simplified Agent API** -- reduce agent definitions to just the essentials with sensible defaults
|
|
638
|
+
- **Provider Abstraction** -- first-class support for OpenAI, Anthropic, and custom providers alongside Bedrock
|
|
639
|
+
- **AG-UI Protocol** -- full compliance with the [ag-ui.com](https://ag-ui.com) protocol for standardized agent-to-frontend event streaming
|
|
640
|
+
|
|
641
|
+
## Requirements
|
|
642
|
+
|
|
643
|
+
- Node.js 20.19.0+
|
|
644
|
+
- TypeScript 5.9+
|
|
645
|
+
- `zod` 4.x (included as a dependency)
|
|
646
|
+
- A configured LLM provider (AWS Bedrock provider included)
|
|
647
|
+
|
|
648
|
+
## Contributing
|
|
649
|
+
|
|
650
|
+
```bash
|
|
651
|
+
# Install dependencies
|
|
652
|
+
npm install
|
|
653
|
+
|
|
654
|
+
# Build
|
|
655
|
+
npm run build
|
|
656
|
+
|
|
657
|
+
# Run unit tests
|
|
658
|
+
npm run test:unit
|
|
659
|
+
|
|
660
|
+
# Run integration tests
|
|
661
|
+
npm run test:integration
|
|
662
|
+
|
|
663
|
+
# Lint
|
|
664
|
+
npm run lint
|
|
665
|
+
|
|
666
|
+
# Watch mode (build + example)
|
|
667
|
+
npm run dev
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
## License
|
|
671
|
+
|
|
672
|
+
ISC
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genui-a3/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Core package for the A3 agentic backend framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"example:setup": "cd example && npm i",
|
|
31
31
|
"example:start": "cd example && npm run start",
|
|
32
32
|
"lint": "run-p lint:project lint:markdown lint:example",
|
|
33
|
-
"lint:example": "eslint './example/**/*.{ts,tsx}' --max-warnings=0 --cache --cache-location .eslintcache",
|
|
34
|
-
"lint:markdown": "markdownlint-cli2 '**/*.md'
|
|
35
|
-
"lint:project": "eslint './**/*.{ts,tsx}' --ignore-pattern 'example/**' --max-warnings=0 --cache --cache-location .eslintcache",
|
|
33
|
+
"lint:example": "eslint './example/**/*.{ts,tsx}' './example_agent_core/**/*.{ts,tsx}' --max-warnings=0 --cache --cache-location .eslintcache",
|
|
34
|
+
"lint:markdown": "markdownlint-cli2 '**/*.md'",
|
|
35
|
+
"lint:project": "eslint './**/*.{ts,tsx}' --ignore-pattern 'example/**' --ignore-pattern 'example_agent_core/**' --max-warnings=0 --cache --cache-location .eslintcache",
|
|
36
36
|
"prepublishOnly": "npm run clean && npm run build",
|
|
37
37
|
"test": "run-p test:unit",
|
|
38
38
|
"test:coverage": "run-p test:unit:coverage",
|