@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 CHANGED
@@ -35,10 +35,24 @@ session.on((event) => {
35
35
  });
36
36
 
37
37
  await session.connect();
38
- await session.send("What is the capital of France?");
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({ message: "Hello" });
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
- autoSend: raw.auto_send ?? true,
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,