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

@agilab/react

v0.1.2

Published

Headless React library that converts AG-UI SSE events into streaming-aware React state

Readme

@agilab/react

npm license

A headless React library that converts raw AG-UI SSE events into streaming-aware React state. You bring your own UI — the library handles the protocol.

Repo structure

| Path | Description | |---|---| | src/ | Library source — hooks, types, reducer | | examples/with-chatscope | Demo chat UI using @chatscope/chat-ui-kit-react |

Run the demo locally

npm install
npm run dev        # starts demo app on http://localhost:5173
npm run typecheck  # type-check library + example

Open the app, enter your AG-UI agent's URL in the config panel, and start chatting.


Using the library

Installation

npm install @agilab/react

Quick start

The two hooks give you everything you need. Here is the minimal pattern — replace the plain HTML with whatever component library you prefer:

import { useAgentRun, useUIState } from '@agilab/react';
import type { RunItem } from '@agilab/react';

export function AgentChat() {
  const [uiState] = useUIState({});
  const { agentState, sendMessage, abort } = useAgentRun({
    config: { url: 'https://your-agent.example.com/run', headers: {} },
    uiState,
  });

  const isStreaming =
    agentState.status === 'streaming' || agentState.status === 'connecting';

  const allRuns = [
    ...agentState.runs,
    ...(agentState.currentRun ? [agentState.currentRun] : []),
  ];

  return (
    <div>
      {allRuns.map(run => (
        <div key={run.runId}>
          {run.userInput && <p><strong>You:</strong> {run.userInput}</p>}

          {run.items.map((item: RunItem) =>
            item.kind === 'text'
              ? <p key={item.messageId}>{item.content}{!item.isComplete && '▊'}</p>
              : <pre key={item.toolCallId}>{item.toolCallName}({item.argsAccumulated})</pre>
          )}
        </div>
      ))}

      {agentState.status === 'error' && <p>{agentState.error}</p>}

      <input onKeyDown={e => e.key === 'Enter' && sendMessage(e.currentTarget.value)} />
      {isStreaming && <button onClick={abort}>Stop</button>}
    </div>
  );
}

Hooks

useAgentRun(options){ agentState, sendMessage, abort }

The main hook. Manages the SSE connection to your agent and reduces all incoming events into a single agentState object.

import { useAgentRun } from '@agilab/react';

const { agentState, sendMessage, abort } = useAgentRun({
  config: {
    url: 'https://your-agent.example.com/run',
    headers: { Authorization: 'Bearer ...' },
  },
  uiState,    // sent to the agent as `state` on every run
  handlers: {
    // called when the agent emits a CUSTOM event named "navigate"
    navigate: (data) => router.push((data as { path: string }).path),
  },
});

Options

| Option | Type | Required | Description | |---|---|---|---| | config.url | string | Yes | Endpoint that accepts AG-UI runs | | config.headers | Record<string, string> | Yes | HTTP headers (e.g. auth) | | uiState | TUIState | Yes | Current UI state sent to the agent on every run | | handlers | HandlerRegistry | No | Map of custom event names to handler functions |

Returns

| Value | Type | Description | |---|---|---| | agentState | AgentState | Full reactive state — see State reference below | | sendMessage(text) | (text: string) => void | Start a new run. Cancels any in-progress run first. | | abort() | () => void | Stop the current run immediately |

useUIState(initial)[state, updaters]

Manages the state your app sends to the agent so it can understand current UI context. Anything you want the agent to be aware of (current page, selected items, filters) goes here.

import { useUIState } from '@agilab/react';

const [uiState, ui] = useUIState({ page: 'home', selectedIds: [] });

ui.update({ page: 'settings' });           // partial update — merges into current state
ui.set({ page: 'home', selectedIds: [] }); // full replace
ui.reset();                                // back to initial value

Pass uiState directly into useAgentRun. The agent receives it as state in every RunAgentInput payload.


Custom event handlers

When your agent emits a CUSTOM event, the library routes it to the matching handler in handlers rather than putting it in state. This is the escape hatch for agent-driven side effects (navigation, toasts, UI mutations) that don't fit the message model.

const { agentState, sendMessage } = useAgentRun({
  config,
  uiState,
  handlers: {
    navigate: (data) => {
      // data is the raw `value` field from the CUSTOM event
      router.push((data as { path: string }).path);
    },
    showToast: (data) => {
      toast((data as { message: string }).message);
    },
  },
});

The handler type is (data: unknown) => void. Cast data to the shape your agent sends.


State

agentState.runs — completed turns

An array of AgentRun objects, one per completed agent turn. Each run holds everything that happened between RUN_STARTED and RUN_FINISHED:

import type { AgentRun, RunItem, ReasoningState } from '@agilab/react';

interface AgentRun {
  runId: string;
  source: 'user' | 'agent'; // 'agent' when the agent initiates a run autonomously
  userInput?: string;        // the user's message (when source === 'user')
  items: RunItem[];          // text messages and tool calls in arrival order
  reasoning?: ReasoningState;
  isStreaming: boolean;
  status: 'streaming' | 'finished' | 'error';
  error?: string;            // set when status === 'error'
  timestamp: number;
}

interface ReasoningState {
  content: string;
  isComplete: boolean;       // false while the thinking block is still streaming
}

// Each item is either a streamed text message or a tool call, in arrival order
type RunItem =
  | {
      kind: 'text';
      messageId: string;
      content: string;
      isComplete: boolean;  // false while tokens are still arriving; true after TEXT_MESSAGE_END
    }
  | {
      kind: 'tool';
      toolCallId: string;
      toolCallName: string;
      argsAccumulated: string;  // raw JSON, grows with each TOOL_CALL_ARGS delta
      argsComplete: boolean;    // true after TOOL_CALL_END
      result?: string;          // set on TOOL_CALL_RESULT
      status: 'streaming' | 'done' | 'has-result' | 'error';
    }

agentState.currentRun — the live run

Same shape as AgentRun, updated in real time while streaming. undefined when idle.

// Combine completed + in-progress for rendering
const allRuns = [...agentState.runs, ...(agentState.currentRun ? [agentState.currentRun] : [])];

allRuns.map(run => (
  <div key={run.runId}>
    {run.userInput && <Bubble direction="out">{run.userInput}</Bubble>}
    <Bubble direction="in">
      {/* reasoning block appears before items if the agent emitted extended thinking */}
      {run.reasoning && <ReasoningBlock reasoning={run.reasoning} />}

      {/* items render in the exact order they arrived from the stream */}
      {run.items.map(item =>
        item.kind === 'tool'
          ? <ToolCallCard key={item.toolCallId} tc={item} />
          : (
            <span key={item.messageId}>
              {item.content}
              {/* streaming cursor — visible while this message is still being typed */}
              {!item.isComplete && <span className="animate-pulse">▊</span>}
            </span>
          )
      )}
    </Bubble>
  </div>
))

agentState.status

'idle'       — no active run
'connecting' — request sent, waiting for first event
'streaming'  — events arriving
'finished'   — RUN_FINISHED received
'error'      — stream error or RUN_ERROR event

agentState.error

Set when status === 'error'. Contains the error message string from the agent or the network layer.

agentState.threadId

The conversation session ID shared across all runs. Automatically generated on first run and kept stable throughout the component's lifetime. Pass it to your own storage layer if you want to persist and resume conversations.

agentState.currentStep

Set while a STEP_STARTED event is active (e.g. a named agent step like "search" or "plan"). Cleared on STEP_FINISHED. Useful for status bars.

{agentState.currentStep && (
  <span>Step: {agentState.currentStep.name}</span>
)}

Tool call items

Tool call items in run.items stream in real time:

// item.kind === 'tool'
{
  kind: 'tool';
  toolCallId: string;
  toolCallName: string;
  argsAccumulated: string; // raw JSON, grows with each TOOL_CALL_ARGS delta
  argsComplete: boolean;   // true after TOOL_CALL_END
  result?: string;         // set on TOOL_CALL_RESULT
  status: 'streaming' | 'done' | 'has-result' | 'error';
}
function ToolCallCard({ tc }: { tc: RunItem & { kind: 'tool' } }) {
  return (
    <div>
      <strong>{tc.toolCallName}</strong>
      <pre>
        {tc.argsComplete
          ? JSON.stringify(JSON.parse(tc.argsAccumulated), null, 2)
          : tc.argsAccumulated}
      </pre>
      {tc.result && <pre>Result: {tc.result}</pre>}
    </div>
  );
}

Common patterns

Detecting streaming / disabling input

const isStreaming =
  agentState.status === 'streaming' || agentState.status === 'connecting';

<button onClick={abort} disabled={!isStreaming}>Stop</button>
<input disabled={isStreaming} onSubmit={sendMessage} />

Showing a typing indicator

{isStreaming && <TypingIndicator />}

Handling errors

{agentState.status === 'error' && (
  <div className="error">{agentState.error ?? 'Unknown error'}</div>
)}

Re-sending cancels the active run

sendMessage automatically aborts any in-progress run before starting a new one. You don't need to call abort first.

Conversation history is managed automatically

The library maintains the full wire-format message history internally and sends it to the agent on every run. You don't need to build or track a messages array — just render from agentState.runs.


License

MIT