@astralform/js 0.2.3 → 1.1.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 +160 -73
- package/dist/index.cjs +972 -1022
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +738 -564
- package/dist/index.d.ts +738 -564
- package/dist/index.js +965 -1019
- package/dist/index.js.map +1 -1
- package/package.json +17 -10
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
|
|
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 "
|
|
24
|
-
|
|
23
|
+
case "block_delta":
|
|
24
|
+
if (event.delta.channel === "text") {
|
|
25
|
+
process.stdout.write(event.delta.text);
|
|
26
|
+
}
|
|
25
27
|
break;
|
|
26
|
-
case "
|
|
28
|
+
case "message_stop":
|
|
27
29
|
console.log("\nDone!");
|
|
28
30
|
break;
|
|
29
31
|
case "error":
|
|
30
|
-
console.error(event.
|
|
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
|
-
- **
|
|
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
|
-
|
|
74
|
-
|
|
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 "
|
|
77
|
-
//
|
|
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 "
|
|
80
|
-
//
|
|
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 "
|
|
83
|
-
//
|
|
106
|
+
case "todo_update":
|
|
107
|
+
// event.todos (TodoItem[])
|
|
84
108
|
break;
|
|
85
|
-
case "
|
|
86
|
-
//
|
|
109
|
+
case "title_generated":
|
|
110
|
+
// event.title
|
|
87
111
|
break;
|
|
88
|
-
case "
|
|
89
|
-
//
|
|
112
|
+
case "context_warning":
|
|
113
|
+
// event.severity, event.utilizationPct, event.remainingTokens, ...
|
|
90
114
|
break;
|
|
91
|
-
case "
|
|
92
|
-
|
|
115
|
+
case "memory_recall":
|
|
116
|
+
case "memory_update":
|
|
117
|
+
// Backend memory subsystem surfaced to the UI
|
|
93
118
|
break;
|
|
94
|
-
case "
|
|
95
|
-
//
|
|
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
|
-
//
|
|
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
|
|
139
|
-
2.
|
|
140
|
-
3. SDK
|
|
141
|
-
4. SDK
|
|
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
|
-
|
|
186
|
+
Observers can show "running…" UIs by watching `block_start(kind="tool_use")` and `block_stop(status="awaiting_client_result")` directly.
|
|
144
187
|
|
|
145
|
-
|
|
188
|
+
### Approval-gated tools
|
|
146
189
|
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
//
|
|
213
|
-
session.
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
session.
|
|
220
|
-
|
|
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 === "
|
|
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
|
-
|
|
348
|
+
HTTP calls (`connect`, `submitToolApproval`, `createJob`, …) throw typed errors:
|
|
279
349
|
|
|
280
350
|
```ts
|
|
281
351
|
import {
|
|
282
|
-
AuthenticationError,
|
|
283
|
-
RateLimitError,
|
|
352
|
+
AuthenticationError, // 401 — invalid API key
|
|
353
|
+
RateLimitError, // 429 — rate limit exceeded
|
|
284
354
|
LLMNotConfiguredError, // LLM provider not set up
|
|
285
|
-
ServerError,
|
|
286
|
-
ConnectionError,
|
|
287
|
-
StreamAbortedError,
|
|
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
|
-
|
|
293
|
-
|
|
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 "
|
|
319
|
-
|
|
401
|
+
case "block_delta":
|
|
402
|
+
if (event.delta.channel === "text") {
|
|
403
|
+
setStreaming((s) => s + event.delta.text);
|
|
404
|
+
}
|
|
320
405
|
break;
|
|
321
|
-
case "
|
|
322
|
-
|
|
323
|
-
|
|
406
|
+
case "message_stop":
|
|
407
|
+
setStreaming((s) => {
|
|
408
|
+
setMessages((m) => [...m, s]);
|
|
409
|
+
return "";
|
|
410
|
+
});
|
|
324
411
|
break;
|
|
325
412
|
}
|
|
326
413
|
});
|