@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,36 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { ThemeProvider } from './ThemeProvider'
|
|
3
|
+
import { SidebarLayout } from '@organisms'
|
|
4
|
+
import { APP_TITLE, APP_DESCRIPTION } from '@constants/ui'
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: APP_TITLE,
|
|
8
|
+
description: APP_DESCRIPTION,
|
|
9
|
+
icons: [
|
|
10
|
+
{ rel: 'icon', url: '/favicon.ico', type: 'image/x-icon', sizes: '32x32', media: '(prefers-color-scheme: light)' },
|
|
11
|
+
{
|
|
12
|
+
rel: 'icon',
|
|
13
|
+
url: '/favicon-dark.ico',
|
|
14
|
+
type: 'image/x-icon',
|
|
15
|
+
sizes: '32x32',
|
|
16
|
+
media: '(prefers-color-scheme: dark)',
|
|
17
|
+
},
|
|
18
|
+
{ rel: 'icon', url: '/icon.svg', type: 'image/svg+xml', media: '(prefers-color-scheme: light)' },
|
|
19
|
+
{ rel: 'icon', url: '/icon.svg', type: 'image/svg+xml', media: '(prefers-color-scheme: dark)' },
|
|
20
|
+
{ rel: 'apple-touch-icon', url: '/apple-icon.png', media: '(prefers-color-scheme: light)' },
|
|
21
|
+
{ rel: 'apple-touch-icon', url: '/apple-icon-dark.png', media: '(prefers-color-scheme: dark)' },
|
|
22
|
+
],
|
|
23
|
+
manifest: '/site.webmanifest',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
27
|
+
return (
|
|
28
|
+
<html lang="en">
|
|
29
|
+
<body>
|
|
30
|
+
<ThemeProvider>
|
|
31
|
+
<SidebarLayout>{children}</SidebarLayout>
|
|
32
|
+
</ThemeProvider>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { getChatSessionInstance } from '@agents'
|
|
4
|
+
import { initRegistry } from '@agents/registry'
|
|
5
|
+
|
|
6
|
+
export async function restartSession(sessionId: string) {
|
|
7
|
+
initRegistry()
|
|
8
|
+
const session = getChatSessionInstance({ sessionId })
|
|
9
|
+
return await session.restart()
|
|
10
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { agentRegistry } from '@agents/registry'
|
|
2
|
+
import { getTransitionTargetMap } from './parseTransitionTargets'
|
|
3
|
+
|
|
4
|
+
export type AgentInfo = {
|
|
5
|
+
id: string
|
|
6
|
+
description: string
|
|
7
|
+
transition: { type: 'deterministic' | 'dynamic' | 'none'; targets: string[] }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns registered agents with transition metadata for the AgentGraph visualization component.
|
|
12
|
+
* Caller is responsible for ensuring agents are registered before calling this function.
|
|
13
|
+
*
|
|
14
|
+
* For deterministic transitions (functions), uses AST parsing to discover the actual
|
|
15
|
+
* target agent IDs from the source code rather than listing all other agents.
|
|
16
|
+
*/
|
|
17
|
+
export function getAgentGraphData(): AgentInfo[] {
|
|
18
|
+
const agents = agentRegistry.getAll()
|
|
19
|
+
const allAgentIds = agents.map((a) => a.id)
|
|
20
|
+
const targetMap = getTransitionTargetMap()
|
|
21
|
+
|
|
22
|
+
return agents.map((agent) => {
|
|
23
|
+
let type: 'deterministic' | 'dynamic' | 'none'
|
|
24
|
+
let targets: string[]
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(agent.transition)) {
|
|
27
|
+
type = 'dynamic'
|
|
28
|
+
targets = agent.transition
|
|
29
|
+
} else if (typeof agent.transition === 'function') {
|
|
30
|
+
type = 'deterministic'
|
|
31
|
+
targets = targetMap.get(agent.id) ?? allAgentIds.filter((id) => id !== agent.id)
|
|
32
|
+
} else {
|
|
33
|
+
type = 'none'
|
|
34
|
+
targets = []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id: agent.id,
|
|
39
|
+
description: agent.description,
|
|
40
|
+
transition: { type, targets },
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import dagre from '@dagrejs/dagre'
|
|
2
|
+
import { MarkerType, type Node, type Edge } from '@xyflow/react'
|
|
3
|
+
import type { AgentInfo } from './getAgentGraphData'
|
|
4
|
+
|
|
5
|
+
const NODE_WIDTH = 160
|
|
6
|
+
const NODE_HEIGHT = 44
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compute a top-to-bottom dagre layout for the agent graph and return
|
|
10
|
+
* React Flow–compatible nodes and edges.
|
|
11
|
+
*
|
|
12
|
+
* Self-loop edges are excluded from dagre (which expects a DAG) and added
|
|
13
|
+
* separately with a custom `selfLoop` edge type.
|
|
14
|
+
*/
|
|
15
|
+
export function getGraphLayout(agents: AgentInfo[], activeAgentId: string | null): { nodes: Node[]; edges: Edge[] } {
|
|
16
|
+
const g = new dagre.graphlib.Graph()
|
|
17
|
+
g.setDefaultEdgeLabel(() => ({}))
|
|
18
|
+
g.setGraph({ rankdir: 'TB', ranksep: 60, nodesep: 40 })
|
|
19
|
+
|
|
20
|
+
for (const agent of agents) {
|
|
21
|
+
g.setNode(agent.id, { width: NODE_WIDTH, height: NODE_HEIGHT })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const agent of agents) {
|
|
25
|
+
for (const target of agent.transition.targets) {
|
|
26
|
+
if (target !== agent.id) {
|
|
27
|
+
g.setEdge(agent.id, target)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dagre.layout(g)
|
|
33
|
+
|
|
34
|
+
const resolvedActiveId = activeAgentId ?? agents[0]?.id ?? null
|
|
35
|
+
|
|
36
|
+
const nodes: Node[] = agents.map((agent) => {
|
|
37
|
+
const { x, y } = g.node(agent.id) as unknown as { x: number; y: number }
|
|
38
|
+
return {
|
|
39
|
+
id: agent.id,
|
|
40
|
+
position: { x: x - NODE_WIDTH / 2, y: y - NODE_HEIGHT / 2 },
|
|
41
|
+
data: {
|
|
42
|
+
label: agent.id,
|
|
43
|
+
isActive: agent.id === resolvedActiveId,
|
|
44
|
+
description: agent.description,
|
|
45
|
+
},
|
|
46
|
+
type: 'agent',
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const positionOf = new Map<string, { x: number; y: number }>()
|
|
51
|
+
for (const agent of agents) {
|
|
52
|
+
const { x, y } = g.node(agent.id) as unknown as { x: number; y: number }
|
|
53
|
+
positionOf.set(agent.id, { x, y })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const edges: Edge[] = []
|
|
57
|
+
for (const agent of agents) {
|
|
58
|
+
const isDynamic = agent.transition.type === 'dynamic'
|
|
59
|
+
const strokeColor = isDynamic ? '#94a3b8' : '#64748b'
|
|
60
|
+
|
|
61
|
+
for (const target of agent.transition.targets) {
|
|
62
|
+
const marker = {
|
|
63
|
+
type: MarkerType.ArrowClosed,
|
|
64
|
+
color: strokeColor,
|
|
65
|
+
width: 15,
|
|
66
|
+
height: 15,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (target === agent.id) {
|
|
70
|
+
edges.push({
|
|
71
|
+
id: `${agent.id}-${target}`,
|
|
72
|
+
source: agent.id,
|
|
73
|
+
target,
|
|
74
|
+
type: 'selfLoop',
|
|
75
|
+
data: { isDynamic },
|
|
76
|
+
markerEnd: marker,
|
|
77
|
+
sourceHandle: 'source-bottom',
|
|
78
|
+
targetHandle: 'target-top',
|
|
79
|
+
})
|
|
80
|
+
} else {
|
|
81
|
+
const sourceY = positionOf.get(agent.id)!.y
|
|
82
|
+
const targetY = positionOf.get(target)!.y
|
|
83
|
+
const isBackEdge = sourceY >= targetY
|
|
84
|
+
|
|
85
|
+
edges.push({
|
|
86
|
+
id: `${agent.id}-${target}`,
|
|
87
|
+
source: agent.id,
|
|
88
|
+
target,
|
|
89
|
+
type: isDynamic ? 'dynamic' : 'deterministic',
|
|
90
|
+
markerEnd: marker,
|
|
91
|
+
sourceHandle: isBackEdge ? 'source-right' : 'source-bottom',
|
|
92
|
+
targetHandle: isBackEdge ? 'target-right' : 'target-top',
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { nodes, edges }
|
|
99
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
import type { Dispatch, SetStateAction } from 'react'
|
|
3
|
+
import type { Message } from '@genui/a3'
|
|
4
|
+
|
|
5
|
+
export interface RestartResult {
|
|
6
|
+
messages: Message[]
|
|
7
|
+
activeAgentId: string | null
|
|
8
|
+
state: Record<string, unknown>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface UseRestartOptions {
|
|
12
|
+
onRestart?: () => Promise<RestartResult>
|
|
13
|
+
setMessages: Dispatch<SetStateAction<Message[]>>
|
|
14
|
+
onSessionUpdate?: (update: { activeAgentId: string | null; state: Record<string, unknown> }) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useRestart({ onRestart, setMessages, onSessionUpdate }: UseRestartOptions) {
|
|
18
|
+
const [isRestarting, setIsRestarting] = useState(false)
|
|
19
|
+
|
|
20
|
+
const handleRestart = useCallback(async () => {
|
|
21
|
+
if (!onRestart) return
|
|
22
|
+
setIsRestarting(true)
|
|
23
|
+
try {
|
|
24
|
+
const fresh = await onRestart()
|
|
25
|
+
setMessages(fresh.messages)
|
|
26
|
+
onSessionUpdate?.({ activeAgentId: fresh.activeAgentId, state: fresh.state })
|
|
27
|
+
} finally {
|
|
28
|
+
setIsRestarting(false)
|
|
29
|
+
}
|
|
30
|
+
}, [onRestart, setMessages, onSessionUpdate])
|
|
31
|
+
|
|
32
|
+
return { isRestarting, handleRestart: onRestart ? handleRestart : undefined }
|
|
33
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import * as fs from 'fs'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
|
|
5
|
+
const SKIP_FILES = new Set(['state.ts', 'registry.ts', 'index.ts'])
|
|
6
|
+
|
|
7
|
+
let cached: Map<string, string[]> | null = null
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse agent source files to extract transition targets using TypeScript's AST.
|
|
11
|
+
* Results are cached for the server lifecycle.
|
|
12
|
+
*
|
|
13
|
+
* For array transitions, extracts string elements directly.
|
|
14
|
+
* For function transitions, walks the function body to collect string literals
|
|
15
|
+
* from return statements and ternary expressions, then cross-references against
|
|
16
|
+
* known agent IDs to filter out non-agent strings.
|
|
17
|
+
*
|
|
18
|
+
* @returns Map from agent ID to transition target agent IDs
|
|
19
|
+
*/
|
|
20
|
+
export function getTransitionTargetMap(): Map<string, string[]> {
|
|
21
|
+
if (cached) return cached
|
|
22
|
+
cached = new Map()
|
|
23
|
+
|
|
24
|
+
const agentsDir = path.join(process.cwd(), 'app/agents')
|
|
25
|
+
let entries: fs.Dirent[]
|
|
26
|
+
try {
|
|
27
|
+
entries = fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
28
|
+
} catch {
|
|
29
|
+
return cached
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const fileNames = entries
|
|
33
|
+
.filter((e) => e.isFile() && e.name.endsWith('.ts') && !SKIP_FILES.has(e.name))
|
|
34
|
+
.map((e) => e.name)
|
|
35
|
+
|
|
36
|
+
// First pass: parse all files and collect agent IDs
|
|
37
|
+
const fileData: { agentId: string; transitionNode: ts.Node | null }[] = []
|
|
38
|
+
const allAgentIds = new Set<string>()
|
|
39
|
+
|
|
40
|
+
for (const fileName of fileNames) {
|
|
41
|
+
try {
|
|
42
|
+
const source = fs.readFileSync(path.join(agentsDir, fileName), 'utf-8')
|
|
43
|
+
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true)
|
|
44
|
+
const info = extractAgentInfo(sf)
|
|
45
|
+
if (info) {
|
|
46
|
+
allAgentIds.add(info.agentId)
|
|
47
|
+
fileData.push(info)
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Skip unparseable files
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Second pass: extract transition targets and cross-reference against known IDs
|
|
55
|
+
for (const { agentId, transitionNode } of fileData) {
|
|
56
|
+
if (!transitionNode) continue
|
|
57
|
+
const targets = extractTargets(transitionNode, allAgentIds)
|
|
58
|
+
if (targets.length > 0) {
|
|
59
|
+
cached.set(agentId, targets)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return cached
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractAgentInfo(
|
|
67
|
+
sf: ts.SourceFile
|
|
68
|
+
): { agentId: string; transitionNode: ts.Node | null } | null {
|
|
69
|
+
let agentId: string | null = null
|
|
70
|
+
let transitionNode: ts.Node | null = null
|
|
71
|
+
|
|
72
|
+
function visit(node: ts.Node) {
|
|
73
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
74
|
+
let id: string | null = null
|
|
75
|
+
let transition: ts.Node | null = null
|
|
76
|
+
|
|
77
|
+
for (const prop of node.properties) {
|
|
78
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue
|
|
79
|
+
if (prop.name.text === 'id' && ts.isStringLiteral(prop.initializer)) {
|
|
80
|
+
id = prop.initializer.text
|
|
81
|
+
}
|
|
82
|
+
if (prop.name.text === 'transition') {
|
|
83
|
+
transition = prop.initializer
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (id) {
|
|
88
|
+
agentId = id
|
|
89
|
+
transitionNode = transition
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
ts.forEachChild(node, visit)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ts.forEachChild(sf, visit)
|
|
96
|
+
return agentId ? { agentId, transitionNode } : null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function extractTargets(node: ts.Node, allAgentIds: Set<string>): string[] {
|
|
100
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
101
|
+
return node.elements
|
|
102
|
+
.filter(ts.isStringLiteral)
|
|
103
|
+
.map((el) => el.text)
|
|
104
|
+
.filter((text) => allAgentIds.has(text))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {
|
|
108
|
+
const strings = new Set<string>()
|
|
109
|
+
if (ts.isBlock(node.body)) {
|
|
110
|
+
collectReturnStrings(node.body, strings)
|
|
111
|
+
} else {
|
|
112
|
+
// Expression body (arrow function without braces)
|
|
113
|
+
collectStringLiterals(node.body, strings)
|
|
114
|
+
}
|
|
115
|
+
return Array.from(strings).filter((s) => allAgentIds.has(s))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return []
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function collectReturnStrings(node: ts.Node, strings: Set<string>) {
|
|
122
|
+
if (ts.isReturnStatement(node) && node.expression) {
|
|
123
|
+
collectStringLiterals(node.expression, strings)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
ts.forEachChild(node, (child) => collectReturnStrings(child, strings))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function collectStringLiterals(node: ts.Node, strings: Set<string>) {
|
|
130
|
+
if (ts.isStringLiteral(node)) {
|
|
131
|
+
strings.add(node.text)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (ts.isConditionalExpression(node)) {
|
|
135
|
+
collectStringLiterals(node.whenTrue, strings)
|
|
136
|
+
collectStringLiterals(node.whenFalse, strings)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
ts.forEachChild(node, (child) => collectStringLiterals(child, strings))
|
|
140
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createAnthropicProvider } from '@genui/a3-anthropic'
|
|
2
|
+
|
|
3
|
+
let _instance: ReturnType<typeof createAnthropicProvider> | null = null
|
|
4
|
+
|
|
5
|
+
export function getAnthropicProvider() {
|
|
6
|
+
if (!_instance) {
|
|
7
|
+
_instance = createAnthropicProvider({
|
|
8
|
+
models: ['claude-sonnet-4-6', 'claude-haiku-4-5-20251001'],
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
return _instance
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createBedrockProvider } from '@genui/a3-bedrock'
|
|
2
|
+
|
|
3
|
+
let _instance: ReturnType<typeof createBedrockProvider> | null = null
|
|
4
|
+
|
|
5
|
+
export function getBedrockProvider() {
|
|
6
|
+
if (!_instance) {
|
|
7
|
+
_instance = createBedrockProvider({
|
|
8
|
+
models: ['us.anthropic.claude-sonnet-4-5-20250929-v1:0', 'us.anthropic.claude-haiku-4-5-20251001-v1:0'],
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
return _instance
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createOpenAIProvider } from '@genui/a3-openai'
|
|
2
|
+
|
|
3
|
+
let _instance: ReturnType<typeof createOpenAIProvider> | null = null
|
|
4
|
+
|
|
5
|
+
export function getOpenAIProvider() {
|
|
6
|
+
if (!_instance) {
|
|
7
|
+
_instance = createOpenAIProvider({ models: ['gpt-4o', 'gpt-4o-mini'] })
|
|
8
|
+
}
|
|
9
|
+
return _instance
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Box, Typography } from '@mui/material'
|
|
2
|
+
import { OnboardingChat } from '@organisms'
|
|
3
|
+
import { getChatSessionInstance } from '@agents'
|
|
4
|
+
import { SESSION_IDS } from '@constants/chat'
|
|
5
|
+
import { ONBOARDING_TAGLINE } from '@constants/ui'
|
|
6
|
+
|
|
7
|
+
export default async function OnboardingPage() {
|
|
8
|
+
const session = getChatSessionInstance({ sessionId: SESSION_IDS.ONBOARDING, initialAgentId: 'onboarding' })
|
|
9
|
+
const sessionData = await session.getOrCreateSessionData()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<Box sx={{ px: 3, py: 3, textAlign: 'center' }}>
|
|
14
|
+
<Typography variant="body1" color="text.secondary" dangerouslySetInnerHTML={{ __html: ONBOARDING_TAGLINE }} />
|
|
15
|
+
</Box>
|
|
16
|
+
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', pb: 3, px: { xs: 2, sm: 3, md: 5, lg: 8 } }}>
|
|
17
|
+
<OnboardingChat sessionId={SESSION_IDS.ONBOARDING} initialMessages={sessionData.messages} />
|
|
18
|
+
</Box>
|
|
19
|
+
</>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { Box, Container } from '@mui/material'
|
|
4
|
+
import { MarkdownRenderer } from '@atoms'
|
|
5
|
+
|
|
6
|
+
export default function Home() {
|
|
7
|
+
const readme = readFileSync(join(process.cwd(), 'docs/A3-README.md'), 'utf-8')
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Container maxWidth="md" sx={{ py: 4 }}>
|
|
11
|
+
<Box sx={{ px: { xs: 1, sm: 2 } }}>
|
|
12
|
+
<MarkdownRenderer content={readme} />
|
|
13
|
+
</Box>
|
|
14
|
+
</Container>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createTheme } from '@mui/material/styles'
|
|
2
|
+
|
|
3
|
+
export const theme = createTheme({
|
|
4
|
+
palette: {
|
|
5
|
+
primary: {
|
|
6
|
+
main: '#2563eb',
|
|
7
|
+
},
|
|
8
|
+
background: {
|
|
9
|
+
default: '#f9fafb',
|
|
10
|
+
paper: '#ffffff',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
components: {
|
|
14
|
+
MuiCssBaseline: {
|
|
15
|
+
styleOverrides: {
|
|
16
|
+
html: { maxWidth: '100vw', overflowX: 'hidden' },
|
|
17
|
+
body: { maxWidth: '100vw', overflowX: 'hidden' },
|
|
18
|
+
a: { color: 'inherit', textDecoration: 'none' },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @genui/a3
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@genui/a3)
|
|
4
|
+
[](https://github.com/generalui/a3/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
**Predictable, governable multi-agent orchestration for TypeScript.**
|
|
7
|
+
|
|
8
|
+
Production agentic systems need more than prompt chains: They need
|
|
9
|
+
deterministic guardrails, structured output, and routing you can reason about.
|
|
10
|
+
|
|
11
|
+
A3 combines flexible LLM reasoning with hard-coded control:
|
|
12
|
+
Zod-validated responses, deterministic or LLM-driven routing, and shared typed state.
|
|
13
|
+
No graphs. No state machines. Just agents and code.
|
|
14
|
+
|
|
15
|
+
## Feature Highlights
|
|
16
|
+
|
|
17
|
+
- **Deterministic + LLM-driven routing** -- code-controlled transitions or let the LLM pick from a bounded set
|
|
18
|
+
- **Structured output** -- Zod schemas validate every LLM response at runtime
|
|
19
|
+
- **Multi-agent orchestration** -- agents hand off to each other with shared typed state
|
|
20
|
+
- **Streaming** -- real-time token streaming with AG-UI-compatible events
|
|
21
|
+
- **Pluggable providers** -- Bedrock, OpenAI, Anthropic; or [build your own](./docs/CUSTOM_PROVIDERS.md)
|
|
22
|
+
- **Pluggable session stores** -- swap in-memory, Redis, or your own persistence
|
|
23
|
+
- **TypeScript-native** -- full type safety from agent definitions to response handling
|
|
24
|
+
- **Dual ESM/CJS** -- works in any Node.js environment
|
|
25
|
+
|
|
26
|
+
## Why A3?
|
|
27
|
+
|
|
28
|
+
Most agentic frameworks optimize for demos, not production.
|
|
29
|
+
Prompt-only agents break when inputs drift.
|
|
30
|
+
Graph-based orchestration becomes unmaintainable at scale.
|
|
31
|
+
None of them give you compile-time safety or runtime validation out of the box.
|
|
32
|
+
|
|
33
|
+
A3's approach: **define agents, register them, and let the framework handle routing —
|
|
34
|
+
with guardrails at every layer.**
|
|
35
|
+
|
|
36
|
+
- **Every response validated:** Zod schemas enforce structure at runtime, not just in types
|
|
37
|
+
- **Routing you can reason about:** deterministic transitions via code, or bounded LLM-driven selection
|
|
38
|
+
- **Typed shared state:** one state object flows across agents with full TypeScript safety
|
|
39
|
+
- **Swap providers, not code:** Bedrock, OpenAI, Anthropic; switch with one line
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
Scaffold a new project:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Fastest way to start — scaffolds a full Next.js app
|
|
47
|
+
npx @genui/a3-create@latest my-app
|
|
48
|
+
cd my-app
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or add A3 to an existing project:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install @genui/a3 @genui/a3-bedrock zod
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { z } from 'zod'
|
|
59
|
+
import { Agent, AgentRegistry, BaseState, ChatSession, MemorySessionStore } from '@genui/a3'
|
|
60
|
+
import { createBedrockProvider } from '@genui/a3-bedrock'
|
|
61
|
+
|
|
62
|
+
interface MyState extends BaseState { userName?: string }
|
|
63
|
+
|
|
64
|
+
const agent: Agent<MyState> = {
|
|
65
|
+
id: 'greeter',
|
|
66
|
+
description: 'Greets the user and collects their name',
|
|
67
|
+
prompt: 'You are a friendly greeting agent. Ask for the user\'s name.',
|
|
68
|
+
outputSchema: z.object({ userName: z.string().optional() }),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
AgentRegistry.getInstance<MyState>().register(agent)
|
|
72
|
+
|
|
73
|
+
const session = new ChatSession<MyState>({
|
|
74
|
+
sessionId: 'demo',
|
|
75
|
+
store: new MemorySessionStore(),
|
|
76
|
+
initialAgentId: 'greeter',
|
|
77
|
+
initialState: { userName: undefined },
|
|
78
|
+
provider: createBedrockProvider({ models: ['us.anthropic.claude-sonnet-4-5-20250929-v1:0'] }),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const result = await session.send({ message: 'Hello!' })
|
|
82
|
+
console.log(result.responseMessage)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
See more examples in the [Quick Start & Examples guide](./docs/QUICK-START-EXAMPLES.md).
|
|
86
|
+
|
|
87
|
+
## Documentation
|
|
88
|
+
|
|
89
|
+
- [Core Concepts](./docs/CORE-CONCEPTS.md) -- agents, routing, state, schemas, streaming, providers, stores
|
|
90
|
+
- [Architecture](./docs/ARCHITECTURE.md) -- system diagram and request flow
|
|
91
|
+
- [API Reference](./docs/API-REFERENCE.md) -- exports, methods, and response fields
|
|
92
|
+
- [Quick Start & Examples](./docs/QUICK-START-EXAMPLES.md) -- single-agent and multi-agent walkthroughs
|
|
93
|
+
- [Providers](./docs/PROVIDERS.md) -- configuring Bedrock, OpenAI, and Anthropic
|
|
94
|
+
- [Custom Providers](./docs/CUSTOM_PROVIDERS.md) -- building your own provider
|
|
95
|
+
- [Resilience](./docs/RESILIENCE.md) -- retries, timeouts, and model fallback
|
|
96
|
+
- [Custom Logging](./docs/CUSTOM_LOGGING.md) -- plugging in your own logger
|
|
97
|
+
- [Custom Stores](./docs/CUSTOM_STORES.md) -- implementing your own session store
|
|
98
|
+
|
|
99
|
+
## Roadmap
|
|
100
|
+
|
|
101
|
+
- **Tool use** -- agent-invoked tool execution within the response cycle
|
|
102
|
+
|
|
103
|
+
## Requirements
|
|
104
|
+
|
|
105
|
+
- Node.js 20.19.0+
|
|
106
|
+
- TypeScript 5.9+
|
|
107
|
+
- `zod` 4.x (included as a dependency)
|
|
108
|
+
- A configured LLM provider ([Bedrock, OpenAI, or Anthropic](./docs/PROVIDERS.md))
|
|
109
|
+
|
|
110
|
+
## Contributing
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm install # Install dependencies
|
|
114
|
+
npm run build # Build
|
|
115
|
+
npm run test:unit # Run unit tests
|
|
116
|
+
npm run lint # Lint
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## Core Exports
|
|
4
|
+
|
|
5
|
+
| Export | Type | Description |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `ChatSession` | Class | Primary interface for sending messages and managing conversations |
|
|
8
|
+
| `AgentRegistry` | Class | Singleton registry for agent registration and lookup |
|
|
9
|
+
| `AGUIAgent` | Class | AG-UI protocol integration for standardized agent-to-frontend streaming |
|
|
10
|
+
| `simpleAgentResponse` | Function | Default blocking response generator for agents |
|
|
11
|
+
| `simpleAgentResponseStream` | Function | Default streaming response generator for agents |
|
|
12
|
+
| `getAgentResponse` | Function | Low-level blocking agent response pipeline (prompt, schema, LLM call, validation) |
|
|
13
|
+
| `getAgentResponseStream` | Function | Low-level streaming agent response pipeline |
|
|
14
|
+
| `manageFlow` | Function | Blocking chat flow orchestration with automatic agent chaining |
|
|
15
|
+
| `manageFlowStream` | Function | Streaming variant of `manageFlow` yielding `StreamEvent`s |
|
|
16
|
+
| `createFullOutputSchema` | Function | Merges agent schema with base response fields |
|
|
17
|
+
| `MemorySessionStore` | Class | In-memory session store for development and testing |
|
|
18
|
+
|
|
19
|
+
## ChatSession Methods
|
|
20
|
+
|
|
21
|
+
| Method | Returns | Description |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `send({ message })` | `Promise<ChatResponse<TState>>` | Send a user message and get the agent's response (blocking) |
|
|
24
|
+
| `send({ message, stream: true })` | `AsyncGenerator<StreamEvent<TState>>` | Send a message and stream the response as events |
|
|
25
|
+
| `getSessionData()` | `Promise<SessionData<TState> \| null>` | Load current session state without sending a message |
|
|
26
|
+
| `getOrCreateSessionData()` | `Promise<SessionData<TState>>` | Load session or create with initial values if none exists |
|
|
27
|
+
| `upsertSessionData(updates)` | `Promise<void>` | Merge partial updates into the current session |
|
|
28
|
+
| `getHistory()` | `Promise<Message[]>` | Retrieve conversation history |
|
|
29
|
+
| `clear()` | `Promise<void>` | Delete the session from the store |
|
|
30
|
+
|
|
31
|
+
## AgentRegistry Methods
|
|
32
|
+
|
|
33
|
+
| Method | Returns | Description |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `getInstance()` | `AgentRegistry<TState>` | Get the singleton instance |
|
|
36
|
+
| `resetInstance()` | `void` | Reset the singleton (for testing) |
|
|
37
|
+
| `register(agents)` | `void` | Register one or more agents (throws on duplicate ID) |
|
|
38
|
+
| `unregister(agentOrId)` | `boolean` | Remove an agent by ID or agent instance |
|
|
39
|
+
| `get(id)` | `Agent<TState> \| undefined` | Look up an agent by ID |
|
|
40
|
+
| `getAll()` | `Agent<TState>[]` | Get all registered agents |
|
|
41
|
+
| `has(id)` | `boolean` | Check if an agent is registered |
|
|
42
|
+
| `getDescriptions()` | `Record<string, string>` | Map of agent IDs to their descriptions |
|
|
43
|
+
| `clear()` | `void` | Remove all registered agents (for testing) |
|
|
44
|
+
| `count` | `number` | Number of registered agents (getter) |
|
|
45
|
+
|
|
46
|
+
## ChatResponse Fields
|
|
47
|
+
|
|
48
|
+
| Field | Type | Description |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `responseMessage` | `string` | The agent's text response to the user |
|
|
51
|
+
| `activeAgentId` | `string \| null` | The agent that generated this response |
|
|
52
|
+
| `nextAgentId` | `string \| null` | The agent that will handle the next message |
|
|
53
|
+
| `state` | `TState` | Updated session state after this turn |
|
|
54
|
+
| `goalAchieved` | `boolean` | Whether the agent considers its goal complete |
|
|
55
|
+
| `sessionId` | `string` | Session identifier |
|
|
56
|
+
| `widgets` | `object \| undefined` | Optional widget data for rich UI rendering |
|
|
57
|
+
|
|
58
|
+
## ChatSessionConfig
|
|
59
|
+
|
|
60
|
+
| Property | Type | Default | Description |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| `sessionId` | `string` | *required* | Unique session identifier |
|
|
63
|
+
| `store` | `SessionStore` | `undefined` | Storage adapter for session persistence |
|
|
64
|
+
| `initialAgentId` | `AgentId` | *required* | Agent to start the conversation |
|
|
65
|
+
| `initialState` | `TState` | `undefined` | Initial state for new sessions |
|
|
66
|
+
| `initialChatContext` | `TContext` | `undefined` | Initial chat context for new sessions |
|
|
67
|
+
| `initialMessages` | `Message[]` | `undefined` | Pre-populated conversation messages |
|
|
68
|
+
| `provider` | `Provider` | *required* | LLM provider instance for this session |
|
|
69
|
+
| `agentRecursionLimit` | `number` | `10` | Maximum automatic agent transitions per `send()` call. See [Transitions — Recursion Limit](./TRANSITIONS.md#recursion-limit) |
|
|
70
|
+
|
|
71
|
+
## Agent Properties
|
|
72
|
+
|
|
73
|
+
| Property | Type | Required | Description |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| `id` | `AgentId` | Yes | Unique identifier for the agent |
|
|
76
|
+
| `description` | `string` | Yes | What this agent does (used in agent pool prompts) |
|
|
77
|
+
| `name` | `string` | No | Human-readable display name |
|
|
78
|
+
| `prompt` | `string \| (input) => Promise<string>` | Yes | System prompt string or async function returning the prompt |
|
|
79
|
+
| `outputSchema` | `ZodObject \| (sessionData) => ZodObject` | Yes | Zod schema defining structured data to extract from LLM responses |
|
|
80
|
+
| `provider` | `Provider` | No | Per-agent provider override; falls back to session-level provider |
|
|
81
|
+
| `generateResponse` | `(input) => Promise \| AsyncGenerator` | No | Custom response generator. See [Custom generateResponse](./CORE-CONCEPTS.md#custom-generateresponse) |
|
|
82
|
+
| `setState` | `(data, state) => TState` | No | Maps extracted LLM data into session state (defaults to shallow merge) |
|
|
83
|
+
| `transition` | `AgentId[] \| (state, goalAchieved) => AgentId` | No | Routing config. **Array**: LLM picks from listed IDs. **Function**: code decides. **Omitted**: LLM unconstrained. See [Transitions](./TRANSITIONS.md) |
|
|
84
|
+
| `filterHistoryStrategy` | `(messages) => Conversation` | No | Custom function to filter conversation history before sending to the LLM |
|
|
85
|
+
| `widgets` | `Record<string, ZodObject> \| (sessionData) => Record` | No | Zod schemas defining widgets available to the agent |
|