Overview
The AG-UI Protocol is a sophisticated multi-agent orchestration pattern designed for complex workflows that require coordinated agent collaboration, shared state management, and human-in-the-loop approvals. Built on the @ag-ui/client library, it provides bidirectional state synchronization between frontend and backend agent systems.
When to Use AG-UI Protocol
Complex agent workflows requiring multiple steps
Multi-agent coordination and collaboration
Shared state across components and sessions
Tool execution with human approval gates
Draft systems requiring review before execution
Not Recommended For
Simple single-message requests (use CopilotKit instead)
Stateless operations (use SSE Streaming)
Single-turn Q&A interactions
Architecture
The AG-UI Protocol implements a sophisticated state management architecture using Zustand stores with bidirectional synchronization between frontend and backend.
State Flow
User Action : User interacts with React component
Local Update : Zustand store updates immediately (optimistic)
Backend Sync : HttpAgent sends state to LangGraph backend
Agent Processing : Backend agent processes with full state context
State Broadcast : Backend pushes state updates via Observable stream
Store Reconciliation : Frontend stores merge backend state changes
State Management
The AG-UI Protocol manages 7 core state components that provide comprehensive context for agent operations.
1. Conversation State
Tracks the complete message history and conversation flow.
interface ConversationState {
messages : Array <{
id : string ;
role : 'user' | 'assistant' | 'system' ;
content : string ;
timestamp : string ;
tool_calls ?: ToolCall [];
}>;
activeMessageId : string | null ;
isStreaming : boolean ;
}
2. Workspace Context
Provides awareness of the user’s current workspace and focus.
interface WorkspaceContext {
activeSheet ?: string | null ;
activeDashboard ?: string | null ;
activePipeline ?: string | null ;
focusMode ?: 'sheet' | 'dashboard' | 'pipeline' | null ;
selectedCells ?: string [];
currentPage ?: string ;
}
3. Draft System
Manages proposed actions requiring human approval before execution.
interface AgentDraft {
id : string ;
type : 'formula' | 'validation' | 'workflow' | 'data_import' ;
title : string ;
description : string ;
status : 'pending' | 'approved' | 'rejected' | 'applied' ;
confidence : number ;
preview ?: {
before ?: any ;
after ?: any ;
affectedCells ?: string [];
};
actions : DraftAction [];
createdAt : string ;
expiresAt ?: string ;
}
Tracks tool calls and their execution status.
interface ToolExecutionState {
pendingTools : string [];
toolResults : Record < string , {
success : boolean ;
data ?: any ;
error ?: string ;
timestamp : string ;
}>;
activeToolCalls : Map < string , ToolCall >;
}
Maintains session-level information and configuration.
interface SessionMetadata {
sessionId : string ;
userId : string ;
activeAgent : AgentId ;
startTime : string ;
lastActivity : string ;
errorCount : number ;
lastError ?: string | null ;
}
6. Data Collections
Stores summaries of available resources and datasets.
interface DataCollections {
dashboards : DashboardSummary [];
workflows : WorkflowSummary [];
pipelines : PipelineSummary [];
datasets : DatasetSummary [];
}
7. Insights & Analytics
Captures agent-generated insights and recommendations.
interface AgentInsight {
id : string ;
type : 'suggestion' | 'warning' | 'error' | 'info' ;
category : string ;
title : string ;
description : string ;
actionable : boolean ;
suggestedAction ?: string ;
confidence : number ;
source : string ;
createdAt : string ;
}
Backend Setup
1. Install Dependencies
npm install @ag-ui/client @ag-ui/core
Already included in OpsHub’s package.json:
{
"dependencies" : {
"@ag-ui/client" : "^0.0.40"
}
}
2. Create Multi-Agent Client
The MultiAgentClient class wraps the AG-UI HttpAgent and provides multi-agent orchestration:
// lib/agent/multi-agent-client.ts
import { HttpAgent , type RunAgentParameters } from '@ag-ui/client' ;
import type { Message , RunAgentInput , BaseEvent } from '@ag-ui/core' ;
export class MultiAgentClient {
private baseUrl : string ;
private activeAgentId : AgentId ;
private httpAgent : HttpAgent | null = null ;
private threadId : string ;
private userId : string ;
constructor ( config : MultiAgentConfig = {}) {
this . baseUrl = config . baseUrl || 'http://localhost:8000' ;
this . threadId = config . threadId || 'default-session' ;
this . userId = config . userId || 'default-user' ;
this . activeAgentId = config . defaultAgent || 'app' ;
}
getHttpAgent () : HttpAgent {
if ( ! this . httpAgent ) {
this . httpAgent = new HttpAgent ({
url: ` ${ this . baseUrl } /agent` ,
threadId: this . threadId ,
debug: process . env . NODE_ENV === 'development' ,
});
}
return this . httpAgent ;
}
async sendMessage ( content : string , metadata ?: Record < string , unknown >) {
const agent = this . getHttpAgent ();
const userMessage : Message = {
id: `user- ${ Date . now () } ` ,
role: 'user' ,
content ,
};
agent . addMessage ( userMessage );
// Convert metadata to AG-UI context format
const contextArray : Array <{ value : string ; description : string }> = [
{ value: this . activeAgentId , description: 'activeAgentId' },
{ value: this . userId , description: 'userId' },
];
if ( metadata ) {
for ( const [ key , value ] of Object . entries ( metadata )) {
contextArray . push ({
value: String ( value ),
description: key ,
});
}
}
const runParams : RunAgentParameters = { context: contextArray };
return agent . runAgent ( runParams );
}
}
3. Backend Agent Configuration
Configure your FastAPI backend to work with AG-UI Protocol:
# Python backend (FastAPI + LangGraph)
from fastapi import FastAPI
from langgraph.graph import StateGraph
from typing import TypedDict, Annotated
class AgentState ( TypedDict ):
messages: list[ dict ]
workspace: dict
drafts: list[ dict ]
insights: list[ dict ]
pendingTools: list[ str ]
toolResults: dict
sessionId: str
userId: str
activeAgent: str
app = FastAPI()
# LangGraph workflow
workflow = StateGraph(AgentState)
@app.post ( "/agent" )
async def agent_endpoint ( request : dict ):
# AG-UI client sends requests here
thread_id = request.get( "threadId" )
messages = request.get( "messages" , [])
context = request.get( "context" , [])
# Process with LangGraph
result = await workflow.ainvoke({
"messages" : messages,
"sessionId" : thread_id,
# ... other state
})
return result
Frontend Implementation
1. Zustand Store Setup
Create a comprehensive Zustand store with state management:
// lib/stores/agentStore.ts
import { create } from 'zustand' ;
import { subscribeWithSelector } from 'zustand/middleware' ;
import { immer } from 'zustand/middleware/immer' ;
interface AgentState {
// Core agent state
activeAgent : AgentId ;
sessionId : string | null ;
error : string | null ;
// Workspace context
workspace : AgentWorkspaceContext ;
// Data collections
dashboards : DashboardSummary [];
workflows : WorkflowSummary [];
pipelines : PipelineSummary [];
datasets : DatasetSummary [];
drafts : AgentDraft [];
insights : AgentInsight [];
// Actions
setActiveAgent : ( agentId : AgentId ) => void ;
updateWorkspace : ( workspace : Partial < AgentWorkspaceContext >) => void ;
upsertDraft : ( draft : AgentDraft ) => void ;
removeDraft : ( draftId : string ) => void ;
appendInsight : ( insight : AgentInsight ) => void ;
resetSession : () => void ;
}
export const useAgentStore = create < AgentState >()(
subscribeWithSelector (
immer (( set ) => ({
activeAgent: 'dashboard' ,
sessionId: null ,
error: null ,
workspace: {},
dashboards: [],
workflows: [],
pipelines: [],
datasets: [],
drafts: [],
insights: [],
setActiveAgent : ( agentId ) => set (( state ) => {
state . activeAgent = agentId ;
}),
updateWorkspace : ( workspace ) => set (( state ) => {
state . workspace = { ... state . workspace , ... workspace };
}),
upsertDraft : ( draft ) => set (( state ) => {
const index = state . drafts . findIndex (( d ) => d . id === draft . id );
if ( index >= 0 ) {
state . drafts [ index ] = draft ;
} else {
state . drafts . push ( draft );
}
}),
removeDraft : ( draftId ) => set (( state ) => {
state . drafts = state . drafts . filter (( d ) => d . id !== draftId );
}),
appendInsight : ( insight ) => set (( state ) => {
const index = state . insights . findIndex (( i ) => i . id === insight . id );
if ( index === - 1 ) {
state . insights . push ( insight );
}
}),
resetSession : () => set (( state ) => {
state . sessionId = null ;
state . error = null ;
state . workspace = {};
state . drafts = [];
state . insights = [];
}),
}))
)
);
2. React Hook for Multi-Agent
Use the useMultiAgent hook for easy component integration:
// lib/agent/use-multi-agent.ts
import { useMultiAgent } from '@/lib/agent/use-multi-agent' ;
export function MyComponent () {
const {
agents ,
activeAgent ,
activeAgentId ,
databaseContext ,
setActiveAgent ,
sendMessage ,
isLoading ,
error ,
} = useMultiAgent ({
defaultAgent: 'dashboard' ,
userId: user . id ,
threadId: sessionId ,
});
return (
< div >
{ /* Your component UI */ }
</ div >
);
}
3. Component Integration
Here’s a complete example of integrating AG-UI Protocol in a React component:
// components/agent/AgentConsole.tsx
'use client' ;
import { useMultiAgent } from '@/lib/agent/use-multi-agent' ;
import { useAgentStore } from '@/lib/stores/agentStore' ;
import { Button } from '@/components/ui/button' ;
import { Input } from '@/components/ui/input' ;
import { Badge } from '@/components/ui/badge' ;
import { useState } from 'react' ;
export function AgentConsole () {
const [ input , setInput ] = useState ( '' );
const {
agents ,
activeAgent ,
sendMessage ,
isLoading ,
} = useMultiAgent ({
userId: 'user-123' ,
threadId: 'session-456' ,
});
const { drafts , insights , workspace } = useAgentStore ();
const handleSend = async () => {
if ( ! input . trim ()) return ;
await sendMessage ( input , {
workspaceId: workspace . activeSheet ,
page: '/dashboard' ,
});
setInput ( '' );
};
return (
< div className = "flex flex-col h-full" >
{ /* Agent Selector */ }
< div className = "flex items-center gap-2 p-4 border-b" >
< span className = "text-sm font-medium" > Active Agent : </ span >
{ agents . map (( agent ) => (
< Badge
key = {agent. id }
variant = {activeAgent?. id === agent . id ? 'default' : 'outline' }
>
{ agent . name }
</ Badge >
))}
</ div >
{ /* Drafts Panel */ }
{ drafts . length > 0 && (
< div className = "p-4 bg-amber-50 border-b" >
< h3 className = "font-medium text-sm mb-2" >
Pending Approvals ({ drafts . length })
</ h3 >
{ drafts . map (( draft ) => (
< DraftCard key = {draft. id } draft = { draft } />
))}
</ div >
)}
{ /* Insights Panel */ }
{ insights . length > 0 && (
< div className = "p-4 bg-blue-50 border-b" >
< h3 className = "font-medium text-sm mb-2" >
Insights ({ insights . length })
</ h3 >
{ insights . map (( insight ) => (
< InsightCard key = {insight. id } insight = { insight } />
))}
</ div >
)}
{ /* Message Input */ }
< div className = "flex items-center gap-2 p-4 border-t" >
< Input
value = { input }
onChange = {(e) => setInput (e.target.value)}
placeholder = "Ask the agent..."
onKeyDown = {(e) => e. key === 'Enter' && handleSend ()}
disabled = { isLoading }
/>
< Button onClick = { handleSend } disabled = { isLoading } >
Send
</ Button >
</ div >
</ div >
);
}
Complete Example: Multi-Step Workflow
Here’s a complete example demonstrating a multi-step workflow with human-in-the-loop approvals:
React Component
Backend Workflow
'use client' ;
import { useMultiAgent } from '@/lib/agent/use-multi-agent' ;
import { useAgentStore } from '@/lib/stores/agentStore' ;
import { useState , useEffect } from 'react' ;
export function PricingWorkflow () {
const { sendMessage , isLoading } = useMultiAgent ({
defaultAgent: 'pricing' ,
userId: 'user-123' ,
});
const { drafts , upsertDraft , removeDraft , workspace } = useAgentStore ();
const [ workflowStatus , setWorkflowStatus ] = useState < 'idle' | 'running' | 'completed' >( 'idle' );
// Start pricing workflow
const startWorkflow = async () => {
setWorkflowStatus ( 'running' );
await sendMessage ( 'Start daily pricing workflow' , {
portfolioId: workspace . activeSheet ,
date: new Date (). toISOString (),
});
};
// Handle draft approval
const approveDraft = async ( draftId : string ) => {
const draft = drafts . find (( d ) => d . id === draftId );
if ( ! draft ) return ;
// Update draft status
upsertDraft ({ ... draft , status: 'approved' });
// Send approval to agent
await sendMessage ( `Approve draft: ${ draftId } ` , {
draftId ,
action: 'approve' ,
});
};
// Handle draft rejection
const rejectDraft = async ( draftId : string , reason : string ) => {
const draft = drafts . find (( d ) => d . id === draftId );
if ( ! draft ) return ;
upsertDraft ({ ... draft , status: 'rejected' });
await sendMessage ( `Reject draft: ${ draftId } ` , {
draftId ,
action: 'reject' ,
reason ,
});
};
return (
< div className = "space-y-4" >
< Button
onClick = { startWorkflow }
disabled = { workflowStatus === 'running' || isLoading }
>
{ workflowStatus === ' running ' ? 'Running...' : 'Start Pricing Workflow' }
</ Button >
{ /* Draft Approvals */ }
< div className = "space-y-2" >
{ drafts . map (( draft ) => (
< div key = {draft. id } className = "p-4 border rounded-lg" >
< h3 className = "font-medium" > {draft. title } </ h3 >
< p className = "text-sm text-muted-foreground" > {draft. description } </ p >
{ draft . preview && (
< div className = "mt-2 p-2 bg-muted rounded" >
< pre className = "text-xs" >
{ JSON . stringify ( draft . preview , null , 2)}
</ pre >
</ div >
)}
< div className = "flex gap-2 mt-4" >
< Button
size = "sm"
onClick = {() => approveDraft (draft.id)}
>
Approve
</ Button >
< Button
size = "sm"
variant = "outline"
onClick = {() => rejectDraft (draft.id, 'Not ready' )}
>
Reject
</ Button >
</ div >
</ div >
))}
</ div >
</ div >
);
}
Draft System
The draft system is a key feature of AG-UI Protocol that enables human-in-the-loop approvals for agent actions.
Draft Lifecycle
Creating Drafts
Agents create drafts for actions requiring approval:
const draft : AgentDraft = {
id: `draft- ${ Date . now () } ` ,
type: 'validation' ,
title: 'Apply ASIC RG94 Validation Rule' ,
description: 'Add NAV variance check for Portfolio ABC' ,
status: 'pending' ,
confidence: 0.92 ,
preview: {
before: { rule: null },
after: {
rule: {
name: 'NAV Variance Check' ,
tolerance: 3 , // basis points
portfolio: 'ABC' ,
},
},
affectedCells: [ 'E4:E50' ],
},
actions: [
{
type: 'add_validation_rule' ,
params: {
ruleId: 'rule-123' ,
portfolio: 'ABC' ,
},
},
],
createdAt: new Date (). toISOString (),
expiresAt: new Date ( Date . now () + 3600000 ). toISOString (), // 1 hour
};
Handling Draft Approvals
Frontend code to handle draft approvals:
const { drafts , upsertDraft } = useAgentStore ();
async function handleApproval ( draftId : string , approved : boolean ) {
const draft = drafts . find (( d ) => d . id === draftId );
if ( ! draft ) return ;
// Update local state
upsertDraft ({
... draft ,
status: approved ? 'approved' : 'rejected' ,
});
// Notify backend
await sendMessage ( `Draft ${ approved ? 'approved' : 'rejected' } ` , {
draftId ,
action: approved ? 'approve' : 'reject' ,
});
// If approved, apply changes
if ( approved ) {
for ( const action of draft . actions ) {
await executeAction ( action );
}
upsertDraft ({ ... draft , status: 'applied' });
}
}
AG-UI Protocol provides robust tool execution with approval gates and result tracking.
interface ToolCall {
id : string ;
name : string ;
description : string ;
parameters : Record < string , any >;
requiresApproval : boolean ;
estimatedDuration ?: number ;
}
Tool Execution with Approval
Direct Tool Execution (No Approval)
Use Cases
1. Complex Data Validation Workflows
Multi-step validation with approvals:
// Agent validates data across multiple checks
const workflow = [
{ step: 'data_quality' , requiresApproval: false },
{ step: 'price_validation' , requiresApproval: true },
{ step: 'reconciliation' , requiresApproval: true },
{ step: 'asic_compliance' , requiresApproval: false },
{ step: 'final_approval' , requiresApproval: true },
];
2. Multi-Agent Collaboration
Multiple agents working together on a task:
const { setActiveAgent , sendMessage } = useMultiAgent ();
// Pricing agent fetches prices
await setActiveAgent ( 'pricing' );
await sendMessage ( 'Fetch EOD prices for all portfolios' );
// Switch to reconciliation agent
await setActiveAgent ( 'reconciliation' );
await sendMessage ( 'Reconcile positions against custodian' );
// Switch to compliance agent
await setActiveAgent ( 'compliance' );
await sendMessage ( 'Run ASIC RG94 validation checks' );
3. Human-in-the-Loop Pricing
Daily pricing workflow with approval gates:
// Agent proposes pricing changes
const pricingDraft = await sendMessage ( 'Calculate NAV for all portfolios' );
// User reviews and approves
await handleApproval ( pricingDraft . id , true );
// Agent applies prices and generates reports
await sendMessage ( 'Apply approved prices and generate reports' );
Best Practices
State Management
Optimistic Updates Update Zustand stores immediately for responsive UI, then reconcile with backend state.
State Granularity Keep state focused. Use multiple small stores instead of one large store.
Subscription Management Use subscribeWithSelector middleware to prevent unnecessary re-renders.
Error Recovery Implement rollback logic when backend state sync fails.
// 1. Selective store subscriptions
const drafts = useAgentStore (( state ) => state . drafts );
// Only re-renders when drafts change
// 2. Memoize expensive computations
const filteredDrafts = useMemo (
() => drafts . filter (( d ) => d . status === 'pending' ),
[ drafts ]
);
// 3. Batch state updates
useAgentStore . setState (( state ) => ({
... state ,
drafts: [ ... state . drafts , newDraft ],
insights: [ ... state . insights , newInsight ],
}));
Error Handling
const { sendMessage , error } = useMultiAgent ();
try {
await sendMessage ( 'Complex query' );
} catch ( err ) {
// Handle error locally
console . error ( 'Message failed:' , err );
// Update store
useAgentStore . getState (). setError ( err . message );
// Show user notification
toast . error ( 'Failed to send message' );
}
Troubleshooting
Common Issues
State not syncing between frontend and backend
Cause : HttpAgent not properly configured or backend not returning state updates.Solution :// Verify HttpAgent configuration
const agent = new HttpAgent ({
url: process . env . NEXT_PUBLIC_AGENT_API_URL ,
threadId: sessionId ,
debug: true , // Enable debug logging
});
// Check backend is returning state in response
// Backend should include full state in response
Drafts not appearing in UI
Cause : Store not being updated or component not subscribed to drafts.Solution :// Ensure component subscribes to drafts
const drafts = useAgentStore (( state ) => state . drafts );
// Verify upsertDraft is being called
console . log ( 'Upserting draft:' , draft );
useAgentStore . getState (). upsertDraft ( draft );
// Check draft is valid
const isValid = draft . id && draft . type && draft . title ;
Agent not receiving workspace context
Cause : Workspace context not being passed in sendMessage metadata.Solution :const { workspace } = useAgentStore ();
await sendMessage ( 'Your message' , {
activeSheet: workspace . activeSheet ,
activeDashboard: workspace . activeDashboard ,
currentPage: window . location . pathname ,
});
Observable stream not emitting events
Cause : Backend not properly implementing event streaming.Solution :# Backend should yield events during processing
async def stream_response ():
yield { "type" : "message_start" }
yield { "type" : "tool_call" , "data" : { ... }}
yield { "type" : "message_delta" , "content" : "..." }
yield { "type" : "message_end" }
Debug Mode
Enable debug logging to troubleshoot issues:
// Enable AG-UI debug mode
const agent = new HttpAgent ({
url: process . env . NEXT_PUBLIC_AGENT_API_URL ,
threadId: sessionId ,
debug: true , // Logs all agent communication
});
// Add custom logging
useAgentStore . subscribe (
( state ) => state . drafts ,
( drafts ) => {
console . log ( '[AgentStore] Drafts updated:' , drafts );
}
);
API Reference
MultiAgentClient
class MultiAgentClient {
constructor ( config : MultiAgentConfig );
// Agent management
initialize () : Promise < void >;
getAgents () : AgentInfo [];
getActiveAgent () : AgentInfo | undefined ;
setActiveAgent ( agentId : AgentId ) : Promise < DatabaseContext >;
// Messaging
sendMessage ( content : string , metadata ?: Record < string , unknown >) : Promise < RunAgentResult >;
streamMessage ( content : string , metadata ?: Record < string , unknown >) : AsyncGenerator < BaseEvent >;
// Configuration
getHttpAgent () : HttpAgent ;
getDatabaseContext ( agentId : AgentId ) : Promise < DatabaseContext >;
}
useMultiAgent Hook
function useMultiAgent ( options ?: UseMultiAgentOptions ) : UseMultiAgentReturn ;
interface UseMultiAgentReturn {
agents : AgentInfo [];
activeAgent : AgentInfo | undefined ;
activeAgentId : AgentId ;
databaseContext : DatabaseContext | null ;
setActiveAgent : ( agentId : AgentId ) => Promise < void >;
getDatabaseContext : ( agentId : AgentId ) => Promise < DatabaseContext >;
sendMessage : ( content : string , metadata ?: Record < string , any >) => Promise < any >;
isLoading : boolean ;
error : Error | null ;
}
useAgentStore
interface AgentState {
// State
activeAgent : AgentId ;
sessionId : string | null ;
workspace : AgentWorkspaceContext ;
drafts : AgentDraft [];
insights : AgentInsight [];
// Actions
setActiveAgent : ( agentId : AgentId ) => void ;
updateWorkspace : ( workspace : Partial < AgentWorkspaceContext >) => void ;
upsertDraft : ( draft : AgentDraft ) => void ;
removeDraft : ( draftId : string ) => void ;
appendInsight : ( insight : AgentInsight ) => void ;
resetSession : () => void ;
}
Next Steps : Explore multi-agent orchestration patterns or dive into advanced state management techniques.