@astralform/js 0.2.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @astralform/js
2
2
 
3
- JavaScript/TypeScript SDK for [Astralform](https://astralform.ai) — AI agent orchestration with SSE streaming, client-side tool execution, and [WebMCP](https://developer.chrome.com/docs/extensions/ai/webmcp) bridge support.
3
+ JavaScript/TypeScript SDK for [Astralform](https://astralform.ai) — AI agent orchestration with SSE streaming and client-side tool execution.
4
4
 
5
5
  ## Install
6
6
 
@@ -20,14 +20,16 @@ const session = new ChatSession({
20
20
 
21
21
  session.on((event) => {
22
22
  switch (event.type) {
23
- case "chunk":
24
- process.stdout.write(event.text);
23
+ case "block_delta":
24
+ if (event.delta.channel === "text") {
25
+ process.stdout.write(event.delta.text);
26
+ }
25
27
  break;
26
- case "complete":
28
+ case "message_stop":
27
29
  console.log("\nDone!");
28
30
  break;
29
31
  case "error":
30
- console.error(event.error.message);
32
+ console.error(`${event.code}: ${event.message}`);
31
33
  break;
32
34
  }
33
35
  });
@@ -41,9 +43,11 @@ session.disconnect();
41
43
 
42
44
  - **SSE Streaming** — Real-time token-by-token responses via Server-Sent Events
43
45
  - **Client-Side Tools** — Register tools that the LLM can call, executed locally in your app
44
- - **WebMCP Bridge** — Auto-discovers browser tools from `navigator.modelContext` (Chrome 146+)
46
+ - **Approval-Gated Tools** — Respond to `tool_approval_requested` events before execution
47
+ - **UI Protocols** — Pluggable renderers for MCP-style embedded resources (A2UI, etc.)
45
48
  - **Multi-Agent** — Route messages to specific agents or let the supervisor choose
46
49
  - **Conversation Management** — Create, switch, delete, and resume conversations
50
+ - **Event Replay** — Translate persisted wire events back into `ChatEvent`s
47
51
  - **Zero Dependencies** — Uses only native APIs (`fetch`, `ReadableStream`, `crypto`)
48
52
  - **Universal** — ESM + CJS, works in browsers and Node.js 18+
49
53
 
@@ -62,7 +66,7 @@ const session = new ChatSession({
62
66
 
63
67
  ## Events
64
68
 
65
- Subscribe to events with `.on()`, which returns an unsubscribe function:
69
+ 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.
66
70
 
67
71
  ```ts
68
72
  const unsubscribe = session.on((event) => {
@@ -70,35 +74,73 @@ const unsubscribe = session.on((event) => {
70
74
  case "connected":
71
75
  // Session connected, project status and tools loaded
72
76
  break;
73
- case "chunk":
74
- // Streaming text chunk: event.text
77
+
78
+ // --- Turn lifecycle ---
79
+ case "message_start":
80
+ // New turn: event.turnId, event.model, event.agentDisplayName
81
+ break;
82
+ case "block_start":
83
+ // A content block opened: event.kind ("text" | "thinking" | "tool_use" | ...)
75
84
  break;
76
- case "complete":
77
- // Response finished: event.content, event.conversationId, event.title
85
+ case "block_delta":
86
+ // Streaming chunk. Narrow by event.delta.channel:
87
+ // - "text": event.delta.text
88
+ // - "thinking": event.delta.text
89
+ // - "input" / "input_arg": partial tool input
90
+ // - "output": interpreter stdout/stderr/progress
91
+ // - "status": "executing" | "awaiting_client_result" | "awaiting_approval" | "denied"
78
92
  break;
79
- case "tool_call":
80
- // Tool invoked: event.request.toolName, event.request.arguments
93
+ case "block_stop":
94
+ // Block finished. event.status === "awaiting_client_result" means
95
+ // a client-side tool is ready to run (see "Client-Side Tools" below).
96
+ break;
97
+ case "message_stop":
98
+ // Turn complete: event.stopReason, event.usage, event.totalMs, event.jobId
99
+ break;
100
+
101
+ // --- Custom events (typed variants) ---
102
+ case "subagent_start":
103
+ case "subagent_stop":
104
+ // event.agent (AgentIdentity), event.taskCallId
81
105
  break;
82
- case "tool_executing":
83
- // Tool running: event.name
106
+ case "todo_update":
107
+ // event.todos (TodoItem[])
84
108
  break;
85
- case "tool_completed":
86
- // Tool finished: event.name, event.result
109
+ case "title_generated":
110
+ // event.title
87
111
  break;
88
- case "agent_start":
89
- // Agent began processing: event.agentName, event.agentDisplayName
112
+ case "context_warning":
113
+ // event.severity, event.utilizationPct, event.remainingTokens, ...
90
114
  break;
91
- case "agent_end":
92
- // Agent finished: event.agentName
115
+ case "memory_recall":
116
+ case "memory_update":
117
+ // Backend memory subsystem surfaced to the UI
93
118
  break;
94
- case "model_info":
95
- // LLM model identified: event.name
119
+ case "tool_approval_requested":
120
+ // Respond via client.submitToolApproval(...)
121
+ break;
122
+ case "asset_created":
123
+ case "attachment_staged":
124
+ case "workspace_ready":
125
+ case "desktop_stream":
126
+ // Workspace / asset pipeline events
127
+ break;
128
+ case "state_changed":
129
+ // event.state ("queued" | "running" | "waiting_for_tool" | ...)
130
+ break;
131
+ case "custom":
132
+ // Unknown custom event — forward-compat passthrough
133
+ break;
134
+
135
+ // --- Transport / errors ---
136
+ case "retry":
137
+ case "stall":
138
+ case "keepalive":
96
139
  break;
97
140
  case "error":
98
- // Error occurred: event.error
141
+ // event.code, event.message, event.blockPath
99
142
  break;
100
143
  case "disconnected":
101
- // Session disconnected
102
144
  break;
103
145
  }
104
146
  });
@@ -135,41 +177,64 @@ await session.send("What time is it in Tokyo?");
135
177
 
136
178
  The tool execution flow is handled automatically:
137
179
 
138
- 1. LLM requests a client tool call via SSE
139
- 2. SDK executes the tool handler locally
140
- 3. SDK posts the result to `/v1/tool-result`
141
- 4. SDK continues the SSE stream for the LLM's final response
180
+ 1. LLM requests a client tool (wire: `block_start(kind="tool_use")`, then streaming `block_delta(channel="input")`)
181
+ 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
182
+ 3. SDK invokes the registered handler locally
183
+ 4. SDK posts the result to `/v1/tool-result`
184
+ 5. SDK continues the SSE stream for the LLM's final response
142
185
 
143
- ## WebMCP Bridge
186
+ Observers can show "running…" UIs by watching `block_start(kind="tool_use")` and `block_stop(status="awaiting_client_result")` directly.
144
187
 
145
- On Chrome 146+ with WebMCP support, the SDK auto-discovers browser-registered tools:
188
+ ### Approval-gated tools
146
189
 
147
- ```ts
148
- await session.connect(); // Automatically calls navigator.modelContext.tools.list()
190
+ 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(...)`:
149
191
 
150
- console.log("WebMCP available:", session.webMCP.isAvailable());
192
+ ```ts
193
+ session.on(async (event) => {
194
+ if (event.type === "tool_approval_requested") {
195
+ const ok = confirm(`Allow ${event.toolName}? (${event.reason ?? ""})`);
196
+ await session.client.submitToolApproval({
197
+ job_id: session.currentJobId ?? "",
198
+ call_id: event.callId,
199
+ decision: ok ? "allow" : "deny",
200
+ scope: "once", // "once" | "conversation" | "always"
201
+ });
202
+ }
203
+ });
151
204
  ```
152
205
 
153
- You can also register tools that appear in both WebMCP and Astralform:
206
+ ## UI Protocols
207
+
208
+ 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:
154
209
 
155
210
  ```ts
156
- session.webMCP.registerTool(
157
- "page_content",
158
- "Get the current page content",
159
- {
160
- type: "object",
161
- properties: {
162
- selector: { type: "string", description: "CSS selector" },
211
+ import { ChatSession, parseEmbeddedResource } from "@astralform/js";
212
+
213
+ await session.connect();
214
+
215
+ // Gate registration on the project's configured protocol.
216
+ if (session.projectStatus?.uiComponents.enabled) {
217
+ session.protocols.register({
218
+ mimeType: session.projectStatus.uiComponents.mimeType!,
219
+ render: (payload) => {
220
+ /* framework-specific render */
163
221
  },
164
- },
165
- async (args) => {
166
- const el = document.querySelector((args.selector as string) || "body");
167
- return el?.textContent ?? "Not found";
168
- },
169
- );
222
+ });
223
+ }
224
+
225
+ // Inside your tool-result block handler:
226
+ session.on((event) => {
227
+ if (event.type === "block_stop" && event.final) {
228
+ const resource = parseEmbeddedResource(event.final.output);
229
+ if (resource) {
230
+ const adapter = session.protocols.get(resource.mimeType);
231
+ adapter?.render(resource.payload);
232
+ }
233
+ }
234
+ });
170
235
  ```
171
236
 
172
- WebMCP tools are registered with the `mcp_webmcp_` prefix in the tool manifest sent to the backend.
237
+ The SDK never imports a renderer — adapters are opaque handles that the consumer narrows on read. Adapters are dropped on `disconnect()`.
173
238
 
174
239
  ## Multi-Agent
175
240
 
@@ -206,18 +271,25 @@ session.conversations; // All conversations
206
271
  session.messages; // Messages in current conversation
207
272
  ```
208
273
 
209
- ## Toggle Tools
274
+ ## Enabling Client Tools
275
+
276
+ Client tools registered via `session.toolRegistry.registerTool(...)` only run when their name is in the session's enabled set:
210
277
 
211
278
  ```ts
212
- // Toggle platform tools (e.g. web search)
213
- session.toggleTool("search"); // Returns true if now enabled, false if disabled
279
+ // Enable / disable a registered client tool
280
+ session.toggleClientTool("mcp_get_current_time"); // returns true if now enabled
281
+
282
+ // Inspect the enabled set
283
+ session.enabledClientTools; // Set<string>
284
+ ```
214
285
 
215
- // Toggle MCP tools
216
- session.toggleMcp("github__list_repos");
286
+ Platform-level features (web search, plan mode) are enabled per-request via the `send` options:
217
287
 
218
- // Check enabled state
219
- session.enabledTools; // Set<string>
220
- session.enabledMcp; // Set<string>
288
+ ```ts
289
+ await session.send("Research the latest on WebGPU", {
290
+ enableSearch: true,
291
+ planMode: true,
292
+ });
221
293
  ```
222
294
 
223
295
  ## Low-Level Client
@@ -236,8 +308,6 @@ const client = new AstralformClient({
236
308
  const status = await client.getProjectStatus();
237
309
  const conversations = await client.getConversations();
238
310
  const messages = await client.getMessages("conversation-id");
239
- const tools = await client.getTools();
240
- const mcpTools = await client.getMcpTools();
241
311
  const agents = await client.getAgents();
242
312
  const skills = await client.getSkills();
243
313
 
@@ -245,7 +315,7 @@ const skills = await client.getSkills();
245
315
  const job = await client.createJob({ message: "Hello" });
246
316
  for await (const event of client.streamJobEvents(job.job_id)) {
247
317
  const data = JSON.parse(event.data);
248
- if (data.type === "content_block_delta") {
318
+ if (data.type === "block_delta" && data.delta.channel === "text") {
249
319
  process.stdout.write(data.delta.text);
250
320
  }
251
321
  }
@@ -275,22 +345,35 @@ const session = new ChatSession(config, myStorage);
275
345
 
276
346
  ## Error Handling
277
347
 
278
- The SDK throws typed errors:
348
+ HTTP calls (`connect`, `submitToolApproval`, `createJob`, …) throw typed errors:
279
349
 
280
350
  ```ts
281
351
  import {
282
- AuthenticationError, // 401 — invalid API key
283
- RateLimitError, // 429 — rate limit exceeded
352
+ AuthenticationError, // 401 — invalid API key
353
+ RateLimitError, // 429 — rate limit exceeded
284
354
  LLMNotConfiguredError, // LLM provider not set up
285
- ServerError, // 5xx or unexpected errors
286
- ConnectionError, // Network failures
287
- StreamAbortedError, // Stream cancelled via disconnect()
355
+ ServerError, // 5xx or unexpected errors
356
+ ConnectionError, // Network failures
357
+ StreamAbortedError, // Stream cancelled via disconnect()
288
358
  } from "@astralform/js";
289
359
 
360
+ try {
361
+ await session.connect();
362
+ } catch (err) {
363
+ if (err instanceof AuthenticationError) {
364
+ // Redirect to login
365
+ }
366
+ }
367
+ ```
368
+
369
+ Errors that arrive over the SSE stream fire as a typed `error` event with a structured shape — no `Error` instance is wrapped:
370
+
371
+ ```ts
290
372
  session.on((event) => {
291
373
  if (event.type === "error") {
292
- if (event.error instanceof AuthenticationError) {
293
- // Redirect to login
374
+ // event.code, event.message, event.blockPath
375
+ if (event.code === "rate_limit_exceeded") {
376
+ // Show backoff UI
294
377
  }
295
378
  }
296
379
  });
@@ -315,12 +398,16 @@ function useChat(apiKey: string, userId: string) {
315
398
 
316
399
  session.on((event: ChatEvent) => {
317
400
  switch (event.type) {
318
- case "chunk":
319
- setStreaming((s) => s + event.text);
401
+ case "block_delta":
402
+ if (event.delta.channel === "text") {
403
+ setStreaming((s) => s + event.delta.text);
404
+ }
320
405
  break;
321
- case "complete":
322
- setMessages((m) => [...m, event.content]);
323
- setStreaming("");
406
+ case "message_stop":
407
+ setStreaming((s) => {
408
+ setMessages((m) => [...m, s]);
409
+ return "";
410
+ });
324
411
  break;
325
412
  }
326
413
  });