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

buildship-agent

v1.0.11

Published

A React library for building AI agent applications with streaming responses, interactive tools, and session management.

Downloads

293

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 requiresResult tools
  • 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 | | 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

  1. Agent calls a tool with requiresResult: true
  2. Agent execution pauses (isPaused becomes true)
  3. Frontend renders UI and collects user input
  4. Frontend calls submitToolResults() with user's selection
  5. 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

  1. 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.",
      // ...
    }
  2. Handle Cancellation: Always provide a way for users to cancel

    await agent.submitToolResults([
      {
        callId: toolCall.callId,
        result: { cancelled: true, reason: "User cancelled" },
      },
    ]);
  3. Loading States: Show appropriate UI while paused

    {
      agent.isPaused && (
        <div className="loading">Waiting for your selection...</div>
      );
    }
  4. 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
    }
  5. 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

  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. Interactive Tools: Use requiresResult: true only when you truly need to pause execution for user input. For simple notifications or displays, use fire-and-forget tools
  4. File URLs: Ensure file URLs are publicly accessible or use signed URLs with sufficient expiration
  5. Error Handling: Wrap handleSend and submitToolResults calls in try-catch blocks for error handling
  6. Widget Registry: Keep your widget registry centralized for easier maintenance
  7. Pending Tool Management: Always handle isPaused state and provide clear UI for users when the agent is waiting for input

License

MIT