bs-agent
v0.0.9
Published
A React library for integrating BuildShip AI agents into your frontend applications.
Maintainers
Readme
@buildship/agent
A React library for integrating BuildShip AI agents into your frontend applications with support for streaming responses, file handling, and client-side widgets.
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
- 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 |
| 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 |
| 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.
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>
);
}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
};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,
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
- File URLs: Ensure file URLs are publicly accessible or use signed URLs with sufficient expiration
- Error Handling: Wrap
handleSendcalls in try-catch blocks for error handling - Widget Registry: Keep your widget registry centralized for easier maintenance
License
MIT
