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

astryx-agui

v0.1.0

Published

Headless React binding layer between AG-UI and Astryx Chat components.

Readme

astryx-agui

Headless React binding layer between AG-UI and Astryx Chat components. It consumes an AG-UI agent's event stream, normalizes it into a predictable chat state (messages, tool calls, reasoning, citations, sources, run status), and exposes small hooks that hand you exactly the props Astryx's @astryxdesign/core/Chat components expect — no UI of its own.

Install

npm install astryx-agui @ag-ui/client @ag-ui/core @astryxdesign/core react

@ag-ui/client, @ag-ui/core, @astryxdesign/core, and react are peer dependencies — install the versions your app already uses.

Quick start

Create a store from an AG-UI agent, wrap your tree in <AguiProvider>, and render Astryx components with the hook output.

import { HttpAgent } from '@ag-ui/client';
import { createAguiStore, AguiProvider, useAguiMessages, useAguiComposer } from 'astryx-agui';
import {
  ChatLayout,
  ChatMessageList,
  ChatMessage,
  ChatMessageBubble,
  ChatComposer,
} from '@astryxdesign/core/Chat';

const agent = new HttpAgent({ url: 'https://your-backend/agent' });
const store = createAguiStore(agent);

function App() {
  return (
    <AguiProvider store={store}>
      <Chat store={store} />
    </AguiProvider>
  );
}

function Chat({ store }: { store: ReturnType<typeof createAguiStore> }) {
  const { messages, messageListProps } = useAguiMessages();
  // The composer's send path needs `store.agent`, so pass the store explicitly.
  const { composerProps } = useAguiComposer({}, store);

  return (
    <ChatLayout>
      <ChatMessageList {...messageListProps}>
        {messages.map((m) => (
          <ChatMessage key={m.id} {...m.bubbleProps}>
            <ChatMessageBubble>{m.content}</ChatMessageBubble>
          </ChatMessage>
        ))}
      </ChatMessageList>
      <ChatComposer {...composerProps} />
    </ChatLayout>
  );
}

Each AguiMessageView gives you id, content, isStreaming, bubbleProps ({ sender }), toolCallIds, citations, and reasoning.

Passing the store explicitly: hooks resolve the store from <AguiProvider> by default. useAguiComposer still needs the store to reach store.agent when sending, so call it as useAguiComposer({}, store). All hooks accept an optional trailing store argument if you prefer not to use the provider.

Streaming

Message views carry content and isStreaming. Apply Astryx's useStreamingText where you render the bubble text so partial tokens animate in as they arrive:

import { useStreamingText } from '@astryxdesign/core/Chat';

function Bubble({ content, isStreaming }: { content: string; isStreaming: boolean }) {
  const text = useStreamingText(content, isStreaming);
  return <ChatMessageBubble>{text}</ChatMessageBubble>;
}

// in the message map:
messages.map((m) => (
  <ChatMessage key={m.id} {...m.bubbleProps}>
    <Bubble content={m.content} isStreaming={m.isStreaming} />
  </ChatMessage>
));

Tool calls

useAguiToolCalls(messageId, options?, store?) returns { toolCallProps: { calls } }, where calls are real Astryx ChatToolCallItem[] (duration is a preformatted string like "340ms" / "1.2s"). Use resolvers — keyed by tool name — to turn raw arguments into display fields:

import { useAguiToolCalls } from 'astryx-agui';
import { ChatToolCalls } from '@astryxdesign/core/Chat';

function ToolCalls({ messageId }: { messageId: string }) {
  const { toolCallProps } = useAguiToolCalls(messageId, {
    resolvers: {
      read: (args: { path: string }) => ({ target: args.path }),
    },
  });
  return <ChatToolCalls {...toolCallProps} />;
}

A resolver returns a Partial<ChatToolCallItem> that is merged over the mechanical defaults (name, status, duration, resultDetail, errorMessage).

Citations

useAguiCitations(messageId, store?) parses OpenAI-style inline citation markers out of the message text, resolves each one against the registered sources, and shapes props for Astryx's <Markdown> plus a source list:

import { useAguiCitations } from 'astryx-agui';
import { Markdown } from '@astryxdesign/core/Chat';

function CitedBubble({ messageId }: { messageId: string }) {
  const { markdownProps, sourceListProps } = useAguiCitations(messageId);
  return (
    <>
      <Markdown {...markdownProps} />
      <SourceList {...sourceListProps} />
    </>
  );
}
  • markdownProps{ children, inlinePlugins } for <Markdown> (the inline plugin renders citation tokens).
  • citations → the resolved citations (each parsed citation joined with its source, if known).
  • sourceListProps{ sources }, deduplicated in first-seen order.

Sources arrive by default on the CUSTOM_CITATION_SOURCE custom event channel. See BACKEND.md for how to emit markers and source events from your agent.

Attachments

useAguiAttachments(options?) manages a pending attachment list and encodes files to AG-UI input content:

import { useAguiAttachments, useAguiComposer } from 'astryx-agui';

function Composer({ store }: { store: ReturnType<typeof createAguiStore> }) {
  const { add, remove, clear, drawerProps, tokenProps, encodeAll } = useAguiAttachments();
  const { composerProps } = useAguiComposer({ attachments: encodeAll }, store);

  return (
    <>
      <AttachmentDrawer {...drawerProps} />
      {tokenProps.map((t) => (
        <AttachmentToken key={t.id} {...t} />
      ))}
      <ChatComposer {...composerProps} onFiles={add} />
    </>
  );
}
  • add(files) / remove(id) / clear() manage the pending list.
  • drawerProps{ count }, tokenProps → per-file { id, label, onRemove }.
  • encodeAll() resolves to EncodedInputContent[]; pass it as useAguiComposer's attachments option to send them alongside the text.

The low-level encodeFile(file) and inputContentType(mime) helpers are exported too.

Pluggable store

createAguiStore(agent, options?) defaults to an internal vanilla store, but you can bring your own state container (Zustand, Redux, etc.) via the adapter option. An adapter satisfies this interface:

interface StoreAdapter<S> {
  getState(): S;
  setState(updater: (prev: S) => S): void;
  subscribe(listener: () => void): () => void;
}

A Zustand adapter is a thin wrapper around a vanilla Zustand store — its getState / setState / subscribe map almost one-to-one. The adapter must be seeded with a valid initial AguiState:

import { createStore } from 'zustand/vanilla';
import type { StoreAdapter, AguiState } from 'astryx-agui';

const initialState: AguiState = {
  messageOrder: [],
  messages: {},
  toolCalls: {},
  sources: {},
  runStatus: 'idle',
};

function createZustandAdapter(initial: AguiState): StoreAdapter<AguiState> {
  const store = createStore<AguiState>()(() => initial);

  return {
    getState: () => store.getState(),
    // pass `true` to replace instead of shallow-merge, since the
    // reducer's updater already returns the complete next state
    setState: (updater) => store.setState((prev) => updater(prev), true),
    subscribe: (listener) => store.subscribe(listener),
  };
}

const myAdapter = createZustandAdapter(initialState);
import { createAguiStore } from 'astryx-agui';
import type { AguiState } from 'astryx-agui';

const store = createAguiStore(agent, {
  adapter: myAdapter, // StoreAdapter<AguiState>
  // citation configuration is also accepted here:
  // markers, citationSourceEvents, citationSourceTools, parseSource
});

Call store.dispose() to unsubscribe from the agent when you tear down.

Typically you create the store once for the lifetime of the component that owns the chat, and dispose it on unmount. With React that's a useEffect cleanup:

import { useState, useEffect } from 'react';
import { HttpAgent } from '@ag-ui/client';
import { createAguiStore, AguiProvider } from 'astryx-agui';

function ChatContainer() {
  // create the store once — not on every render
  const [store] = useState(() =>
    createAguiStore(new HttpAgent({ url: 'https://your-backend/agent' })),
  );

  // unsubscribe from the agent when this component unmounts
  useEffect(() => () => store.dispose(), [store]);

  return (
    <AguiProvider store={store}>
      <Chat store={store} />
    </AguiProvider>
  );
}

Outside React, call it wherever you dispose of the owning scope — e.g. when closing a chat session, navigating away, or during server/test teardown:

const store = createAguiStore(agent);
try {
  // ...drive the agent, read state...
} finally {
  store.dispose();
}

Escape hatch

Need something the shaped props don't cover? useAguiMessages().raw exposes the full normalized AguiState (messages, tool calls, sources, reasoning, run status), so you can read any slice directly:

const { raw } = useAguiMessages();
raw.runStatus; // 'idle' | 'running' | 'done' | 'error'
raw.toolCalls; // Record<string, NormalizedToolCall>
raw.sources;   // Record<string, CitationSource>

API reference

Hooks (each accepts an optional trailing store argument):

  • useAguiMessages(store?){ messages, messageListProps, raw }
  • useAguiComposer(options?, store?){ composerProps, isRunning, stop }
  • useAguiAttachments(options?){ add, remove, clear, drawerProps, tokenProps, encodeAll, isEmpty }
  • useAguiToolCalls(messageId, options?, store?){ toolCallProps }
  • useAguiReasoning(messageId, store?)
  • useAguiCitations(messageId, store?){ markdownProps, citations, sourceListProps }
  • useAguiSources(store?){ sources }
  • useAguiRun(store?){ status, error, isRunning }
  • useAguiChat(options?, store?) — facade combining messages, composer, and run status
  • useAguiSelector(selector, store?) — subscribe to any slice of AguiState

Store & provider:

  • createAguiStore(agent, options?) / AguiProvider / useAguiContext()
  • createVanillaStore(initialState) and the StoreAdapter interface

Citations & attachments utilities:

  • parseCitations, createCitationPlugin, matchCitationToken
  • encodeFile, inputContentType
  • CITATION_MARKERS, CITATION_SOURCE_EVENT, CITATION_PROMPT_INSTRUCTIONS

See BACKEND.md for the agent-side contract (emitting citation markers and source events).

License

MIT