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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@livefire2015/solid-ag-chat

v1.0.7

Published

SolidJS chat components with official AG-UI protocol support

Readme

@livefire2015/solid-ag-chat v1

SolidJS library for building chat UIs with official AG-UI protocol support.

npm version License: MIT

What This Library Provides

This is a library - not a ready-to-use chat UI. It provides:

  • SolidJS Primitives - Provider, hooks for building custom chat UIs
  • Official AG-UI SDK Client - Stateless agent execution with streaming
  • Reactive State Management - Client-side state with conversation context
  • Type Safety - Full TypeScript types for AG-UI protocol
  • Mock Client - Testing infrastructure included

You build the UI - This library handles the protocol, state, and reactivity.

AG-UI Protocol Overview

AG-UI is a stateless agent protocol using the official @ag-ui/client SDK:

┌─────────────┐    RxJS Observable     ┌─────────────┐
│   Browser   │◄───────────────────────│   Server    │
│   Client    │     /agent/run         │  (Python)   │
│  (SolidJS)  │───────────────────────►│ (PydanticAI)│
└─────────────┘                        └─────────────┘

Key Features:

  • 📨 Stateless Execution: Client sends full conversation history with each request
  • 🔄 Streaming Events: TEXT_MESSAGE_START/CONTENT/END, TOOL_CALL_*, etc.
  • 🛠️ Tool Calls: Official toolCalls array in messages
  • 💬 Conversation Management: Client-side multi-turn context tracking

Installation

npm install @livefire2015/solid-ag-chat

Peer dependencies:

  • solid-js@^1.8.0
  • @ag-ui/core@^0.0.39
  • @ag-ui/client@^0.0.39
  • rxjs@^7.8.1

Quick Start

1. Wrap Your App with ChatProvider

import { ChatProvider, createSdkAgent } from '@livefire2015/solid-ag-chat';

function App() {
  // Create official AG-UI SDK client
  const client = createSdkAgent({
    baseUrl: 'http://localhost:8000',
    headers: {
      'Authorization': 'Bearer YOUR_TOKEN'  // Optional auth
    },
    agentEndpoint: '/agent/run',              // Default: /agent/run
    conversationsEndpoint: '/conversations'    // Default: /conversations
  });

  return (
    <ChatProvider client={client}>
      <YourChatUI />
    </ChatProvider>
  );
}

2. Build Your UI with Hooks

import { useConversationList, useConversation } from '@livefire2015/solid-ag-chat';
import { For, onMount } from 'solid-js';

function YourChatUI() {
  // Manage conversation list
  const {
    conversations,
    activeId,
    load,
    create,
    setActive
  } = useConversationList();

  // Work with active conversation
  const { messages, isStreaming, send } = useConversation();

  // Load conversations on mount
  onMount(() => load());

  return (
    <div class="flex h-screen">
      {/* Conversation List */}
      <aside class="w-64 border-r">
        <button onClick={() => create('New Chat')}>+ New Chat</button>
        <For each={conversations()}>
          {conv => (
            <button
              onClick={() => setActive(conv.id)}
              class={activeId() === conv.id ? 'active' : ''}
            >
              {conv.title}
            </button>
          )}
        </For>
      </aside>

      {/* Messages */}
      <main class="flex-1 flex flex-col">
        <div class="flex-1 overflow-auto">
          <For each={messages()}>
            {msg => (
              <div class={`message ${msg.role}`}>
                {/* Text content */}
                {msg.content && <p>{msg.content}</p>}

                {/* Tool calls */}
                {msg.toolCalls && (
                  <For each={msg.toolCalls}>
                    {tc => (
                      <div class="tool-call">
                        🔧 {tc.function.name}({tc.function.arguments})
                      </div>
                    )}
                  </For>
                )}
              </div>
            )}
          </For>
          {isStreaming() && <div class="typing-indicator">...</div>}
        </div>

        {/* Input */}
        <form onSubmit={e => {
          e.preventDefault();
          const input = e.target.elements.message;
          send(input.value);  // Sends to /agent/run with conversation history
          input.value = '';
        }}>
          <input name="message" placeholder="Type a message..." />
          <button type="submit">Send</button>
        </form>
      </main>
    </div>
  );
}

API Reference

ChatProvider

Provides AG-UI client and reactive state to all children.

interface ChatProviderProps {
  client: AgUiClient;              // Required: AG-UI client instance
  upload?: (files: File[]) => Promise<AttachmentDoc[]>;  // Optional: file upload handler
  sessionId?: string;              // Optional: session identifier
  initialConversationId?: string;  // Optional: initial conversation to load
  children: JSX.Element;
}

Example:

<ChatProvider
  client={createSdkAgent({ baseUrl: 'http://localhost:8000' })}
  upload={async (files) => {
    // Upload files to your server
    const formData = new FormData();
    files.forEach(f => formData.append('files', f));
    const res = await fetch('/upload', { method: 'POST', body: formData });
    return res.json();
  }}
>
  <App />
</ChatProvider>

useConversationList()

Hook for managing the conversation list.

interface UseConversationListReturn {
  conversations: () => ConversationDoc[];     // Reactive conversation list
  activeId: () => Id | undefined;             // Current conversation ID
  load: () => Promise<void>;                  // Load conversations from server
  create: (title?, metadata?) => Promise<ConversationDoc>;  // Create new conversation
  setActive: (id: Id) => void;                // Switch conversation
  archive: (id: Id) => Promise<void>;         // Archive conversation
}

Example:

function ConversationSidebar() {
  const { conversations, activeId, load, create, setActive } = useConversationList();

  onMount(() => load());  // Load on mount

  return (
    <div>
      <button onClick={() => create('New Chat')}>+ New</button>
      <For each={conversations()}>
        {conv => (
          <button
            onClick={() => setActive(conv.id)}
            class={activeId() === conv.id ? 'active' : ''}
          >
            {conv.title}
          </button>
        )}
      </For>
    </div>
  );
}

useConversation(conversationId?)

Hook for a specific conversation's messages and actions.

interface UseConversationReturn {
  messages: () => MessageDoc[];               // Reactive message list
  isStreaming: () => boolean;                 // Is any message streaming?
  load: () => Promise<void>;                  // Load messages from server
  send: (text: string, opts?) => Promise<void>;  // Send message (streams via /agent/run)
  cancel: (messageId: Id) => Promise<void>;   // Cancel streaming message
}

Example:

function MessageView() {
  const { messages, isStreaming, load, send, cancel } = useConversation();

  onMount(() => load());  // Load messages on mount

  return (
    <div>
      <For each={messages()}>
        {msg => (
          <div>
            <MessageBubble message={msg} />
            {msg.status === 'streaming' && (
              <button onClick={() => cancel(msg.id)}>Cancel</button>
            )}
          </div>
        )}
      </For>
      {isStreaming() && <TypingIndicator />}
      <input onSubmit={e => send(e.target.value)} />
    </div>
  );
}

useMessages(conversationId?)

Get reactive messages array for a conversation.

const messages: () => MessageDoc[] = useMessages(conversationId);

Example:

function MessageList(props: { conversationId: string }) {
  const messages = useMessages(props.conversationId);

  return (
    <For each={messages()}>
      {msg => <div>{msg.content}</div>}
    </For>
  );
}

useStreamingText(messageId)

Get current streaming text for a message (if streaming).

const streamingText: () => string = useStreamingText(messageId);

Example:

function MessageBubble(props: { message: MessageDoc }) {
  const streamingText = useStreamingText(props.message.id);

  return (
    <div>
      {props.message.status === 'streaming' ? (
        <p>{streamingText()}</p>
      ) : (
        <p>{props.message.content}</p>
      )}
    </div>
  );
}

Message Structure

Messages follow the official AG-UI schema with content for text and toolCalls for tool interactions:

interface Message {
  id: string;
  role: 'user' | 'assistant' | 'system' | 'tool' | 'developer';
  content: string;                    // Text content
  toolCalls?: ToolCall[];            // Tool invocations (assistant messages)

  // Custom fields for conversation management
  conversationId?: string;
  status?: 'streaming' | 'completed' | 'errored' | 'canceled';
  usage?: { prompt?: number; completion?: number; total?: number };
  createdAt?: string;
  metadata?: Record<string, unknown>;
  attachments?: string[];            // Attachment IDs
}

interface ToolCall {
  id: string;
  type: 'function';
  function: {
    name: string;
    arguments: string;               // JSON string
  };
}

Rendering Messages:

<For each={messages()}>
  {msg => (
    <div class={`message-${msg.role}`}>
      {/* Text content */}
      {msg.content && <p class="content">{msg.content}</p>}

      {/* Tool calls */}
      {msg.toolCalls && (
        <For each={msg.toolCalls}>
          {tc => {
            const args = JSON.parse(tc.function.arguments);
            return (
              <div class="tool-call">
                <span class="tool-icon">🔧</span>
                <span class="tool-name">{tc.function.name}</span>
                <pre class="tool-args">{JSON.stringify(args, null, 2)}</pre>
              </div>
            );
          }}
        </For>
      )}
    </div>
  )}
</For>

Advanced Usage

Custom Transport (Bring Your Own Client)

import type { AgUiClient } from '@livefire2015/solid-ag-chat';

class MyCustomClient implements AgUiClient {
  on(type, handler) { /* ... */ }
  off(type, handler) { /* ... */ }
  async createConversation(title?, metadata?) { /* ... */ }
  async sendMessage(convId, text, opts?) { /* ... */ }
  // ... implement other methods
  close() { /* ... */ }
}

<ChatProvider client={new MyCustomClient()}>
  <App />
</ChatProvider>

File Upload Handling

<ChatProvider
  client={client}
  upload={async (files) => {
    // 1. Upload files to your server
    const formData = new FormData();
    files.forEach(f => formData.append('files', f));
    const res = await fetch('/api/upload', {
      method: 'POST',
      body: formData,
    });

    // 2. Return attachment metadata
    const attachments = await res.json();
    return attachments.map(att => ({
      id: att.id,
      name: att.filename,
      mime: att.contentType,
      size: att.size,
      url: att.downloadUrl,
      state: 'available' as const,
    }));
  }}
>
  <App />
</ChatProvider>

Testing with MockAgClient

import { MockAgClient, ChatProvider } from '@livefire2015/solid-ag-chat';

function DevApp() {
  const mockClient = new MockAgClient({
    tokenDelayMs: 30,  // Simulate streaming
    replyGenerator: (userText) => {
      return `Mock: ${userText}`.match(/.{1,5}/g) || [];
    }
  });

  return (
    <ChatProvider client={mockClient}>
      <App />
    </ChatProvider>
  );
}

Backend Integration

Your server must implement the official AG-UI protocol. Example with PydanticAI:

from fastapi import FastAPI
from pydantic_ai import Agent
from pydantic_ai.messages import ModelMessage
from pydantic_ai.models.openai import OpenAIModel
from ag_ui.server import stream_run_events

app = FastAPI()
agent = Agent(OpenAIModel('gpt-4'))

# Conversation CRUD (custom endpoints)
@app.get("/conversations")
async def list_conversations():
    return [{"id": "c1", "title": "Chat 1", "createdAt": "...", ...}]

@app.post("/conversations")
async def create_conversation(payload: dict):
    conv = create_new_conversation(payload.get('title'))
    return conv

@app.get("/conversations/{id}/messages")
async def get_messages(id: str):
    return [{"id": "m1", "conversationId": id, "role": "user", "content": "Hello"}]

# Official AG-UI agent execution endpoint
@app.post("/agent/run")
async def run_agent(input: RunAgentInput):
    """
    Official AG-UI endpoint for stateless agent execution.
    Client sends full conversation history with each request.
    """
    # Convert AG-UI messages to PydanticAI format
    message_history = [
        ModelMessage(role=msg['role'], content=msg['content'])
        for msg in input.messages
    ]

    # Run agent and stream AG-UI events
    result = await agent.run(
        user_prompt=input.messages[-1]['content'],
        message_history=message_history[:-1]
    )

    # Stream official AG-UI events (TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, etc.)
    return StreamingResponse(
        stream_run_events(result),
        media_type='text/event-stream'
    )

Official AG-UI Events

The SDK client emits these official AG-UI events during streaming:

Message Events

| Event | Description | |-------|-------------| | TEXT_MESSAGE_START | Text message streaming started | | TEXT_MESSAGE_CONTENT | Text delta (incremental content) | | TEXT_MESSAGE_END | Text message completed |

Tool Events

| Event | Description | |-------|-------------| | TOOL_CALL_START | Tool call started | | TOOL_CALL_ARGS | Tool arguments delta | | TOOL_CALL_END | Tool call completed | | TOOL_CALL_RESULT | Tool result available |

State Events

| Event | Description | |-------|-------------| | STATE_SNAPSHOT | Full state snapshot | | STATE_DELTA | JSON Patch state delta | | MESSAGES_SNAPSHOT | Messages snapshot |

Custom Conversation Events

| Event | Description | |-------|-------------| | conversation.created | New conversation created | | conversation.updated | Conversation changed | | conversation.archived | Conversation archived | | message.created | User message created | | message.errored | Message failed | | message.canceled | Message canceled |

Migration from v0.x

If you're upgrading from the old parts[] system:

Before (v0.x):

<For each={msg.parts}>
  {part => (
    <>
      {part.kind === 'text' && <p>{part.text}</p>}
      {part.kind === 'tool_call' && <div>{part.name}</div>}
    </>
  )}
</For>

After (v1.0):

{msg.content && <p>{msg.content}</p>}
{msg.toolCalls && (
  <For each={msg.toolCalls}>
    {tc => <div>{tc.function.name}</div>}
  </For>
)}

Client Migration:

// Before: SseAgClient with REST + SSE
const client = new SseAgClient({
  baseUrl: 'http://localhost:8000',
  paths: { conversations: '/conversations', messages: '/conversations/:id/messages' }
});

// After: Official SDK client
const client = createSdkAgent({
  baseUrl: 'http://localhost:8000',
  agentEndpoint: '/agent/run',              // Stateless agent execution
  conversationsEndpoint: '/conversations'    // Conversation management
});

License

MIT

Links