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

@astralform/js

v4.1.0

Published

Astralform JS SDK - AI agent orchestration with WebMCP support

Readme

@astralform/js

JavaScript/TypeScript SDK for Astralform — AI agent orchestration with SSE streaming and client-side tool execution.

Install

npm install @astralform/js

Quick Start

import { ChatSession } from "@astralform/js";

const session = new ChatSession({
  apiKey: "your-api-key",
  userId: "user-123",
});

session.on((event) => {
  switch (event.type) {
    case "block_delta":
      if (event.delta.channel === "text") {
        process.stdout.write(event.delta.text);
      }
      break;
    case "message_stop":
      console.log("\nDone!");
      break;
    case "error":
      console.error(`${event.code}: ${event.message}`);
      break;
  }
});

await session.connect();
await session.send("What is the capital of France?");
session.disconnect();

Features

  • SSE Streaming — Real-time token-by-token responses via Server-Sent Events
  • Client-Side Tools — Register tools that the LLM can call, executed locally in your app
  • Approval-Gated Tools — Respond to tool_approval_requested events before execution
  • UI Protocols — Pluggable renderers for MCP-style embedded resources (A2UI, etc.)
  • Multi-Agent — Route messages to specific agents or let the supervisor choose
  • Conversation Management — Create, switch, delete, and resume conversations
  • Event Replay — Translate persisted wire events back into ChatEvents
  • Zero Dependencies — Uses only native APIs (fetch, ReadableStream, crypto)
  • Universal — ESM + CJS, works in browsers and Node.js 18+

Configuration

import { ChatSession } from "@astralform/js";

const session = new ChatSession({
  apiKey: "your-api-key",   // Required — Astralform agent API key
  userId: "user-123",       // Required — identifies the end user
  baseURL: "http://localhost:8000", // Optional — defaults to https://api.astralform.ai
  fetch: customFetch,       // Optional — custom fetch implementation
});

Events

Subscribe to events with .on(), which returns an unsubscribe function. The SDK forwards a typed ChatEvent for every wire event — consumers build their own block / message state from the stream.

const unsubscribe = session.on((event) => {
  switch (event.type) {
    case "connected":
      // Session connected, agent status and tools loaded
      break;

    // --- Turn lifecycle ---
    case "message_start":
      // New turn: event.turnId, event.model, event.agentDisplayName
      break;
    case "block_start":
      // A content block opened: event.kind ("text" | "thinking" | "tool_use" | ...)
      break;
    case "block_delta":
      // Streaming chunk. Narrow by event.delta.channel:
      //   - "text": event.delta.text
      //   - "thinking": event.delta.text
      //   - "input" / "input_arg": partial tool input
      //   - "output": interpreter stdout/stderr/progress
      //   - "status": "executing" | "awaiting_client_result" | "awaiting_approval" | "denied"
      break;
    case "block_stop":
      // Block finished. event.status === "awaiting_client_result" means
      // a client-side tool is ready to run (see "Client-Side Tools" below).
      break;
    case "message_stop":
      // Turn complete: event.stopReason, event.usage, event.totalMs, event.jobId
      break;

    // --- Custom events (typed variants) ---
    case "subagent_start":
    case "subagent_stop":
      // event.agent (AgentIdentity), event.taskCallId
      break;
    case "todo_update":
      // event.todos (TodoItem[])
      break;
    case "title_generated":
      // event.title
      break;
    case "context_warning":
      // event.severity, event.utilizationPct, event.remainingTokens, ...
      break;
    case "memory_recall":
    case "memory_update":
      // Backend memory subsystem surfaced to the UI
      break;
    case "tool_approval_requested":
      // Respond via client.submitToolApproval(...)
      break;
    case "asset_created":
    case "attachment_staged":
    case "workspace_ready":
    case "desktop_stream":
      // Workspace / asset pipeline events
      break;
    case "state_changed":
      // event.state ("queued" | "running" | "waiting_for_tool" | ...)
      break;
    case "custom":
      // Unknown custom event — forward-compat passthrough
      break;

    // --- Transport / errors ---
    case "retry":
    case "stall":
    case "keepalive":
      break;
    case "error":
      // event.code, event.message, event.blockPath
      break;
    case "disconnected":
      break;
  }
});

// Later: unsubscribe()

Client-Side Tools

Register tools that the LLM can invoke. Tool names must start with mcp_ so the backend routes them to the client for execution.

session.toolRegistry.registerTool(
  "mcp_get_current_time",
  "Get the current date and time",
  {
    type: "object",
    properties: {
      timezone: {
        type: "string",
        description: "IANA timezone (e.g. America/New_York)",
      },
    },
  },
  async (args) => {
    const tz = (args.timezone as string) || "UTC";
    return new Date().toLocaleString("en-US", { timeZone: tz });
  },
);

await session.send("What time is it in Tokyo?");
// The LLM calls mcp_get_current_time → SDK executes it → result sent back → LLM responds

The tool execution flow is handled automatically:

  1. LLM requests a client tool (wire: block_start(kind="tool_use"), then streaming block_delta(channel="input"))
  2. Backend signals ready-to-run with block_stop(status="awaiting_client_result")final.call_id, final.tool_name, and final.input carry the parsed arguments
  3. SDK invokes the registered handler locally
  4. SDK posts the result to /v1/tool-result
  5. SDK continues the SSE stream for the LLM's final response

Observers can show "running…" UIs by watching block_start(kind="tool_use") and block_stop(status="awaiting_client_result") directly.

Approval-gated tools

When a tool requires user approval, the backend emits a tool_approval_requested event instead of proceeding to awaiting_client_result. Respond with client.submitToolApproval(...):

session.on(async (event) => {
  if (event.type === "tool_approval_requested") {
    const ok = confirm(`Allow ${event.toolName}? (${event.reason ?? ""})`);
    await session.client.submitToolApproval({
      job_id: session.currentJobId ?? "",
      call_id: event.callId,
      decision: ok ? "allow" : "deny",
      scope: "once", // "once" | "conversation" | "always"
    });
  }
});

UI Protocols

When the backend renders rich UI surfaces (A2UI today, other protocols in the future), tool output arrives wrapped as an MCP-style embedded resource. Register a framework-specific renderer keyed by MIME type:

import { ChatSession, parseEmbeddedResource } from "@astralform/js";

await session.connect();

// Gate registration on the agent's configured protocol.
if (session.agentStatus?.uiComponents.enabled) {
  session.protocols.register({
    mimeType: session.agentStatus.uiComponents.mimeType!,
    render: (payload) => {
      /* framework-specific render */
    },
  });
}

// Inside your tool-result block handler:
session.on((event) => {
  if (event.type === "block_stop" && event.final) {
    const resource = parseEmbeddedResource(event.final.output);
    if (resource) {
      const adapter = session.protocols.get(resource.mimeType);
      adapter?.render(resource.payload);
    }
  }
});

The SDK never imports a renderer — adapters are opaque handles that the consumer narrows on read. Adapters are dropped on disconnect().

Multi-Agent

Send messages to specific agents:

await session.connect();

// List available agents
console.log(session.agents);

// Send to a specific agent
await session.send("Help me debug this", { agentName: "debugger" });

Conversation Management

// Create a new conversation
const id = await session.createNewConversation();

// Switch to an existing conversation (loads messages from backend)
await session.switchConversation("conversation-id");

// Delete a conversation
await session.deleteConversation("conversation-id");

// Edit and resend from a checkpoint
await session.resendFromCheckpoint("message-id", "Updated message");

// Access state
session.conversationId;   // Current conversation ID
session.conversations;    // All conversations
session.messages;         // Messages in current conversation

Enabling Client Tools

Client tools registered via session.toolRegistry.registerTool(...) only run when their name is in the session's enabled set:

// Enable / disable a registered client tool
session.toggleClientTool("mcp_get_current_time"); // returns true if now enabled

// Inspect the enabled set
session.enabledClientTools; // Set<string>

Platform-level features (e.g. plan mode) are enabled per-request via the send options. Web search needs no option: when the agent's search feature is enabled server-side, the agent decides per-task whether to search.

await session.send("Research the latest on WebGPU", {
  planMode: true,
});

Low-Level Client

For direct API access without session state management:

import { AstralformClient } from "@astralform/js";

const client = new AstralformClient({
  apiKey: "your-api-key",
  userId: "user-123",
});

// REST endpoints
const status = await client.getAgentStatus();
const conversations = await client.getConversations();
const messages = await client.getMessages("conversation-id");
const agents = await client.getAgents();
const skills = await client.getSkills();

// Job-based streaming
const job = await client.createJob({ message: "Hello" });
for await (const event of client.streamJobEvents(job.job_id)) {
  const data = JSON.parse(event.data);
  if (data.type === "block_delta" && data.delta.channel === "text") {
    process.stdout.write(data.delta.text);
  }
}

Custom Storage

The SDK uses in-memory storage by default. Implement ChatStorage for persistence:

import { ChatSession, type ChatStorage } from "@astralform/js";

const myStorage: ChatStorage = {
  fetchConversations: async () => { /* ... */ },
  fetchConversation: async (id) => { /* ... */ },
  createConversation: async (id, title) => { /* ... */ },
  updateConversationTitle: async (id, title) => { /* ... */ },
  deleteConversation: async (id) => { /* ... */ },
  fetchMessages: async (conversationId) => { /* ... */ },
  addMessage: async (message, conversationId) => { /* ... */ },
  updateMessageStatus: async (id, status) => { /* ... */ },
  deleteMessage: async (id) => { /* ... */ },
};

const session = new ChatSession(config, myStorage);

Error Handling

HTTP calls (connect, submitToolApproval, createJob, …) throw typed errors:

import {
  AuthenticationError, // 401 — invalid API key
  RateLimitError, // 429 — rate limit exceeded
  LLMNotConfiguredError, // LLM provider not set up
  ServerError, // 5xx or unexpected errors
  ConnectionError, // Network failures
  StreamAbortedError, // Stream cancelled via disconnect()
} from "@astralform/js";

try {
  await session.connect();
} catch (err) {
  if (err instanceof AuthenticationError) {
    // Redirect to login
  }
}

Errors that arrive over the SSE stream fire as a typed error event with a structured shape — no Error instance is wrapped:

session.on((event) => {
  if (event.type === "error") {
    // event.code, event.message, event.blockPath
    if (event.code === "rate_limit_exceeded") {
      // Show backoff UI
    }
  }
});

Framework Integration

The SDK is headless — it works with any UI framework. Here's a React example:

import { ChatSession, type ChatEvent } from "@astralform/js";
import { useEffect, useRef, useState } from "react";

function useChat(apiKey: string, userId: string) {
  const sessionRef = useRef<ChatSession>();
  const [messages, setMessages] = useState<string[]>([]);
  const [streaming, setStreaming] = useState("");

  useEffect(() => {
    const session = new ChatSession({ apiKey, userId });
    sessionRef.current = session;

    session.on((event: ChatEvent) => {
      switch (event.type) {
        case "block_delta":
          if (event.delta.channel === "text") {
            setStreaming((s) => s + event.delta.text);
          }
          break;
        case "message_stop":
          setStreaming((s) => {
            setMessages((m) => [...m, s]);
            return "";
          });
          break;
      }
    });

    session.connect();
    return () => session.disconnect();
  }, [apiKey, userId]);

  const send = (text: string) => sessionRef.current?.send(text);

  return { messages, streaming, send };
}

Development

npm install        # Install dependencies
npm run build      # Build ESM + CJS + types
npm test           # Run tests
npm run typecheck  # Type check

License

MIT