ArctenArcten Docs

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

  1. Memoize tools arrays to prevent re-renders:
const tools = useMemo(() => [tool1, tool2], []);
const safeTools = useMemo(() => [tool3, tool4], []);
  1. 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" }}
  1. 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