@astralform/js 9.1.0 → 9.3.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 +20 -2
- package/dist/index.cjs +74 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +198 -5
- package/dist/index.d.ts +198 -5
- package/dist/index.js +73 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,10 +35,24 @@ session.on((event) => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
await session.connect();
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
// A conversation's first message names its model — one the agent's team has
|
|
40
|
+
// connected. Later messages in the same conversation may omit it and reuse the
|
|
41
|
+
// conversation's model.
|
|
42
|
+
const models = await session.client.getModels(); // the list backs a model picker
|
|
43
|
+
const model = models[0];
|
|
44
|
+
if (!model) throw new Error("Connect a model provider for this agent first.");
|
|
45
|
+
await session.send("What is the capital of France?", {
|
|
46
|
+
provider: model.provider,
|
|
47
|
+
model: model.model,
|
|
48
|
+
});
|
|
39
49
|
session.disconnect();
|
|
40
50
|
```
|
|
41
51
|
|
|
52
|
+
The examples below omit `provider` and `model` for brevity. On a conversation's first
|
|
53
|
+
message, pass them as above: newer Astralform servers reject a new conversation that
|
|
54
|
+
names no model, and every server accepts one that does.
|
|
55
|
+
|
|
42
56
|
## Features
|
|
43
57
|
|
|
44
58
|
- **SSE Streaming** — Real-time token-by-token responses via Server-Sent Events
|
|
@@ -323,7 +337,11 @@ const skills = await client.getSkills();
|
|
|
323
337
|
const commands = await client.listSkillCommands();
|
|
324
338
|
|
|
325
339
|
// Job-based streaming
|
|
326
|
-
const job = await client.createJob({
|
|
340
|
+
const job = await client.createJob({
|
|
341
|
+
message: "Hello",
|
|
342
|
+
provider: "anthropic", // a new conversation names its model
|
|
343
|
+
model: "claude-sonnet-5",
|
|
344
|
+
});
|
|
327
345
|
for await (const event of client.streamJobEvents(job.job_id)) {
|
|
328
346
|
const data = JSON.parse(event.data);
|
|
329
347
|
if (data.type === "block_delta" && data.delta.channel === "text") {
|
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
VOICE_POLISH_MODES: () => VOICE_POLISH_MODES,
|
|
39
39
|
generateId: () => generateId,
|
|
40
40
|
isEmbeddedResource: () => isEmbeddedResource,
|
|
41
|
+
isToolImageStub: () => isToolImageStub,
|
|
41
42
|
isToolOutputStub: () => isToolOutputStub,
|
|
42
43
|
isVoiceLLMMode: () => isVoiceLLMMode,
|
|
43
44
|
isVoicePolishMode: () => isVoicePolishMode,
|
|
@@ -371,6 +372,11 @@ function isToolOutputStub(value) {
|
|
|
371
372
|
const v = value;
|
|
372
373
|
return v.__stub === "tool_output" && typeof v.call_id === "string" && v.call_id.length > 0;
|
|
373
374
|
}
|
|
375
|
+
function isToolImageStub(value) {
|
|
376
|
+
if (typeof value !== "object" || value === null) return false;
|
|
377
|
+
const v = value;
|
|
378
|
+
return v.__stub === "tool_image" && typeof v.call_id === "string" && v.call_id.length > 0 && typeof v.index === "number" && Number.isInteger(v.index) && v.index >= 0;
|
|
379
|
+
}
|
|
374
380
|
|
|
375
381
|
// src/client.ts
|
|
376
382
|
var DEFAULT_BASE_URL = "https://api.astralform.ai";
|
|
@@ -694,6 +700,13 @@ var AstralformClient = class {
|
|
|
694
700
|
return await response.json();
|
|
695
701
|
});
|
|
696
702
|
}
|
|
703
|
+
/** A GET whose body is binary. Read inside the race, for the reason above. */
|
|
704
|
+
async getBlob(path) {
|
|
705
|
+
return this.withDeadline(async (signal) => {
|
|
706
|
+
const response = await this.send("GET", path, void 0, signal);
|
|
707
|
+
return response.blob();
|
|
708
|
+
});
|
|
709
|
+
}
|
|
697
710
|
async post(path, body) {
|
|
698
711
|
return this.withDeadline(async (signal) => {
|
|
699
712
|
const response = await this.send("POST", path, body, signal);
|
|
@@ -913,6 +926,7 @@ var AstralformClient = class {
|
|
|
913
926
|
const params = new URLSearchParams();
|
|
914
927
|
if (jobId) params.set("job_id", jobId);
|
|
915
928
|
if (options?.toolOutputs === "stub") params.set("tool_outputs", "stub");
|
|
929
|
+
if (options?.toolImages === "stub") params.set("tool_images", "stub");
|
|
916
930
|
const query = params.toString();
|
|
917
931
|
return this.get(
|
|
918
932
|
`/v1/conversations/${encodeURIComponent(conversationId)}/events${query ? `?${query}` : ""}`
|
|
@@ -927,6 +941,25 @@ var AstralformClient = class {
|
|
|
927
941
|
);
|
|
928
942
|
return res.output;
|
|
929
943
|
}
|
|
944
|
+
/**
|
|
945
|
+
* The image behind a {@link ToolImageStub}, as a `Blob` of the preview's bytes.
|
|
946
|
+
*
|
|
947
|
+
* Takes the stub itself and nothing else — the `job_id` scoping that
|
|
948
|
+
* `getToolOutput`'s id form warns about cannot be dropped from a value that
|
|
949
|
+
* carries it. There is deliberately no by-ids form.
|
|
950
|
+
*
|
|
951
|
+
* A `Blob` rather than a URL because the route is authenticated: an `<img
|
|
952
|
+
* src>` cannot send the bearer token, so a browser consumer turns this into
|
|
953
|
+
* an object URL (`URL.createObjectURL`) and revokes it when the image
|
|
954
|
+
* unmounts. The response is `private, immutable`, so a repeat fetch of the
|
|
955
|
+
* same preview is served by the HTTP cache.
|
|
956
|
+
*/
|
|
957
|
+
async getToolImage(conversationId, stub) {
|
|
958
|
+
const query = stub.job_id ? `?job_id=${encodeURIComponent(stub.job_id)}` : "";
|
|
959
|
+
return this.getBlob(
|
|
960
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}/tool-output/${encodeURIComponent(stub.call_id)}/images/${stub.index}${query}`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
930
963
|
async submitToolResult(request) {
|
|
931
964
|
await this.post("/v1/tool-result", request);
|
|
932
965
|
}
|
|
@@ -1029,7 +1062,11 @@ var AstralformClient = class {
|
|
|
1029
1062
|
// `VoicePolishMode`; `structured` is the server's own default.
|
|
1030
1063
|
defaultMode: isVoicePolishMode(raw.default_mode) ? raw.default_mode : "structured",
|
|
1031
1064
|
silenceAutoStopSeconds: raw.silence_auto_stop_seconds ?? 2,
|
|
1032
|
-
|
|
1065
|
+
// Deliberately not read from the wire. Auto-send was removed from the
|
|
1066
|
+
// platform (2026-09-14) and the server pins `auto_send` to false; an
|
|
1067
|
+
// older server still sending `true` must not make a client auto-send, so
|
|
1068
|
+
// an absent field and an explicit `true` both read as false.
|
|
1069
|
+
autoSend: false,
|
|
1033
1070
|
maxRecordingSeconds: raw.max_recording_seconds ?? 300,
|
|
1034
1071
|
supportsStreaming: Boolean(raw.supports_streaming),
|
|
1035
1072
|
hotwords: raw.hotwords ?? []
|
|
@@ -2797,6 +2834,12 @@ var StreamManager = class _StreamManager {
|
|
|
2797
2834
|
* Defaults to inline, so this is inert until a consumer opts in.
|
|
2798
2835
|
*/
|
|
2799
2836
|
this.toolOutputs = "inline";
|
|
2837
|
+
/**
|
|
2838
|
+
* Whether restore asks for tool image previews inline or as fetch handles.
|
|
2839
|
+
* Separate from `toolOutputs` because the server keeps them separate — see
|
|
2840
|
+
* `ToolImageStub` — and on the same one-setting-both-waves rule.
|
|
2841
|
+
*/
|
|
2842
|
+
this.toolImages = "inline";
|
|
2800
2843
|
this._state = "idle";
|
|
2801
2844
|
this._activeConversationId = null;
|
|
2802
2845
|
this._backgroundJobs = /* @__PURE__ */ new Map();
|
|
@@ -2853,6 +2896,25 @@ var StreamManager = class _StreamManager {
|
|
|
2853
2896
|
setToolOutputMode(mode) {
|
|
2854
2897
|
this.toolOutputs = mode;
|
|
2855
2898
|
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Ask restore for stubbed image previews, fetched on demand.
|
|
2901
|
+
*
|
|
2902
|
+
* The same caution as `setToolOutputMode`, sharper: a consumer that cannot
|
|
2903
|
+
* resolve one (`isToolImageStub`, `client.getToolImage`) finds an `images`
|
|
2904
|
+
* entry with no `url` and draws a broken image where the preview was.
|
|
2905
|
+
*/
|
|
2906
|
+
setToolImageMode(mode) {
|
|
2907
|
+
this.toolImages = mode;
|
|
2908
|
+
}
|
|
2909
|
+
/**
|
|
2910
|
+
* The ONE options object both event waves fetch with — the newest page and
|
|
2911
|
+
* every `loadEarlierTurns` page. Built in one place so an opt-in added here
|
|
2912
|
+
* reaches both by construction: two literals at two call sites is how one
|
|
2913
|
+
* wave would silently keep asking for inline payloads.
|
|
2914
|
+
*/
|
|
2915
|
+
get eventFetchOptions() {
|
|
2916
|
+
return { toolOutputs: this.toolOutputs, toolImages: this.toolImages };
|
|
2917
|
+
}
|
|
2856
2918
|
emit(event) {
|
|
2857
2919
|
for (const handler of this.handlers) {
|
|
2858
2920
|
try {
|
|
@@ -3353,9 +3415,7 @@ var StreamManager = class _StreamManager {
|
|
|
3353
3415
|
const boundary = this.oldestDrawnMessageId;
|
|
3354
3416
|
const eventLists = await Promise.all(
|
|
3355
3417
|
page.jobs.map(
|
|
3356
|
-
(job) => session.client.getConversationEvents(conversationId, job.job_id,
|
|
3357
|
-
toolOutputs: this.toolOutputs
|
|
3358
|
-
}).catch(() => [])
|
|
3418
|
+
(job) => session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
|
|
3359
3419
|
)
|
|
3360
3420
|
);
|
|
3361
3421
|
if (stop()) return 0;
|
|
@@ -3414,7 +3474,13 @@ var StreamManager = class _StreamManager {
|
|
|
3414
3474
|
conversationId,
|
|
3415
3475
|
position: "prepend",
|
|
3416
3476
|
hasMore: completed ? page.hasMore : true,
|
|
3417
|
-
complete: completed
|
|
3477
|
+
complete: completed,
|
|
3478
|
+
// Empty on the incomplete path, matching the blocks a consumer is
|
|
3479
|
+
// required to discard there: that page's cursor did not advance, so
|
|
3480
|
+
// these same turns replay from the start on the next request and
|
|
3481
|
+
// rehydrating from them now would key state to turns about to be
|
|
3482
|
+
// re-emitted.
|
|
3483
|
+
jobs: completed ? page.jobs : []
|
|
3418
3484
|
});
|
|
3419
3485
|
return emitted;
|
|
3420
3486
|
} finally {
|
|
@@ -3470,9 +3536,7 @@ var StreamManager = class _StreamManager {
|
|
|
3470
3536
|
});
|
|
3471
3537
|
const eventLists = await Promise.all(
|
|
3472
3538
|
replayableJobs.map(
|
|
3473
|
-
(job) => this.session.client.getConversationEvents(conversationId, job.job_id,
|
|
3474
|
-
toolOutputs: this.toolOutputs
|
|
3475
|
-
}).catch(() => [])
|
|
3539
|
+
(job) => this.session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
|
|
3476
3540
|
)
|
|
3477
3541
|
);
|
|
3478
3542
|
if (stopReplay()) return false;
|
|
@@ -3503,7 +3567,7 @@ var StreamManager = class _StreamManager {
|
|
|
3503
3567
|
this.oldestDrawnMessageId = plan[0]?.messageId ?? null;
|
|
3504
3568
|
complete = true;
|
|
3505
3569
|
if (stopReplay()) return false;
|
|
3506
|
-
this.emit({ type: "restoreSettled", conversationId });
|
|
3570
|
+
this.emit({ type: "restoreSettled", conversationId, jobs });
|
|
3507
3571
|
const versionCount = replayableJobs.filter(
|
|
3508
3572
|
(j) => j.status === "completed"
|
|
3509
3573
|
).length;
|
|
@@ -3621,6 +3685,7 @@ function parseEmbeddedResource(value) {
|
|
|
3621
3685
|
VOICE_POLISH_MODES,
|
|
3622
3686
|
generateId,
|
|
3623
3687
|
isEmbeddedResource,
|
|
3688
|
+
isToolImageStub,
|
|
3624
3689
|
isToolOutputStub,
|
|
3625
3690
|
isVoiceLLMMode,
|
|
3626
3691
|
isVoicePolishMode,
|