@astralform/js

中文文档
9.13.0发布于 2026/10/25MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 2220

Astralform JS SDK - AI agent orchestration with WebMCP support

原始使用文档
文档目录17 节
  1. Install
  2. Quick Start
  3. Features
  4. Configuration
  5. Events
  6. Client-Side Tools
  7. Approval-gated tools
  8. UI Protocols
  9. Multi-Agent
  10. Conversation Management
  11. Enabling Client Tools
  12. Low-Level Client
  13. Custom Storage
  14. Error Handling
  15. Framework Integration
  16. Development
  17. License

@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();

// A conversation's first message names its model — one the agent's team has
// connected. Later messages in the same conversation may omit it and reuse the
// conversation's model.
const models = await session.client.getModels(); // the list backs a model picker
const model = models[0];
if (!model) throw new Error("Connect a model provider for this agent first.");
await session.send("What is the capital of France?", {
  provider: model.provider,
  model: model.model,
});
session.disconnect();

The examples below omit provider and model for brevity. On a conversation's first message, pass them as above: newer Astralform servers reject a new conversation that names no model, and every server accepts one that does.

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
  timeoutMs: 30_000,        // Optional — REST request deadline, defaults to 30s
});

timeoutMs bounds each REST request end to end — connect, headers, and the body read — rejecting with ConnectionError when it expires. File uploads and the SSE stream are exempt: both are long-running by design, and the stream carries its own AbortSignal.

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. Rejects if the server refused it — a 404 counts as
// already deleted and resolves, anything else leaves the conversation in place.
try {
  await session.deleteConversation("conversation-id");
} catch (err) {
  // Still there: tell the user rather than removing it from your own UI.
}

// 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();
const commands = await client.listSkillCommands();

// Job-based streaming
const job = await client.createJob({
  message: "Hello",
  provider: "anthropic", // a new conversation names its model
  model: "claude-sonnet-5",
});
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 — carries `status` when it came from a response
  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

版本与发布记录

@astralform/js 共发布 66 个版本,首次发布于 2026/03/17;过去 12 个月发布了 66 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
9.13.0latest2026/10/2513 天前
9.12.02026/10/2513 天前
9.11.02026/10/2414 天前
9.10.02026/10/2315 天前
9.9.02026/10/2018 天前
9.8.32026/10/1820 天前

关于 @astralform/js 的常见问题

@astralform/js 是什么?

Astralform JS SDK - AI agent orchestration with WebMCP support

如何安装 @astralform/js?

在项目目录运行 npm install @astralform/js,或使用 pnpm add / yarn add / bun add。

@astralform/js 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@astralform/js 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@astralform/js 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@astralform/js 可以商用吗?

@astralform/js 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@astralform/js 的下载量有多少?

根据 npm 官方下载统计,@astralform/js 在 2026/10/07 – 2026/11/05 这 30 天内被下载 2,220 次,日均约 74 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@astralform/js 还在维护吗?

最新版本 9.13.0 发布于 2026/10/25(13 天前);至今共发布 66 个版本,其中过去 12 个月发布了 66 个;首次发布于 2026/03/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。