@astralform/js 9.2.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.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
  }
@@ -2801,6 +2834,12 @@ var StreamManager = class _StreamManager {
2801
2834
  * Defaults to inline, so this is inert until a consumer opts in.
2802
2835
  */
2803
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";
2804
2843
  this._state = "idle";
2805
2844
  this._activeConversationId = null;
2806
2845
  this._backgroundJobs = /* @__PURE__ */ new Map();
@@ -2857,6 +2896,25 @@ var StreamManager = class _StreamManager {
2857
2896
  setToolOutputMode(mode) {
2858
2897
  this.toolOutputs = mode;
2859
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
+ }
2860
2918
  emit(event) {
2861
2919
  for (const handler of this.handlers) {
2862
2920
  try {
@@ -3357,9 +3415,7 @@ var StreamManager = class _StreamManager {
3357
3415
  const boundary = this.oldestDrawnMessageId;
3358
3416
  const eventLists = await Promise.all(
3359
3417
  page.jobs.map(
3360
- (job) => session.client.getConversationEvents(conversationId, job.job_id, {
3361
- toolOutputs: this.toolOutputs
3362
- }).catch(() => [])
3418
+ (job) => session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
3363
3419
  )
3364
3420
  );
3365
3421
  if (stop()) return 0;
@@ -3480,9 +3536,7 @@ var StreamManager = class _StreamManager {
3480
3536
  });
3481
3537
  const eventLists = await Promise.all(
3482
3538
  replayableJobs.map(
3483
- (job) => this.session.client.getConversationEvents(conversationId, job.job_id, {
3484
- toolOutputs: this.toolOutputs
3485
- }).catch(() => [])
3539
+ (job) => this.session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
3486
3540
  )
3487
3541
  );
3488
3542
  if (stopReplay()) return false;
@@ -3631,6 +3685,7 @@ function parseEmbeddedResource(value) {
3631
3685
  VOICE_POLISH_MODES,
3632
3686
  generateId,
3633
3687
  isEmbeddedResource,
3688
+ isToolImageStub,
3634
3689
  isToolOutputStub,
3635
3690
  isVoiceLLMMode,
3636
3691
  isVoicePolishMode,