@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/dist/index.js CHANGED
@@ -319,6 +319,11 @@ function isToolOutputStub(value) {
319
319
  const v = value;
320
320
  return v.__stub === "tool_output" && typeof v.call_id === "string" && v.call_id.length > 0;
321
321
  }
322
+ function isToolImageStub(value) {
323
+ if (typeof value !== "object" || value === null) return false;
324
+ const v = value;
325
+ 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;
326
+ }
322
327
 
323
328
  // src/client.ts
324
329
  var DEFAULT_BASE_URL = "https://api.astralform.ai";
@@ -642,6 +647,13 @@ var AstralformClient = class {
642
647
  return await response.json();
643
648
  });
644
649
  }
650
+ /** A GET whose body is binary. Read inside the race, for the reason above. */
651
+ async getBlob(path) {
652
+ return this.withDeadline(async (signal) => {
653
+ const response = await this.send("GET", path, void 0, signal);
654
+ return response.blob();
655
+ });
656
+ }
645
657
  async post(path, body) {
646
658
  return this.withDeadline(async (signal) => {
647
659
  const response = await this.send("POST", path, body, signal);
@@ -861,6 +873,7 @@ var AstralformClient = class {
861
873
  const params = new URLSearchParams();
862
874
  if (jobId) params.set("job_id", jobId);
863
875
  if (options?.toolOutputs === "stub") params.set("tool_outputs", "stub");
876
+ if (options?.toolImages === "stub") params.set("tool_images", "stub");
864
877
  const query = params.toString();
865
878
  return this.get(
866
879
  `/v1/conversations/${encodeURIComponent(conversationId)}/events${query ? `?${query}` : ""}`
@@ -875,6 +888,25 @@ var AstralformClient = class {
875
888
  );
876
889
  return res.output;
877
890
  }
891
+ /**
892
+ * The image behind a {@link ToolImageStub}, as a `Blob` of the preview's bytes.
893
+ *
894
+ * Takes the stub itself and nothing else — the `job_id` scoping that
895
+ * `getToolOutput`'s id form warns about cannot be dropped from a value that
896
+ * carries it. There is deliberately no by-ids form.
897
+ *
898
+ * A `Blob` rather than a URL because the route is authenticated: an `<img
899
+ * src>` cannot send the bearer token, so a browser consumer turns this into
900
+ * an object URL (`URL.createObjectURL`) and revokes it when the image
901
+ * unmounts. The response is `private, immutable`, so a repeat fetch of the
902
+ * same preview is served by the HTTP cache.
903
+ */
904
+ async getToolImage(conversationId, stub) {
905
+ const query = stub.job_id ? `?job_id=${encodeURIComponent(stub.job_id)}` : "";
906
+ return this.getBlob(
907
+ `/v1/conversations/${encodeURIComponent(conversationId)}/tool-output/${encodeURIComponent(stub.call_id)}/images/${stub.index}${query}`
908
+ );
909
+ }
878
910
  async submitToolResult(request) {
879
911
  await this.post("/v1/tool-result", request);
880
912
  }
@@ -977,7 +1009,11 @@ var AstralformClient = class {
977
1009
  // `VoicePolishMode`; `structured` is the server's own default.
978
1010
  defaultMode: isVoicePolishMode(raw.default_mode) ? raw.default_mode : "structured",
979
1011
  silenceAutoStopSeconds: raw.silence_auto_stop_seconds ?? 2,
980
- autoSend: raw.auto_send ?? true,
1012
+ // Deliberately not read from the wire. Auto-send was removed from the
1013
+ // platform (2026-09-14) and the server pins `auto_send` to false; an
1014
+ // older server still sending `true` must not make a client auto-send, so
1015
+ // an absent field and an explicit `true` both read as false.
1016
+ autoSend: false,
981
1017
  maxRecordingSeconds: raw.max_recording_seconds ?? 300,
982
1018
  supportsStreaming: Boolean(raw.supports_streaming),
983
1019
  hotwords: raw.hotwords ?? []
@@ -2745,6 +2781,12 @@ var StreamManager = class _StreamManager {
2745
2781
  * Defaults to inline, so this is inert until a consumer opts in.
2746
2782
  */
2747
2783
  this.toolOutputs = "inline";
2784
+ /**
2785
+ * Whether restore asks for tool image previews inline or as fetch handles.
2786
+ * Separate from `toolOutputs` because the server keeps them separate — see
2787
+ * `ToolImageStub` — and on the same one-setting-both-waves rule.
2788
+ */
2789
+ this.toolImages = "inline";
2748
2790
  this._state = "idle";
2749
2791
  this._activeConversationId = null;
2750
2792
  this._backgroundJobs = /* @__PURE__ */ new Map();
@@ -2801,6 +2843,25 @@ var StreamManager = class _StreamManager {
2801
2843
  setToolOutputMode(mode) {
2802
2844
  this.toolOutputs = mode;
2803
2845
  }
2846
+ /**
2847
+ * Ask restore for stubbed image previews, fetched on demand.
2848
+ *
2849
+ * The same caution as `setToolOutputMode`, sharper: a consumer that cannot
2850
+ * resolve one (`isToolImageStub`, `client.getToolImage`) finds an `images`
2851
+ * entry with no `url` and draws a broken image where the preview was.
2852
+ */
2853
+ setToolImageMode(mode) {
2854
+ this.toolImages = mode;
2855
+ }
2856
+ /**
2857
+ * The ONE options object both event waves fetch with — the newest page and
2858
+ * every `loadEarlierTurns` page. Built in one place so an opt-in added here
2859
+ * reaches both by construction: two literals at two call sites is how one
2860
+ * wave would silently keep asking for inline payloads.
2861
+ */
2862
+ get eventFetchOptions() {
2863
+ return { toolOutputs: this.toolOutputs, toolImages: this.toolImages };
2864
+ }
2804
2865
  emit(event) {
2805
2866
  for (const handler of this.handlers) {
2806
2867
  try {
@@ -3301,9 +3362,7 @@ var StreamManager = class _StreamManager {
3301
3362
  const boundary = this.oldestDrawnMessageId;
3302
3363
  const eventLists = await Promise.all(
3303
3364
  page.jobs.map(
3304
- (job) => session.client.getConversationEvents(conversationId, job.job_id, {
3305
- toolOutputs: this.toolOutputs
3306
- }).catch(() => [])
3365
+ (job) => session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
3307
3366
  )
3308
3367
  );
3309
3368
  if (stop()) return 0;
@@ -3362,7 +3421,13 @@ var StreamManager = class _StreamManager {
3362
3421
  conversationId,
3363
3422
  position: "prepend",
3364
3423
  hasMore: completed ? page.hasMore : true,
3365
- complete: completed
3424
+ complete: completed,
3425
+ // Empty on the incomplete path, matching the blocks a consumer is
3426
+ // required to discard there: that page's cursor did not advance, so
3427
+ // these same turns replay from the start on the next request and
3428
+ // rehydrating from them now would key state to turns about to be
3429
+ // re-emitted.
3430
+ jobs: completed ? page.jobs : []
3366
3431
  });
3367
3432
  return emitted;
3368
3433
  } finally {
@@ -3418,9 +3483,7 @@ var StreamManager = class _StreamManager {
3418
3483
  });
3419
3484
  const eventLists = await Promise.all(
3420
3485
  replayableJobs.map(
3421
- (job) => this.session.client.getConversationEvents(conversationId, job.job_id, {
3422
- toolOutputs: this.toolOutputs
3423
- }).catch(() => [])
3486
+ (job) => this.session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
3424
3487
  )
3425
3488
  );
3426
3489
  if (stopReplay()) return false;
@@ -3451,7 +3514,7 @@ var StreamManager = class _StreamManager {
3451
3514
  this.oldestDrawnMessageId = plan[0]?.messageId ?? null;
3452
3515
  complete = true;
3453
3516
  if (stopReplay()) return false;
3454
- this.emit({ type: "restoreSettled", conversationId });
3517
+ this.emit({ type: "restoreSettled", conversationId, jobs });
3455
3518
  const versionCount = replayableJobs.filter(
3456
3519
  (j) => j.status === "completed"
3457
3520
  ).length;
@@ -3568,6 +3631,7 @@ export {
3568
3631
  VOICE_POLISH_MODES,
3569
3632
  generateId,
3570
3633
  isEmbeddedResource,
3634
+ isToolImageStub,
3571
3635
  isToolOutputStub,
3572
3636
  isVoiceLLMMode,
3573
3637
  isVoicePolishMode,