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

dooers-agents-client

v0.2.3

Published

agents-client — react sdk for agents-server

Downloads

469

Readme

dooers-agents-client

React agents client SDK for agents server SDK.

Install

npm install dooers-agents-client

Peer dependency: react >= 18

Quick Start

Wrap your component tree in WorkerProvider, then use hooks anywhere inside.

import { WorkerProvider, useConnection, useThreadDetails, useMessage } from "dooers-agents-client"

function App() {
  return (
    <WorkerProvider
      url="ws://localhost:8000/ws"
      workerId="worker-1"
      userId="user-1"
      userName="Alice"
    >
      <Chat threadId="thread-1" />
    </WorkerProvider>
  )
}

function Chat({ threadId }: { threadId: string }) {
  const { status } = useConnection()
  const { events, isWaiting } = useThreadDetails(threadId)
  const { send } = useMessage()

  return (
    <div>
      <p>{status}</p>

      {events.map((event) => (
        <div key={event.id}>
          <strong>{event.actor}</strong>
          {event.content?.map((part, i) =>
            part.type === "text" ? <p key={i}>{part.text}</p> : null
          )}
        </div>
      ))}

      {isWaiting && <p>Thinking...</p>}

      <button onClick={() => send({ text: "Hello!", threadId })}>
        Send
      </button>
    </div>
  )
}

Provider

WorkerProvider manages the WebSocket lifecycle. On mount it connects, on unmount it disconnects. When any prop changes, the connection resets.

<WorkerProvider
  url="ws://localhost:8000/ws"   // WebSocket endpoint
  workerId="worker-1"            // Worker to connect to
  organizationId="org-1"         // Context passed to handler
  workspaceId="ws-1"
  userId="user-1"
  userName="Alice"
  userEmail="[email protected]"
  systemRole="user"              // "admin" | "user" (default: "user")
  organizationRole="member"      // "owner" | "manager" | "member" (default: "member")
  workspaceRole="member"         // "manager" | "member" (default: "member")
  authToken="sk-..."             // Optional authentication
  onError={(err) => console.error(err.code, err.message)}
>
  {children}
</WorkerProvider>

All props except children and onError are primitive strings, so React can diff them cheaply.

Hooks

All hooks must be called inside a WorkerProvider.

useConnection

const { status, error, reconnectFailed, reconnect } = useConnection()

// status: "idle" | "connecting" | "connected" | "disconnected" | "error"

Auto-reconnects up to 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s). When all attempts fail, reconnectFailed becomes true and you can offer a manual retry button calling reconnect().

useThreadDetails

const { thread, events, runs, isLoading, isWaiting } = useThreadDetails(threadId)

Subscribes on mount, unsubscribes on unmount. Refcounted internally, so multiple components subscribing to the same thread share a single WebSocket subscription.

events includes optimistic messages that appear instantly when sending, then get replaced by the server-confirmed version. isWaiting is true while any run has status "running".

useThreadEvents

Load older messages beyond the initial snapshot.

const { hasOlderEvents, loadOlderEvents } = useThreadEvents(threadId)

if (hasOlderEvents) {
  loadOlderEvents(50)  // load 50 more
}

useThreadsList

Read-only thread list for the connected worker.

const { threads, isLoading, hasMore, totalCount } = useThreadsList()

Threads load automatically on connect. totalCount reflects the server-reported total.

useThreadsActions

Thread mutations (delete, load more).

const { deleteThread, loadMore } = useThreadsActions()

loadMore() for cursor-based pagination. deleteThread(id) broadcasts deletion to all connected clients.

useMessage

const { send } = useMessage()

// Existing thread
send({ text: "Hello!", threadId: "t-1" })

// New thread (omit threadId — returns the created thread ID)
const { threadId } = await send({ text: "Start a conversation" })

// Rich content
send({
  threadId: "t-1",
  content: [
    { type: "text", text: "Check this out:" },
    { type: "image", url: "https://...", mimeType: "image/png" },
  ],
})

useFeedback

const { feedback, like, dislike } = useFeedback(eventId, "event")

// feedback: "like" | "dislike" | null
like()
dislike("The response was incorrect")

Second argument is the target type: "event", "run", or "thread".

useAnalytics

Live event stream. Subscribe and unsubscribe explicitly.

const { events, counters, subscribe, unsubscribe } = useAnalytics()

useEffect(() => {
  subscribe()
  return () => unsubscribe()
}, [subscribe, unsubscribe])

// events: AnalyticsEvent[] — latest events
// counters: Record<string, number> — aggregated counters

useSettings

Worker settings with real-time sync across clients.

const { fields, updatedAt, isLoading, subscribe, unsubscribe, patchField } = useSettings()

// fields: (SettingsField | SettingsFieldGroup)[]
patchField("model", "gpt-4o")

Subscribe explicitly — designed so you only receive updates when the settings UI is visible:

useEffect(() => {
  subscribe()
  return () => unsubscribe()
}, [subscribe, unsubscribe])

Types

All public types are camelCase. The SDK transforms the wire format (snake_case) internally.

import type {
  User,              // { userId, userName, userEmail, systemRole, organizationRole, workspaceRole }
  Thread,            // { id, workerId, organizationId, workspaceId, owner, users, title, ... }
  ThreadEvent,       // { id, threadId, type, actor, author, user, content, data, createdAt, ... }
  Run,               // { id, threadId, agentId, status, startedAt, endedAt, error }
  ContentPart,       // TextPart | ImagePart | DocumentPart
  ConnectionStatus,  // "idle" | "connecting" | "connected" | "disconnected" | "error"
  Actor,             // "user" | "assistant" | "system" | "tool"
  EventType,         // "message" | "tool.call" | "tool.result" | "tool.transaction" | ...
  RunStatus,         // "running" | "succeeded" | "failed" | "canceled"
  ThreadState,       // { thread, events, runs, isLoading, isWaiting }
  SettingsField,
  SettingsFieldGroup,
  SettingsItem,      // SettingsField | SettingsFieldGroup
  AnalyticsEvent,
  FeedbackType,      // "like" | "dislike"
  FeedbackTarget,    // "event" | "run" | "thread"
} from "dooers-agents-client"

import { isSettingsFieldGroup } from "dooers-agents-client"  // type guard

Architecture

WorkerProvider
  ├── WorkerClient       WebSocket lifecycle, reconnection, optimistic events
  └── WorkerStore        Zustand vanilla store
        ├── connection   { status, error, reconnectFailed }
        ├── threads      Record<id, Thread>
        ├── events       Record<threadId, ThreadEvent[]>
        ├── optimistic   Record<threadId, ThreadEvent[]>  (merged into events by useThreadDetails)
        ├── runs         Record<threadId, Run[]>
        ├── settings     { fields, updatedAt, isLoading }
        ├── analytics    { events, counters }
        └── feedback     Record<targetId, FeedbackType>

The client handles:

  • Optimistic messages — shown immediately on send, reconciled when the server confirms via clientEventId
  • Refcounted subscriptions — multiple useThreadDetails(id) calls share one WebSocket subscription
  • Gap recovery — on reconnect, sends afterEventId to resume from where the client left off
  • Exponential backoff — reconnects at 1s, 2s, 4s, 8s, 16s intervals before giving up

See Also