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

@openhex-ai/agent-sdk

v0.3.0

Published

Build AI agents on the Openhex platform. Programmable agent loop, tools, hooks, sessions, MCP, and an embeddable React chat UI — the same runtime that powers Openhex agents, as a library.

Readme

@openhex-ai/agent-sdk

Build AI agents on the Openhex platform. The same agent loop, built-in tools, hooks, sessions, and MCP support that power Openhex agents — exposed as a programmable TypeScript library.

Status. The chat API, the React chat UI (/react), and the workspace API are implemented — all speak the exact protocol the platform exposes (the user-side apps' conversations protocol and the published /api/v2/workspaces/* surface). The local query() agent loop is still a scaffold (throws NotImplementedError) and lands in a follow-up MR.

Install

npm install @openhex-ai/agent-sdk
# or
pnpm add @openhex-ai/agent-sdk

Authentication

Get an API key (mysta_…) from the Openhex console, then set it as an environment variable (or pass { apiKey } to the client):

export OPENHEX_API_KEY=mysta_...

Chat with an agent

The chat API mirrors the user-side apps' conversations protocol: a message creates (or continues) a conversation and routes it to the agent (auto-provisioning the agent's pod on first contact); the reply streams back over SSE until the turn completes.

import { OpenhexClient } from '@openhex-ai/agent-sdk';

const client = new OpenhexClient({ agentId: '…' /* apiKey from env */ });

// Simplest: send and await the aggregated turn (creates a conversation).
const turn = await client.sendMessage('Summarize the latest support tickets');
console.log(turn.text);
console.log(turn.toolCalls); // tools the agent used
console.log(turn.conversationId); // reuse to continue the chat

Multi-turn conversations

A Conversation remembers its id so follow-ups keep context:

const convo = client.conversation();
await convo.send('Remember my project is called Orbit.');
const turn = await convo.send('What is my project called?'); // → "Orbit"

Streaming

import { extractText, isTurnComplete } from '@openhex-ai/agent-sdk';

for await (const record of client.runTurn('Group them by theme')) {
  if (record.sender === 'assistant') process.stdout.write(extractText(record));
  if (isTurnComplete(record)) break;
}

Low-level protocol

client.chat exposes the wire 1:1 when you need full control:

// POST /conversations/send — create (targetAgentIds) or continue (conversationId)
const { conversationId, userEventId } = await client.chat.send({
  message: 'hello',
  targetAgentIds: [agentId],
});

// GET /conversations/:id/stream — resumable (auto-reconnects with backoff).
// Resume from a record's `id`, or from `userEventId` to capture just the reply.
for await (const record of client.chat.stream(conversationId, { lastEventId: userEventId })) {
  console.log(record.sender, record.raw.type);
  if (record.raw.type === 'result') break;
}

await client.chat.interrupt(conversationId); // POST /conversations/:id/interrupt
const { entries } = await client.chat.messages(conversationId); // full history

Each stream record is { id, seq, sender, event, timestamp, sessionId, raw } where raw carries the agent-runtime event ({ type: 'assistant', message: { content: […] } }, { type: 'result' }, etc.) — identical to what the app's chat UI consumes.

React chat UI (/react)

Drop a chat box onto any page and it talks to your agent over the same protocol. The React layer is a separate entry point so the core SDK stays framework-free; react / react-dom are peer dependencies and the styles are self-contained (no CSS import, no build step).

npm install @openhex-ai/agent-sdk react react-dom

Turnkey floating widget

import { ChatWidget } from '@openhex-ai/agent-sdk/react';

export function App() {
  return (
    <ChatWidget
      agentId="ce9382ad-…"
      token={sessionToken} // a member session token from YOUR backend
      title="HYROX 备赛教练"
      greeting="嗨!我是你的 HYROX 备赛教练,问我任何训练问题吧。"
      accentColor="#0f766e"
      theme="auto" // light | dark | auto
    />
  );
}

That renders a launcher bubble (bottom-right) that opens a panel — a corner panel on desktop, full-screen on mobile. Use <ChatBox> instead to embed the panel inline in your own layout (it fills its parent).

Browser-safe auth — never ship an API key

A mysta_… API key is a secret; it must never reach the browser. Mint a short-lived member session token on your backend and hand it to the widget:

// —— your backend (Node) ——
import { OpenhexClient } from '@openhex-ai/agent-sdk';
const openhex = new OpenhexClient({ apiKey: process.env.OPENHEX_API_KEY }); // sk_ws_… or owner JWT
app.post('/chat-token', async (req, res) => {
  const { token } = await openhex.workspace('acme').mintSession({
    sp_user_ref: req.user.id,
    ttl_seconds: 3600,
  });
  res.json({ token });
});
// —— your frontend ——
<ChatWidget
  agentId="…"
  getToken={async () => (await fetch('/chat-token', { method: 'POST' })).json().then(r => r.token)}
/>

getToken is called before every request, so an expiring token refreshes transparently. Pass a static token if you mint one per page load, or a fully built client (an OpenhexClient) for total control.

Headless hook — bring your own UI

useOpenhexChat owns the conversation state (messages, streaming, interrupt, retry) so you can render whatever you like:

import { useOpenhexChat } from '@openhex-ai/agent-sdk/react';

function MyChat() {
  const { messages, send, isResponding, interrupt } = useOpenhexChat({
    agentId: '…',
    getToken,
  });
  // …render `messages`, call `send(text)` / `interrupt()`
}

| Prop | Purpose | | ----------------------------------------------- | ---------------------------------------------------------- | | agentId | agent to route a new conversation to | | conversationId | resume an existing conversation (loads history) | | token / getToken | member session token (static or refreshing) | | client | a pre-built OpenhexClient / AgentChatClient (advanced) | | theme / accentColor | one-prop theming (light | dark | auto) | | greeting, title, avatarUrl, placeholder | presentation | | injectStyles={false} | opt out of the bundled CSS and style it yourself |

Workspaces

A workspace is a billing + membership boundary you (a service provider / partner) own. client.workspaces wraps the published /api/v2/workspaces/* API 1:1 — provision members, mint member sessions, grant credits, and read reporting. It speaks the same contract documented at /api/v2/workspaces/openapi.json.

Auth is the Bearer token the client is configured with:

| Token | Unlocks | | ----------------------------- | ----------------------------------------------------------------------------------------- | | Workspace API key (sk_ws_…) | whoami, member provision/suspend, sessions, member list, credit grants | | Owner user JWT | the above plus reporting (ledger, insights, member ledger) and API-key management | | none | the public SMS-signup endpoints (sendSmsCode / verifySmsCode) |

import { OpenhexClient } from '@openhex-ai/agent-sdk';

// A partner backend authenticates with its workspace API key.
const client = new OpenhexClient({ apiKey: 'sk_ws_…' });

// Confirm which workspace the key belongs to.
const me = await client.workspaces.whoami();

// A slug-bound handle drops the slug from every call.
const ws = client.workspace(me.slug);

// Provision a member (idempotent on your sp_user_ref), grant credits,
// then mint a session JWT for the member's client.
const member = await ws.provisionMember({ sp_user_ref: 'user-42', display_name: 'Ada' });
await ws.grantCredits(member.member_id, {
  amount: 500,
  idempotency_key: `bonus:${member.member_id}`,
});
const session = await ws.mintSession({ sp_user_ref: 'user-42', ttl_seconds: 3600 });

Public SMS signup (no auth — requires public_sms_signup_enabled on the workspace):

const pub = new OpenhexClient({ apiKey: 'unused' }); // token ignored on these routes
await pub.workspaces.sendSmsCode('acme', { phone: '13800000000' });
const session = await pub.workspaces.verifySmsCode('acme', {
  phone: '13800000000',
  code: '123456',
});

The admin-side routes (workspace ledger, insights, per-member ledger, API-key management, and the 4 workspace-lifecycle CRUD endpoints) are intentionally not part of this client. They power the SP admin webapp and aren't in the published partner spec — they'd 401 with a workspace API key anyway.

Local agent loop (scaffold)

import { query } from '@openhex-ai/agent-sdk';

for await (const message of query({
  prompt: 'Summarize the latest support tickets',
  options: { allowedTools: ['Read', 'WebSearch'] },
})) {
  if (message.type === 'result') console.log(message.result);
}

Custom tools

import { query } from '@openhex-ai/agent-sdk';
import { tool, createSdkMcpServer } from '@openhex-ai/agent-sdk/tools';
import { z } from 'zod';

const getWeather = tool(
  'get_weather',
  'Look up the weather for a city',
  z.object({ city: z.string() }),
  async ({ city }) => ({ content: [{ type: 'text', text: `Sunny in ${city}` }] })
);

const q = query({
  prompt: "What's the weather in Tokyo?",
  options: {
    mcpServers: { local: createSdkMcpServer({ name: 'local', tools: [getWeather] }) },
  },
});

Concepts

| Concept | Where | | --------------------------- | ----------------------------------------------------------- | | Chat with a published agent | client.sendMessage(), client.runTurn(), client.chat.* | | Embeddable chat UI | <ChatWidget>, <ChatBox>, useOpenhexChat() (/react) | | Resumable record stream | client.chat.stream({ lastEventId }) | | One-shot / streaming runs | query() (scaffold) | | Stateful client + sessions | OpenhexClient | | Built-in & custom tools | options.allowedTools, tool(), createSdkMcpServer() | | Lifecycle hooks | options.hooks, hookMatcher() | | Subagents | options.agents | | External integrations (MCP) | options.mcpServers | | Permissions | options.permissionMode, options.canUseTool |

The surface intentionally mirrors the Claude Agent SDK so the mental model transfers directly.

Development

pnpm --filter @openhex-ai/agent-sdk build      # bundle to dist/ via tsup
pnpm --filter @openhex-ai/agent-sdk typecheck  # tsc --noEmit
pnpm --filter @openhex-ai/agent-sdk test       # jest

License

See LICENSE.