npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bs-agent

v0.0.9

Published

A React library for integrating BuildShip AI agents into your frontend applications.

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/agent

Quick 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

  1. Single Provider: Only use one AgentContextProvider at the root of your app
  2. Tool Descriptions: Write clear, detailed descriptions for client tools to help the LLM understand when to use them
  3. File URLs: Ensure file URLs are publicly accessible or use signed URLs with sufficient expiration
  4. Error Handling: Wrap handleSend calls in try-catch blocks for error handling
  5. Widget Registry: Keep your widget registry centralized for easier maintenance

License

MIT