buildship-agent
v1.0.11
Published
A React library for building AI agent applications with streaming responses, interactive tools, and session management.
Downloads
293
Maintainers
Readme
buildship-agent
A React library for building AI agent applications with streaming responses, interactive tools, and session management. Works with any AI agent backend that supports Server-Sent Events (SSE).
Features
- Real-time Streaming: Built on Server-Sent Events (SSE) for live agent responses
- Session Management: Automatic conversation persistence with localStorage
- File Support: Upload and send files to your agents
- Client Tools/Widgets: Render interactive components from agent responses
- Interactive Tools: Pause agent execution and collect user input with
requiresResulttools - TypeScript: Full type safety with comprehensive TypeScript definitions
- Multi-Agent: Support for multiple agents in a single application
Installation
npm install buildship-agentQuick Start
import { AgentContextProvider, useAgentContext } from "buildship-agent";
// 1. Wrap your app with the provider
function App() {
return (
<AgentContextProvider>
<YourApp />
</AgentContextProvider>
);
}
// 2. Use the agent in your components
function ChatComponent() {
const agent = useAgentContext(
"your-agent-id",
"https://your-agent-url.com",
"your-access-key",
);
const handleSend = () => {
agent.handleSend("Hello, agent!");
};
return (
<div>
{agent.messages.map((msg, idx) => (
<div key={idx}>
<strong>{msg.role}:</strong> {msg.content}
</div>
))}
<button onClick={handleSend} disabled={agent.inProgress}>
Send Message
</button>
</div>
);
}Setup
AgentContextProvider
The AgentContextProvider must wrap your application to enable agent
functionality. It manages:
- Global session state across all agents
- Automatic localStorage persistence
- Agent runner registry
import { AgentContextProvider } from "buildship-agent";
function App() {
return (
<AgentContextProvider>{/* Your app components */}</AgentContextProvider>
);
}Basic Usage
Using the Agent Hook
The useAgentContext hook is the primary interface for interacting with agents:
import { useAgentContext } from "buildship-agent";
function AgentChat() {
const agent = useAgentContext(
"my-agent-id", // Unique identifier for your agent
"https://agent-url.com", // Your agent's endpoint URL
"your-access-key", // Access key for authenticated requests (sent as Bearer token)
);
// Send a message
const sendMessage = async () => {
await agent.handleSend("What's the weather today?");
};
// Display messages
return (
<div>
{agent.messages.map((message, idx) => (
<div key={idx}>
<strong>{message.role === "user" ? "You" : "Agent"}:</strong>
<p>{message.content}</p>
</div>
))}
{agent.inProgress && <div>Agent is thinking...</div>}
<button onClick={sendMessage} disabled={agent.inProgress}>
Send
</button>
</div>
);
}AgentRunner API
The useAgentContext hook returns an AgentRunner object with:
| Property | Type | Description |
| ---------------------- | ------------------------------- | ------------------------------------------- |
| messages | Message[] | Array of conversation messages |
| inProgress | boolean | Whether the agent is currently processing |
| isPaused | boolean | Whether agent is paused waiting for results |
| pendingToolCalls | PendingToolCall[] | Tools waiting for user input |
| sessionId | string | Current conversation session ID |
| sessions | Session[] | All sessions for this agent |
| debugData | Record<string, DebugDataType> | Debug information indexed by executionId |
| handleSend | Function | Send a message to the agent |
| submitToolResults | Function | Submit results from interactive tools |
| switchSession | Function | Switch to a different session |
| deleteSession | Function | Delete a session |
| addOptimisticMessage | Function | Add message to UI without sending |
| abort | Function | Cancel current agent execution |
Session Management
function SessionList() {
const agent = useAgentContext("agent-id", "agent-url", "access-key");
return (
<div>
{agent.sessions.map((session) => (
<div key={session.id}>
<button onClick={() => agent.switchSession(session.id)}>
{session.name || "Untitled Session"}
</button>
<button onClick={() => agent.deleteSession(session.id)}>
Delete
</button>
</div>
))}
{/* Create new session */}
<button onClick={() => agent.switchSession()}>New Session</button>
</div>
);
}File Handling
To send files to your agent:
1. Upload Files to Storage
First, upload files to a publicly accessible URL (e.g., Firebase Storage, AWS S3):
async function uploadFiles(files: File[]): Promise<Record<string, string>> {
// Upload to your storage provider
const fileMap: Record<string, string> = {};
for (const file of files) {
const url = await uploadToStorage(file); // Your upload logic
const fileId = file.name.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
fileMap[fileId] = url;
}
return fileMap;
}2. Send Files with Message
function FileUpload() {
const agent = useAgentContext("agent-id", "agent-url", "access-key");
const [files, setFiles] = useState<File[]>([]);
const handleSendWithFiles = async () => {
// Upload files and get URL mapping
const fileMap = await uploadFiles(files);
// Create input with file references
const fileIds = Object.keys(fileMap).join(", ");
const input = `Analyze these files: ${fileIds}`;
// Send to agent with file context
await agent.handleSend(input, {
context: {
mapped_file_ids_with_url: fileMap,
},
});
};
return (
<div>
<input
type="file"
multiple
onChange={(e) => setFiles(Array.from(e.target.files || []))}
/>
<button onClick={handleSendWithFiles}>Send with Files</button>
</div>
);
}File Context Structure
{
context: {
mapped_file_ids_with_url: {
"file_name_pdf": "https://storage.url/file1.pdf",
"image_png": "https://storage.url/image.png"
}
}
}Client Tools / Widgets
Client tools allow your agent to render interactive widgets directly in the chat interface. There are two types of client tools:
- Fire-and-forget tools (default): Agent calls the tool and continues immediately without waiting for a result
- Interactive tools (
requiresResult: true): Agent pauses execution and waits for user input before continuing
1. Define Tool Configuration
Create tool configurations with Zod schemas:
import { z } from "zod";
export const chartToolConfig = {
name: "render_chart",
description: "Renders a bar chart with the provided data",
schema: z.object({
title: z.string().describe("Chart title"),
data: z
.array(
z.object({
label: z.string(),
value: z.number(),
}),
)
.describe("Data points for the chart"),
}),
};
// Create your widget component
export function ChartWidget({ title, data }) {
return (
<div>
<h3>{title}</h3>
{/* Your chart rendering logic */}
</div>
);
}2. Create Widget Registry
import type { ComponentType } from "react";
import type { ClientToolDefinition } from "buildship-agent";
import { z } from "zod";
// Import all your widgets
import { ChartWidget, chartToolConfig } from "./widgets/chart";
import { MapWidget, mapToolConfig } from "./widgets/map";
const allConfigs = [chartToolConfig, mapToolConfig];
// Registry for rendering widgets
export const widgetRegistry: Record<string, ComponentType<any>> = {
[chartToolConfig.name]: ChartWidget,
[mapToolConfig.name]: MapWidget,
};
// Convert to agent tool definitions
export function getToolDefinitions(): ClientToolDefinition[] {
return allConfigs.map((config) => {
const parameters = z.toJSONSchema(config.schema);
// Remove $schema property for LLM compatibility
if (
parameters &&
typeof parameters === "object" &&
"$schema" in parameters
) {
const { $schema, ...rest } = parameters as any;
return {
name: config.name,
description: config.description,
parameters: rest,
};
}
return {
name: config.name,
description: config.description,
parameters,
};
});
}3. Pass Tools to Agent
import { getToolDefinitions } from "./widget-registry";
function ChatWithWidgets() {
const agent = useAgentContext("agent-id", "agent-url", "access-key");
const tools = getToolDefinitions();
const handleSend = async (input: string) => {
await agent.handleSend(input, {
clientTools: tools,
});
};
return (
<div>
<button onClick={() => handleSend("Show me a chart")}>
Ask for Chart
</button>
</div>
);
}4. Render Widgets in Messages
import { widgetRegistry } from "./widget-registry";
function MessageDisplay() {
const agent = useAgentContext("agent-id", "agent-url", "access-key");
return (
<div>
{agent.messages.map((message) => (
<div key={message.executionId}>
{/* Render message parts (text + widgets) */}
{message.parts?.map((part, idx) => {
if (part.type === "text") {
return <p key={idx}>{part.text}</p>;
} else if (part.type === "widget") {
const Widget = widgetRegistry[part.toolName];
if (!Widget) return null;
return <Widget key={idx} {...part.inputs} />;
}
return null;
})}
{/* Fallback: render plain content if no parts */}
{!message.parts && <p>{message.content}</p>}
</div>
))}
</div>
);
}Interactive Client Tools (Pause & Resume)
Interactive client tools allow agents to pause execution and wait for user input before continuing. This is useful for scenarios like:
- Flight/hotel selection from search results
- Form inputs and confirmations
- Date/time pickers
- File selection
- Custom decision points
How It Works
- Agent calls a tool with
requiresResult: true - Agent execution pauses (
isPausedbecomestrue) - Frontend renders UI and collects user input
- Frontend calls
submitToolResults()with user's selection - Agent resumes execution with the result and continues
Complete Example: Flight Booking
import { useState, useEffect } from "react";
import { useAgentContext } from "buildship-agent";
import type { ClientToolDefinition } from "buildship-agent";
// Define interactive tool
const FLIGHT_PICKER_TOOL: ClientToolDefinition = {
name: "show_flight_picker",
description: "Show flight picker UI and get user's flight selection",
parameters: {
type: "object",
properties: {
origin: { type: "string", description: "Origin airport code" },
destination: { type: "string", description: "Destination airport code" },
date: { type: "string", description: "Travel date" },
flights: {
type: "array",
description: "Available flights",
items: {
type: "object",
properties: {
flightNumber: { type: "string" },
airline: { type: "string" },
price: { type: "number" },
departureTime: { type: "string" },
},
},
},
},
required: ["origin", "destination", "flights"],
},
requiresResult: true, // ✅ Agent will pause and wait
};
// Flight picker component
function FlightPickerModal({ flights, onSelect, onCancel }) {
return (
<div className="modal">
<h3>Select a Flight</h3>
{flights.map((flight) => (
<div key={flight.flightNumber} className="flight-option">
<div>
{flight.airline} {flight.flightNumber}
</div>
<div>Departure: {flight.departureTime}</div>
<div>${flight.price}</div>
<button onClick={() => onSelect(flight)}>Select</button>
</div>
))}
<button onClick={onCancel}>Cancel</button>
</div>
);
}
// Main chat component
function FlightBookingChat() {
const agent = useAgentContext(
"flight-agent",
"https://api.example.com/executeAgent/flight-agent",
"your-access-key",
);
const [showFlightPicker, setShowFlightPicker] = useState(false);
const [currentToolCall, setCurrentToolCall] = useState(null);
// Monitor for pending tool calls
useEffect(() => {
if (agent.isPaused && agent.pendingToolCalls.length > 0) {
const toolCall = agent.pendingToolCalls[0];
if (toolCall.toolName === "show_flight_picker") {
setCurrentToolCall(toolCall);
setShowFlightPicker(true);
}
}
}, [agent.isPaused, agent.pendingToolCalls]);
const handleFlightSelect = async (selectedFlight) => {
if (!currentToolCall) return;
// Submit the result back to the agent
await agent.submitToolResults([
{
callId: currentToolCall.callId,
result: selectedFlight,
},
]);
// Close the modal
setShowFlightPicker(false);
setCurrentToolCall(null);
};
const handleCancel = async () => {
if (!currentToolCall) return;
// Submit null/cancel result
await agent.submitToolResults([
{
callId: currentToolCall.callId,
result: { cancelled: true },
},
]);
setShowFlightPicker(false);
setCurrentToolCall(null);
};
const handleSendMessage = async (input: string) => {
await agent.handleSend(input, {
clientTools: [FLIGHT_PICKER_TOOL],
});
};
return (
<div>
{/* Messages */}
<div className="messages">
{agent.messages.map((msg, idx) => (
<div key={idx} className={msg.role}>
{msg.content}
</div>
))}
{agent.isPaused && <div className="status">Waiting for input...</div>}
</div>
{/* Flight Picker Modal */}
{showFlightPicker && currentToolCall && (
<FlightPickerModal
flights={currentToolCall.inputs.flights}
onSelect={handleFlightSelect}
onCancel={handleCancel}
/>
)}
{/* Input */}
<input
type="text"
placeholder="Ask to book a flight..."
onKeyPress={(e) => {
if (e.key === "Enter") {
handleSendMessage(e.currentTarget.value);
e.currentTarget.value = "";
}
}}
disabled={agent.inProgress}
/>
</div>
);
}submitToolResults Options
agent.submitToolResults(
toolResults: ClientToolResult[],
options?: {
input?: string; // Optional follow-up message
context?: object; // Additional context
additionalHeaders?: Record<string, string>;
}
)Example: Multiple Tool Results
If the agent calls multiple tools that require results:
function MultiToolHandler() {
const agent = useAgentContext("agent-id", "agent-url", "access-key");
const handleSubmitAll = async () => {
// Submit results for all pending tools at once
const results = agent.pendingToolCalls.map((toolCall) => ({
callId: toolCall.callId,
result: getResultForTool(toolCall.toolName, toolCall.inputs),
}));
await agent.submitToolResults(results);
};
return (
<div>
{agent.isPaused && (
<div>
<h3>Pending Actions ({agent.pendingToolCalls.length})</h3>
{agent.pendingToolCalls.map((toolCall) => (
<div key={toolCall.callId}>
<strong>{toolCall.toolName}</strong>
<pre>{JSON.stringify(toolCall.inputs, null, 2)}</pre>
</div>
))}
<button onClick={handleSubmitAll}>Submit All</button>
</div>
)}
</div>
);
}Example: Resume with Follow-up Message
const handleSubmitWithMessage = async (result: any) => {
await agent.submitToolResults(
[{ callId: currentToolCall.callId, result }],
{
input: "Great! Now book the hotel too.", // Optional follow-up
},
);
};Fire-and-Forget Tools (No Pause)
For tools that don't need user input, simply omit requiresResult:
const NOTIFICATION_TOOL: ClientToolDefinition = {
name: "show_notification",
description: "Display a notification to the user",
parameters: {
type: "object",
properties: {
message: { type: "string" },
type: { type: "string", enum: ["info", "success", "warning", "error"] },
},
},
// No requiresResult - agent continues immediately
};Best Practices for Interactive Tools
Clear Tool Descriptions: Help the LLM understand when to use the tool
{ name: "show_flight_picker", description: "Show a flight selection UI when the user asks to book or search for flights. Use this AFTER searching for available flights.", // ... }Handle Cancellation: Always provide a way for users to cancel
await agent.submitToolResults([ { callId: toolCall.callId, result: { cancelled: true, reason: "User cancelled" }, }, ]);Loading States: Show appropriate UI while paused
{ agent.isPaused && ( <div className="loading">Waiting for your selection...</div> ); }Error Handling: Wrap submitToolResults in try-catch
try { await agent.submitToolResults([{ callId, result }]); } catch (error) { console.error("Failed to submit tool result:", error); // Show error to user }Type Safety: Type your tool inputs and results
type FlightPickerInputs = { origin: string; destination: string; flights: Flight[]; }; type FlightPickerResult = Flight | { cancelled: true };
API Reference
handleSend Options
agent.handleSend(input: string, options?: {
// Custom context data for the agent
context?: Record<string, unknown>;
// Don't add user message to UI (for optimistic updates)
skipUserMessage?: boolean;
// Additional HTTP headers
additionalHeaders?: Record<string, string>;
// Server-side tool definitions
tools?: ClientToolDefinition[];
// Client-side widget definitions
clientTools?: ClientToolDefinition[];
})Types
Message
type Message = {
role: "user" | "agent";
content: string; // Full text content
parts?: MessagePart[]; // Structured parts (text + widgets)
executionId?: string; // Links to debug data
};
type MessagePart =
| { type: "text"; text: string; firstSequence: number; lastSequence: number }
| {
type: "widget";
toolName: string;
callId: string;
inputs: any;
sequence: number;
};Session
type Session = {
id: string;
createdAt: number;
updatedAt: number;
messages: Message[];
name?: string;
};ClientToolDefinition
type ClientToolDefinition = {
name: string; // Unique tool identifier
description: string; // Human-readable description for LLM
parameters: unknown; // JSON Schema object
requiresResult?: boolean; // If true, agent pauses and waits for user input
};
type ClientToolResult = {
callId: string; // ID from the client_tool_call event
result: any; // User's input or selection
};
type PendingToolCall = {
toolName: string;
callId: string;
inputs: any;
requiresResult: boolean;
};Advanced Features
Debug Data
Access detailed execution information:
function DebugView() {
const agent = useAgentContext("agent-id", "agent-url", "access-key");
return (
<div>
{Object.entries(agent.debugData).map(([executionId, debug]) => (
<details key={executionId}>
<summary>Execution {executionId}</summary>
<pre>{JSON.stringify(debug, null, 2)}</pre>
</details>
))}
</div>
);
}Custom Headers
await agent.handleSend("message", {
additionalHeaders: {
"X-Custom-Header": "value",
Authorization: "Bearer token",
},
});Abort Requests
function CancellableRequest() {
const agent = useAgentContext("agent-id", "agent-url", "access-key");
return (
<div>
<button onClick={() => agent.handleSend("Long task...")}>Start</button>
<button onClick={() => agent.abort()} disabled={!agent.inProgress}>
Cancel
</button>
</div>
);
}Optimistic Updates
function OptimisticMessage() {
const agent = useAgentContext("agent-id", "agent-url", "access-key");
const handleSend = async (input: string) => {
// Add message to UI immediately
agent.addOptimisticMessage(input);
// Send to agent without adding duplicate
await agent.handleSend(input, { skipUserMessage: true });
};
return <button onClick={() => handleSend("Hello")}>Send</button>;
}Storage and Persistence
The library automatically persists conversations to localStorage:
- Sessions: Stored under
buildship:agent:conversations - Debug Data: Stored under
buildship:agent:debug
Sessions are automatically synced across browser tabs and survive page refreshes.
TypeScript Support
The package is written in TypeScript and exports all types:
import type {
Message,
Session,
ClientToolDefinition,
ClientToolResult,
PendingToolCall,
AgentRunner,
DebugDataType,
MessagePart,
} from "buildship-agent";Best Practices
- Single Provider: Only use one
AgentContextProviderat the root of your app - Tool Descriptions: Write clear, detailed descriptions for client tools to help the LLM understand when to use them
- Interactive Tools: Use
requiresResult: trueonly when you truly need to pause execution for user input. For simple notifications or displays, use fire-and-forget tools - File URLs: Ensure file URLs are publicly accessible or use signed URLs with sufficient expiration
- Error Handling: Wrap
handleSendandsubmitToolResultscalls in try-catch blocks for error handling - Widget Registry: Keep your widget registry centralized for easier maintenance
- Pending Tool Management: Always handle
isPausedstate and provide clear UI for users when the agent is waiting for input
License
MIT
