@hhopkins/agent-runtime-react
v0.1.1
Published
React hooks and client for interacting with agent-service
Maintainers
Readme
@hhopkins/agent-runtime-react
React hooks and client library for connecting to @hhopkins/agent-runtime. Provides type-safe, real-time access to AI agent sessions with support for message streaming, file tracking, and subagent conversations.
Features
- ✅ Type-safe React hooks for session management
- ✅ Real-time WebSocket updates for streaming responses
- ✅ Context-based state management with optimized re-renders
- ✅ Full TypeScript support with comprehensive type definitions
- ✅ Architecture-agnostic - works with Claude SDK and Gemini CLI
- ✅ Session lifecycle management - create, load, destroy sessions
- ✅ Message streaming - real-time conversation blocks
- ✅ File workspace tracking - monitor agent-created files
- ✅ Subagent support - nested agent conversations (Claude SDK)
Installation
npm install @hhopkins/agent-runtime-react
# or
pnpm add @hhopkins/agent-runtime-reactNote: This package depends on @hhopkins/agent-runtime for type definitions. The runtime package is automatically installed as a dependency.
Quick Start
1. Wrap your app with the provider
import { AgentServiceProvider } from '@hhopkins/agent-runtime-react';
function App() {
return (
<AgentServiceProvider
apiUrl="http://localhost:3002"
wsUrl="http://localhost:3003"
apiKey="your-api-key"
debug={process.env.NODE_ENV === 'development'}
>
<YourApp />
</AgentServiceProvider>
);
}2. Use hooks in your components
import {
useAgentSession,
useMessages,
useWorkspaceFiles,
} from '@hhopkins/agent-runtime-react';
function ChatInterface() {
const { session, createSession, destroySession } = useAgentSession();
const { blocks, sendMessage, isStreaming } = useMessages(session?.info.sessionId || '');
const { files } = useWorkspaceFiles(session?.info.sessionId || '');
async function handleCreateSession() {
const sessionId = await createSession('my-agent-profile', 'claude-agent-sdk');
console.log('Created session:', sessionId);
}
async function handleSendMessage(message: string) {
await sendMessage(message);
}
return (
<div>
{!session ? (
<button onClick={handleCreateSession}>Start New Session</button>
) : (
<>
<ConversationView blocks={blocks} isStreaming={isStreaming} />
<MessageInput onSend={handleSendMessage} disabled={isStreaming} />
<FileList files={files} />
<button onClick={destroySession}>End Session</button>
</>
)}
</div>
);
}Core Concepts
Provider
The AgentServiceProvider component manages:
- REST API client for session operations
- WebSocket connection for real-time updates
- Global state for all sessions
- Event routing from WebSocket to state updates
State Management
Built on React Context + useReducer:
- Global state: All sessions indexed by sessionId
- Session state: Blocks, files, subagents, metadata
- Real-time updates: WebSocket events update state automatically
- Optimized re-renders: Context splitting prevents unnecessary updates
Sessions
Sessions represent individual agent conversations:
- Created with
createSession(agentProfileRef, architecture) - Loaded with
loadSession(sessionId) - Destroyed with
destroySession() - Auto-join WebSocket rooms for real-time updates
API Reference
Hooks
useSessionList()
Access and manage the list of all sessions.
const { sessions, isLoading, refresh, getSession } = useSessionList();Returns:
sessions: Array of session metadataisLoading: Whether initial load is in progressrefresh(): Manually refresh session listgetSession(sessionId): Get specific session by ID
useAgentSession(sessionId?)
Manage a single agent session lifecycle.
const {
session,
status,
isLoading,
error,
createSession,
loadSession,
destroySession,
syncSession,
} = useAgentSession();Parameters:
sessionId(optional): Auto-load this session on mount
Returns:
session: Current session state (blocks, files, subagents)status: Session status (active,inactive, etc.)isLoading: Whether an operation is in progresserror: Error from last operationcreateSession(profileRef, architecture): Create new sessionloadSession(sessionId): Load existing sessiondestroySession(): Destroy current sessionsyncSession(): Manually sync to persistence
Example:
function SessionManager() {
const { createSession, session, status } = useAgentSession();
const handleCreate = async () => {
try {
const sessionId = await createSession(
'my-coding-agent',
'claude-agent-sdk'
);
console.log('Session created:', sessionId);
} catch (error) {
console.error('Failed to create session:', error);
}
};
return (
<div>
<p>Status: {status}</p>
{!session && <button onClick={handleCreate}>Create Session</button>}
</div>
);
}useMessages(sessionId)
Access conversation blocks and send messages.
const {
blocks,
metadata,
isStreaming,
error,
sendMessage,
getBlock,
getBlocksByType,
} = useMessages(sessionId);Parameters:
sessionId(required): Session to track
Returns:
blocks: Array of conversation blocks (user messages, assistant text, tool uses, etc.)metadata: Session metadata (tokens, cost, model)isStreaming: Whether agent is currently streamingerror: Error from last message sendsendMessage(content): Send message to agentgetBlock(blockId): Get specific blockgetBlocksByType(type): Filter blocks by type
Example:
function ConversationView({ sessionId }: { sessionId: string }) {
const { blocks, sendMessage, isStreaming } = useMessages(sessionId);
return (
<div>
{blocks.map((block) => (
<BlockRenderer key={block.id} block={block} />
))}
{isStreaming && <TypingIndicator />}
<MessageInput onSend={sendMessage} disabled={isStreaming} />
</div>
);
}useWorkspaceFiles(sessionId)
Track files created/modified by the agent.
const {
files,
isLoading,
getFile,
getFilesByPattern,
getFilesByExtension,
} = useWorkspaceFiles(sessionId);Parameters:
sessionId(required): Session to track
Returns:
files: Array of workspace filesisLoading: Whether session is loadinggetFile(path): Get specific filegetFilesByPattern(regex): Filter by path patterngetFilesByExtension(ext): Filter by extension
Example:
function FileExplorer({ sessionId }: { sessionId: string }) {
const { files, getFilesByExtension } = useWorkspaceFiles(sessionId);
const pythonFiles = getFilesByExtension('.py');
const tsFiles = getFilesByExtension('.ts');
return (
<div>
<h3>Python Files ({pythonFiles.length})</h3>
{pythonFiles.map((file) => (
<FileItem key={file.path} file={file} />
))}
<h3>TypeScript Files ({tsFiles.length})</h3>
{tsFiles.map((file) => (
<FileItem key={file.path} file={file} />
))}
</div>
);
}useSubagents(sessionId)
Access subagent conversations (Claude SDK only).
const {
subagents,
count,
hasRunningSubagents,
getSubagent,
getSubagentBlocks,
getSubagentsByStatus,
} = useSubagents(sessionId);Parameters:
sessionId(required): Session to track
Returns:
subagents: Array of all subagentscount: Number of subagentshasRunningSubagents: Whether any are runninggetSubagent(subagentId): Get specific subagentgetSubagentBlocks(subagentId): Get blocks for subagentgetSubagentsByStatus(status): Filter by status
Example:
function SubagentMonitor({ sessionId }: { sessionId: string }) {
const { subagents, hasRunningSubagents } = useSubagents(sessionId);
return (
<div>
<h3>
Subagents ({subagents.length})
{hasRunningSubagents && <Spinner />}
</h3>
{subagents.map((subagent) => (
<SubagentCard key={subagent.id} subagent={subagent} />
))}
</div>
);
}Types
Conversation Blocks
All conversation elements are represented as typed blocks:
type ConversationBlock =
| UserMessageBlock // User input
| AssistantTextBlock // Agent text response
| ToolUseBlock // Agent tool invocation
| ToolResultBlock // Tool execution result
| ThinkingBlock // Agent reasoning (extended thinking)
| SystemBlock // System events
| SubagentBlock; // Subagent reference (Claude SDK)Each block has:
id: Unique identifiertimestamp: ISO timestamptype: Block type discriminator
Session Status
type SessionStatus =
| "pending"
| "active"
| "inactive"
| "completed"
| "failed"
| "building-sandbox";Architecture Types
type AGENT_ARCHITECTURE_TYPE = "claude-agent-sdk" | "gemini-cli";Type Guards
Use type guards to narrow block types:
import { isAssistantTextBlock, isToolUseBlock } from '@hhopkins/agent-runtime-react';
function BlockRenderer({ block }: { block: ConversationBlock }) {
if (isAssistantTextBlock(block)) {
return <div>{block.content}</div>;
}
if (isToolUseBlock(block)) {
return <ToolCallDisplay toolName={block.toolName} input={block.input} />;
}
// ... handle other block types
}Advanced Usage
Custom REST Client
For advanced use cases, you can access the REST client directly:
import { useContext } from 'react';
import { AgentServiceContext } from '@hhopkins/agent-runtime-react';
function CustomComponent() {
const context = useContext(AgentServiceContext);
const handleCustomOperation = async () => {
// Direct access to REST client
const isHealthy = await context.restClient.healthCheck();
console.log('Server healthy:', isHealthy);
};
return <button onClick={handleCustomOperation}>Health Check</button>;
}WebSocket Events
Listen to raw WebSocket events:
import { useContext, useEffect } from 'react';
import { AgentServiceContext } from '@hhopkins/agent-runtime-react';
function EventMonitor() {
const context = useContext(AgentServiceContext);
useEffect(() => {
const handler = (data: any) => {
console.log('Block started:', data);
};
context.wsManager.on('session:block:start', handler);
return () => {
context.wsManager.off('session:block:start', handler);
};
}, [context.wsManager]);
return <div>Monitoring events...</div>;
}Examples
Complete Chat Interface
import {
AgentServiceProvider,
useAgentSession,
useMessages,
useSubagents,
isAssistantTextBlock,
isUserMessageBlock,
isToolUseBlock,
} from '@hhopkins/agent-runtime-react';
function App() {
return (
<AgentServiceProvider
apiUrl="http://localhost:3002"
wsUrl="http://localhost:3003"
apiKey={process.env.REACT_APP_AGENT_API_KEY!}
>
<ChatApp />
</AgentServiceProvider>
);
}
function ChatApp() {
const { session, createSession, destroySession } = useAgentSession();
if (!session) {
return (
<button onClick={() => createSession('default', 'claude-agent-sdk')}>
Start Session
</button>
);
}
return (
<div>
<ConversationPanel sessionId={session.info.sessionId} />
<button onClick={destroySession}>End Session</button>
</div>
);
}
function ConversationPanel({ sessionId }: { sessionId: string }) {
const { blocks, sendMessage, isStreaming } = useMessages(sessionId);
const { subagents } = useSubagents(sessionId);
const [input, setInput] = useState('');
const handleSend = async () => {
if (input.trim()) {
await sendMessage(input);
setInput('');
}
};
return (
<div>
<div className="messages">
{blocks.map((block) => {
if (isUserMessageBlock(block)) {
return <UserMessage key={block.id} content={block.content} />;
}
if (isAssistantTextBlock(block)) {
return <AssistantMessage key={block.id} content={block.content} />;
}
if (isToolUseBlock(block)) {
return <ToolCall key={block.id} tool={block} />;
}
return null;
})}
</div>
{subagents.length > 0 && (
<div className="subagents">
<h4>Active Tasks ({subagents.length})</h4>
{subagents.map((sub) => (
<SubagentStatus key={sub.id} subagent={sub} />
))}
</div>
)}
<div className="input">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
disabled={isStreaming}
/>
<button onClick={handleSend} disabled={isStreaming}>
Send
</button>
</div>
</div>
);
}Troubleshooting
WebSocket not connecting
Ensure the WebSocket server is running and the URL is correct:
<AgentServiceProvider
wsUrl="http://localhost:3003" // Check port
// ...
/>Sessions not updating
Check that you're providing the correct sessionId to hooks:
// ✅ Correct
const { blocks } = useMessages(session?.info.sessionId || '');
// ❌ Wrong - missing sessionId
const { blocks } = useMessages();TypeScript errors
Ensure you have React types installed:
npm install --save-dev @types/reactLicense
MIT
Contributing
Contributions welcome! Please open an issue or PR.
