useAgent Hook
Build custom AI chat interfaces with the headless useAgent React hook
useAgent Hook
The useAgent hook provides a headless alternative to the ArctenAgent UI component, giving you complete control over the chat interface while maintaining all the powerful features like tool execution, conversation persistence, and token management.
The hook returns various state variables (messages, conversations, ...) and also various action functions (sendMessage, loadConversation, addToolOutput, ... ) that you can use to build your own custom on-brand agentic chat UI.
Quick Start
import { useAgent } from "@arcteninc/core";
// Import tool metadata - adjust the relative path to point to .arcten folder in your project root
import { toolMetadata } from "../.arcten/tool-metadata";
function MyCustomChat() {
const {
messages,
sendMessage,
status,
error
} = useAgent({
user: {
id: "user123",
email: "user@example.com"
},
toolMetadata
});
return (
<div>
{messages.map(msg => (
<div key={msg.id}>
{msg.parts.map(part => part.type === "text" && part.text)}
</div>
))}
<button onClick={() => sendMessage({ text: "Hello!" })}>
Send
</button>
</div>
);
}Core Features
1. Basic Chat
Send messages and receive AI responses:
import { toolMetadata } from "../.arcten/tool-metadata";
const { messages, sendMessage, status } = useAgent({
user: { id: "user123" },
toolMetadata
});
// Send a message
sendMessage({ text: "What's the weather?" });
// Check status
if (status === "streaming") {
// AI is responding...
}2. Adding Tools
Provide tools that the AI can call to perform actions:
import { toolMetadata } from "../.arcten/tool-metadata";
// Define your tools - these are examples, implement your own
function getWeather(city: string) {
return `Weather in ${city}: Sunny, 72°F`;
}
function sendEmail(to: string, subject: string, body: string) {
console.log(`Sending email to ${to}`);
return "Email sent successfully";
}
const { messages, sendMessage, addToolOutput } = useAgent({
user: { id: "user123" },
toolMetadata,
// Tools that auto-execute without confirmation
safeTools: [getWeather],
// Tools that require user approval
tools: [sendEmail],
// Handle tool approval requests
onToolCall: ({ toolCall }) => {
if (confirm(`Allow ${toolCall.toolName}?`)) {
// Execute the tool
const result = sendEmail(...Object.values(toolCall.args));
// Send result back to AI
addToolOutput({
toolCallId: toolCall.toolCallId,
tool: toolCall.toolName,
output: result
});
}
}
});3. Conversation Persistence
Load and manage conversation history:
import { toolMetadata } from "../.arcten/tool-metadata";
const {
conversations,
loadConversation,
deleteConversation,
startNewConversation
} = useAgent({
user: { id: "user123" },
toolMetadata
});
// List previous conversations
conversations.map(conv => (
<button onClick={() => loadConversation(conv.chatId)}>
{conv.title}
</button>
));
// Start fresh
startNewConversation();Configuration Options
import { toolMetadata } from "../.arcten/tool-metadata";
useAgent({
// API Configuration
apiBaseUrl: "https://api.arcten.com", // Default API URL
tokenEndpoint: "/api/arcten/token", // Your token endpoint
// Authentication
user: { // Required for persistence
id: "unique-user-id",
email: "user@example.com",
name: "John Doe"
},
// Tool Metadata (generated from arcten-extract-types)
toolMetadata, // Tool type definitions
// Tools
tools: [toolFunction1], // Tools requiring approval
safeTools: [toolFunction2], // Auto-execute tools
// Customization
systemPrompt: "You are a helpful assistant",
initialMessages: [], // Pre-populate messages
conversationId: "existing-id", // Load specific conversation
// Callbacks
onToolCall: ({ toolCall }) => {}, // Handle tool approvals
onFinish: ({ message, messages }) => {}, // Response complete
// Advanced
clientToken: "sk_xxx", // Provide pre-fetched token
skipTokenFetch: true // Skip token fetching
});Return Values
const {
// Messages
messages, // UIMessage[] - All chat messages
setMessages, // Manually update messages
// Actions
sendMessage, // Send user message
stop, // Stop streaming response
addToolOutput, // Provide tool execution result
// Conversations
conversations, // List of user's conversations
loadConversation, // Load conversation by ID
deleteConversation, // Delete conversation
startNewConversation, // Clear and start fresh
// State
id, // Current conversation ID
status, // "ready" | "submitted" | "streaming" | "error"
error, // Error object if any
isLoadingConversations,// Loading state
// Auth
clientToken, // Current auth token
tokenError // Token fetch error
} = useAgent(options);Message Structure
Messages use a parts-based structure for flexibility:
interface UIMessage {
id: string;
role: "user" | "assistant" | "system";
parts: Array<{
type: "text" | "reasoning" | "tool-*";
text?: string;
// Tool-specific fields
toolCallId?: string;
state?: "input-available" | "output-available";
input?: Record<string, any>;
output?: any;
}>;
}
// Example: Render messages with tool calls
messages.map(message => (
<div key={message.id}>
{message.parts.map((part, i) => {
if (part.type === "text") {
return <p key={i}>{part.text}</p>;
}
if (part.type?.startsWith("tool-")) {
// Create your own component to display tool calls
return <ToolDisplay key={i} part={part} />;
}
return null;
})}
</div>
))Tool Execution Flow
Safe Tools (Auto-Execute)
// Define a safe tool
function getCurrentTime() {
return new Date().toISOString();
}
useAgent({
safeTools: [getCurrentTime]
// No approval needed - executes automatically
});Regular Tools (Require Approval)
function deleteFile(filename: string) {
// Dangerous operation
fs.unlinkSync(filename);
return "File deleted";
}
const { addToolOutput } = useAgent({
tools: [deleteFile],
onToolCall: ({ toolCall }) => {
// Simple confirmation dialog
const approved = confirm(
`Allow ${toolCall.toolName} with args: ${JSON.stringify(toolCall.args)}?`
);
if (approved) {
// Execute and return result
const result = deleteFile(toolCall.args.filename);
addToolOutput({
toolCallId: toolCall.toolCallId,
tool: toolCall.toolName,
output: result
});
} else {
// User denied
addToolOutput({
toolCallId: toolCall.toolCallId,
tool: toolCall.toolName,
output: "User denied tool execution"
});
}
}
});Complete Example
Here's a fully-featured chat implementation:
import { useState } from "react";
import { useAgent } from "@arcteninc/core";
// Import tool metadata - adjust the relative path to point to .arcten folder in your project root
import { toolMetadata } from "../.arcten/tool-metadata";
export function CustomChat() {
const [input, setInput] = useState("");
const [pendingTools, setPendingTools] = useState(new Map());
const {
messages,
sendMessage,
status,
stop,
addToolOutput,
conversations,
loadConversation,
deleteConversation,
startNewConversation
} = useAgent({
user: {
id: "user123",
email: "user@example.com"
},
toolMetadata,
systemPrompt: "You are a helpful assistant",
safeTools: [
function getTime() { return new Date().toISOString(); }
],
tools: [
function calculate(expression: string) {
return eval(expression).toString();
}
],
onToolCall: ({ toolCall }) => {
setPendingTools(prev =>
new Map(prev).set(toolCall.toolCallId, toolCall)
);
}
});
const handleSend = () => {
// Don't send if there are pending tool approvals
if (pendingTools.size > 0) {
alert("Please approve or deny pending tool calls first");
return;
}
if (input.trim()) {
sendMessage({ text: input });
setInput("");
}
};
const approveTool = (toolCall) => {
// Find and execute the tool from your tools array
// Note: You'll need to keep references to your tool functions
const toolsMap = {
calculate: (expression) => eval(expression).toString()
// Add your other tools here
};
const tool = toolsMap[toolCall.toolName];
if (!tool) {
console.error(`Tool ${toolCall.toolName} not found`);
return;
}
const result = tool(...Object.values(toolCall.args));
// Send result to AI
addToolOutput({
toolCallId: toolCall.toolCallId,
tool: toolCall.toolName,
output: result
});
// Remove from pending
setPendingTools(prev => {
const next = new Map(prev);
next.delete(toolCall.toolCallId);
return next;
});
};
const denyTool = (toolCall) => {
// Send denial to AI
addToolOutput({
toolCallId: toolCall.toolCallId,
tool: toolCall.toolName,
output: "User denied tool execution"
});
// Remove from pending
setPendingTools(prev => {
const next = new Map(prev);
next.delete(toolCall.toolCallId);
return next;
});
};
return (
// Note: These CSS classes are examples - add your own styles
<div className="chat-container">
{/* History */}
<div className="history">
<h3>History</h3>
<button onClick={startNewConversation}>New Chat</button>
<ul>
{conversations.map(conv => (
<li key={conv._id}>
<span onClick={() => loadConversation(conv.chatId)}>
{conv.title}
</span>
<button onClick={() => deleteConversation(conv._id)}>
×
</button>
</li>
))}
</ul>
</div>
{/* Messages */}
<div className="messages">
{messages.map(msg => (
<div key={msg.id} className={msg.role}>
{msg.parts
.filter(p => p.type === "text")
.map(p => p.text)
.join("")}
</div>
))}
{/* Pending Tool Approvals */}
{Array.from(pendingTools.values()).map(toolCall => (
<div key={toolCall.toolCallId} className="tool-approval">
<p>Tool: {toolCall.toolName}</p>
<pre>{JSON.stringify(toolCall.args, null, 2)}</pre>
<button onClick={() => approveTool(toolCall)}>
Approve
</button>
<button onClick={() => denyTool(toolCall)}>
Deny
</button>
</div>
))}
</div>
{/* Input */}
<div className="input">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && !pendingTools.size && handleSend()}
placeholder={pendingTools.size > 0 ? "Handle tool calls first..." : "Type a message..."}
disabled={pendingTools.size > 0}
/>
{status === "streaming" ? (
<button onClick={stop}>Stop</button>
) : (
<button onClick={handleSend} disabled={pendingTools.size > 0}>
Send
</button>
)}
</div>
</div>
);
}Performance Tips
- Memoize tools arrays to prevent re-renders:
const tools = useMemo(() => [tool1, tool2], []);
const safeTools = useMemo(() => [tool3, tool4], []);- User object optimization - The hook handles this internally by comparing user content, not reference. You can safely pass inline objects:
// This won't cause unnecessary re-fetches
user={{ id: "123", email: "user@example.com" }}- Token management - Tokens are automatically fetched and refreshed. For SSR or shared tokens:
// Provide pre-fetched token
useAgent({ clientToken: "sk_xxx", skipTokenFetch: true })
// Or skip token for public/demo usage
useAgent({ skipTokenFetch: true })TypeScript Support
The hook is fully typed. Import types as needed:
import {
useAgent,
type ToolFunction,
type OnToolCallOptions,
type OnFinishOptions,
type UIMessage
} from "@arcteninc/core";
const myTool: ToolFunction = (param: string) => {
return `Result: ${param}`;
};
const handleToolCall = ({ toolCall }: OnToolCallOptions) => {
// TypeScript knows toolCall structure
};Next Steps
- Check out Tool Development for creating powerful tools
- See UI Customization for styling examples
- Learn about User & Conversations for persistence