@astralform/js 7.1.1 → 7.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
@@ -194,12 +194,13 @@ function createRateLimitErrorFromHttp(response, rawText) {
194
194
 
195
195
  // src/streaming.ts
196
196
  async function* streamJobSSE(options) {
197
- const { url, headers, signal, fetchFn } = options;
197
+ const { url, headers, signal, fetchFn, method = "GET", body } = options;
198
198
  let response;
199
199
  try {
200
200
  response = await fetchFn(url, {
201
- method: "GET",
201
+ method,
202
202
  headers,
203
+ body,
203
204
  signal
204
205
  });
205
206
  } catch (err) {
@@ -261,6 +262,57 @@ async function* streamJobSSE(options) {
261
262
  }
262
263
  }
263
264
 
265
+ // src/types.ts
266
+ var ChatEventType = {
267
+ // Connection lifecycle (SDK-local, not wire)
268
+ Connected: "connected",
269
+ Disconnected: "disconnected",
270
+ // Turn lifecycle
271
+ MessageStart: "message_start",
272
+ MessageStop: "message_stop",
273
+ // Block lifecycle
274
+ BlockStart: "block_start",
275
+ BlockDelta: "block_delta",
276
+ BlockStop: "block_stop",
277
+ // Reliability
278
+ Stall: "stall",
279
+ Retry: "retry",
280
+ Error: "error",
281
+ Keepalive: "keepalive",
282
+ // Conversation-level (typed custom events)
283
+ UserMessage: "user_message",
284
+ TitleGenerated: "title_generated",
285
+ TodoUpdate: "todo_update",
286
+ PlanUpdate: "plan_update",
287
+ NoteUpdate: "note_update",
288
+ ContextUpdate: "context_update",
289
+ SubagentStart: "subagent_start",
290
+ SubagentStop: "subagent_stop",
291
+ ContextWarning: "context_warning",
292
+ MemoryRecall: "memory_recall",
293
+ MemoryUpdate: "memory_update",
294
+ DesktopStream: "desktop_stream",
295
+ AttachmentStaged: "attachment_staged",
296
+ WorkspaceReady: "workspace_ready",
297
+ AssetCreated: "asset_created",
298
+ ToolApprovalRequested: "tool_approval_requested",
299
+ ToolApprovalGranted: "tool_approval_granted",
300
+ ToolPermissionDenied: "tool_permission_denied",
301
+ ToolHarnessWarning: "tool_harness_warning",
302
+ UserUnavailable: "user_unavailable",
303
+ PromptSuggestion: "prompt_suggestion",
304
+ StateChanged: "state_changed",
305
+ // Generic fallthrough for unknown custom events
306
+ Custom: "custom"
307
+ };
308
+ var VOICE_POLISH_MODES = ["raw", "light", "structured", "formal"];
309
+ function isVoicePolishMode(value) {
310
+ return typeof value === "string" && VOICE_POLISH_MODES.includes(value);
311
+ }
312
+ function isVoiceLLMMode(mode) {
313
+ return mode !== "raw";
314
+ }
315
+
264
316
  // src/client.ts
265
317
  var DEFAULT_BASE_URL = "https://api.astralform.ai";
266
318
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -689,6 +741,94 @@ var AstralformClient = class {
689
741
  const raw = await response.json();
690
742
  return this.mapAsset(raw);
691
743
  }
744
+ // --- Voice input ---
745
+ /** The agent's voice-input defaults (`GET /v1/voice/config`). */
746
+ async getVoiceConfig() {
747
+ const raw = await this.get("/v1/voice/config");
748
+ return {
749
+ enabled: Boolean(raw.enabled),
750
+ modes: raw.modes ?? [...VOICE_POLISH_MODES],
751
+ // A mode this SDK does not know must not reach a `switch` typed as
752
+ // `VoicePolishMode`; `structured` is the server's own default.
753
+ defaultMode: isVoicePolishMode(raw.default_mode) ? raw.default_mode : "structured",
754
+ silenceAutoStopSeconds: raw.silence_auto_stop_seconds ?? 2,
755
+ autoSend: raw.auto_send ?? true,
756
+ maxRecordingSeconds: raw.max_recording_seconds ?? 300,
757
+ supportsStreaming: Boolean(raw.supports_streaming),
758
+ hotwords: raw.hotwords ?? []
759
+ };
760
+ }
761
+ /**
762
+ * Transcribe one recording with the agent's configured speech-to-text
763
+ * provider (`POST /v1/voice/transcriptions`). 16 kHz mono 16-bit WAV is the
764
+ * reference format; anything the provider accepts works.
765
+ *
766
+ * Deliberately outside `withDeadline`: a recording can run to
767
+ * `VoiceConfig.maxRecordingSeconds`, so the 30 s default would cut real
768
+ * uploads off. Pass `options.signal` to give up on a stalled one; the
769
+ * promise then rejects with the abort reason — the runtime's `AbortError`,
770
+ * or whatever was passed to `abort(reason)`.
771
+ */
772
+ async transcribeVoice(audio, options = {}) {
773
+ const formData = new FormData();
774
+ formData.append("file", audio, options.filename ?? "recording.wav");
775
+ if (options.hotwords?.length) {
776
+ formData.append("hotwords", options.hotwords.join(", "));
777
+ }
778
+ if (options.language) {
779
+ formData.append("language", options.language);
780
+ }
781
+ const response = await this.fetchFn(`${this.baseURL}/v1/voice/transcriptions`, {
782
+ method: "POST",
783
+ headers: this.authHeaders,
784
+ body: formData,
785
+ signal: options.signal
786
+ }).catch((err) => {
787
+ if (options.signal?.aborted) {
788
+ throw err;
789
+ }
790
+ throw new ConnectionError(
791
+ err instanceof Error ? err.message : "Failed to connect"
792
+ );
793
+ });
794
+ await this.handleError(response);
795
+ const raw = await response.json();
796
+ return {
797
+ text: raw.text ?? "",
798
+ language: raw.language ?? null,
799
+ durationMs: raw.duration_ms ?? null,
800
+ asrMs: raw.asr_ms ?? 0
801
+ };
802
+ }
803
+ /**
804
+ * Stream the LLM rewrite of a transcript (`POST /v1/voice/polish`) as typed
805
+ * frames.
806
+ *
807
+ * Failures the server reports mid-stream arrive as an `error` frame, but
808
+ * the iteration itself can reject: aborting `signal` closes the connection
809
+ * (which cancels the model call upstream) and rejects with
810
+ * `StreamAbortedError`; a non-2xx open rejects with `AuthenticationError`,
811
+ * `RateLimitError` or `ServerError`; a network failure with
812
+ * `ConnectionError`. Wrap the `for await` accordingly.
813
+ */
814
+ async *streamVoicePolish(request, options = {}) {
815
+ const frames = streamJobSSE({
816
+ url: `${this.baseURL}/v1/voice/polish`,
817
+ headers: { ...this.headers, Accept: "text/event-stream" },
818
+ method: "POST",
819
+ body: JSON.stringify({
820
+ text: request.text,
821
+ mode: request.mode,
822
+ hotwords: request.hotwords ?? []
823
+ }),
824
+ signal: options.signal,
825
+ fetchFn: this.fetchFn
826
+ });
827
+ for await (const frame of frames) {
828
+ const event = parseVoicePolishFrame(frame);
829
+ if (event) yield event;
830
+ }
831
+ }
692
832
  async listUploads(conversationId) {
693
833
  const raw = await this.get(
694
834
  `/v1/conversations/${encodeURIComponent(conversationId)}/uploads`
@@ -775,6 +915,35 @@ var AstralformClient = class {
775
915
  }));
776
916
  }
777
917
  };
918
+ function parseVoicePolishFrame(frame) {
919
+ let payload;
920
+ try {
921
+ const parsed = JSON.parse(frame.data);
922
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
923
+ payload = parsed;
924
+ } catch {
925
+ return null;
926
+ }
927
+ switch (frame.event) {
928
+ case "delta":
929
+ return typeof payload.text === "string" ? { type: "delta", text: payload.text } : null;
930
+ case "done":
931
+ return typeof payload.text === "string" ? {
932
+ type: "done",
933
+ text: payload.text,
934
+ polishMs: payload.polish_ms ?? 0
935
+ } : null;
936
+ case "error":
937
+ return {
938
+ type: "error",
939
+ reason: payload.reason ?? "unknown",
940
+ partial: payload.partial ?? "",
941
+ ...typeof payload.detail === "string" ? { detail: payload.detail } : {}
942
+ };
943
+ default:
944
+ return null;
945
+ }
946
+ }
778
947
 
779
948
  // src/storage.ts
780
949
  var InMemoryStorage = class {
@@ -2259,50 +2428,6 @@ function planRestore(args) {
2259
2428
  return steps;
2260
2429
  }
2261
2430
 
2262
- // src/types.ts
2263
- var ChatEventType = {
2264
- // Connection lifecycle (SDK-local, not wire)
2265
- Connected: "connected",
2266
- Disconnected: "disconnected",
2267
- // Turn lifecycle
2268
- MessageStart: "message_start",
2269
- MessageStop: "message_stop",
2270
- // Block lifecycle
2271
- BlockStart: "block_start",
2272
- BlockDelta: "block_delta",
2273
- BlockStop: "block_stop",
2274
- // Reliability
2275
- Stall: "stall",
2276
- Retry: "retry",
2277
- Error: "error",
2278
- Keepalive: "keepalive",
2279
- // Conversation-level (typed custom events)
2280
- UserMessage: "user_message",
2281
- TitleGenerated: "title_generated",
2282
- TodoUpdate: "todo_update",
2283
- PlanUpdate: "plan_update",
2284
- NoteUpdate: "note_update",
2285
- ContextUpdate: "context_update",
2286
- SubagentStart: "subagent_start",
2287
- SubagentStop: "subagent_stop",
2288
- ContextWarning: "context_warning",
2289
- MemoryRecall: "memory_recall",
2290
- MemoryUpdate: "memory_update",
2291
- DesktopStream: "desktop_stream",
2292
- AttachmentStaged: "attachment_staged",
2293
- WorkspaceReady: "workspace_ready",
2294
- AssetCreated: "asset_created",
2295
- ToolApprovalRequested: "tool_approval_requested",
2296
- ToolApprovalGranted: "tool_approval_granted",
2297
- ToolPermissionDenied: "tool_permission_denied",
2298
- ToolHarnessWarning: "tool_harness_warning",
2299
- UserUnavailable: "user_unavailable",
2300
- PromptSuggestion: "prompt_suggestion",
2301
- StateChanged: "state_changed",
2302
- // Generic fallthrough for unknown custom events
2303
- Custom: "custom"
2304
- };
2305
-
2306
2431
  // src/stream-manager.ts
2307
2432
  var StreamManager = class {
2308
2433
  constructor(session) {
@@ -2324,6 +2449,13 @@ var StreamManager = class {
2324
2449
  * its awaits. This can.
2325
2450
  */
2326
2451
  this.turnCounter = 0;
2452
+ /**
2453
+ * True while a `resync` is between its probe and its restore. Visibility and
2454
+ * focus listeners can both fire for one return, and two overlapping resyncs
2455
+ * would each detach the other's stream mid-flight — the second call must
2456
+ * find the flag set and leave the first to converge.
2457
+ */
2458
+ this._resyncing = false;
2327
2459
  this.session = session;
2328
2460
  this.attach();
2329
2461
  }
@@ -2508,6 +2640,73 @@ var StreamManager = class {
2508
2640
  }
2509
2641
  }
2510
2642
  }
2643
+ // ── Resync after the page sat in the background ───────────────
2644
+ /**
2645
+ * Re-attach to whatever the server says is live for the ACTIVE conversation.
2646
+ *
2647
+ * The reconnect machinery inside ``consumeEventStream`` only runs while a
2648
+ * stream is being consumed — and a page suspended in the background (locked
2649
+ * phone, app switch, hidden tab) can outlive it: timers are throttled or
2650
+ * suspended, so the stall watchdog may never fire while hidden; the
2651
+ * reconnect budget (``SSE_MAX_RECONNECTS``) can burn out in fail-fast
2652
+ * attempts; and a 401 from a rotated access token ends the loop outright as
2653
+ * non-retryable. What is left is a manager that believes a turn is streaming
2654
+ * (or has given up on one that is still running) with nothing attached — and
2655
+ * no navigation will ever fix it, because ``switchTo`` early-returns on the
2656
+ * conversation it is already on.
2657
+ *
2658
+ * So the consumer calls this when the user COMES BACK
2659
+ * (``visibilitychange → visible``, window ``focus``). One ``getActiveJob``
2660
+ * probe, then:
2661
+ *
2662
+ * - attached to exactly the job the server calls live → healthy. The stall
2663
+ * watchdog owns zombie recovery from here, now that timers run again.
2664
+ * No-op.
2665
+ * - anything else — attached to a job the server no longer calls live,
2666
+ * attached to nothing while a job runs, or idle with a live job another
2667
+ * tab/device started — → detach and re-run ``restore``, the same path a
2668
+ * conversation reopen takes, with all of its supersession and takeover
2669
+ * guards inherited.
2670
+ *
2671
+ * Skipped while a restore is already in flight (it is converging on server
2672
+ * truth by itself) and while another resync holds the flag — see
2673
+ * ``_resyncing``.
2674
+ */
2675
+ async resync() {
2676
+ const conversationId = this._activeConversationId;
2677
+ if (!conversationId) return;
2678
+ if (this._state === "restoring" || this._resyncing) return;
2679
+ this._resyncing = true;
2680
+ const gen = this.generation;
2681
+ const turn = this.turnCounter;
2682
+ try {
2683
+ let activeJobId = null;
2684
+ try {
2685
+ activeJobId = (await this.session.client.getActiveJob(conversationId)).jobId;
2686
+ } catch {
2687
+ return;
2688
+ }
2689
+ if (gen !== this.generation || this.turnStarted(turn)) return;
2690
+ const attached = this._state === "streaming" || this.session.isStreaming;
2691
+ if (activeJobId === null && !attached) return;
2692
+ if (activeJobId !== null && this.session.isStreaming && this.session.currentJobId === activeJobId) {
2693
+ return;
2694
+ }
2695
+ this.session.detach();
2696
+ this.session.currentJobId = null;
2697
+ this._state = "idle";
2698
+ try {
2699
+ await this.restore(conversationId, gen);
2700
+ } catch {
2701
+ }
2702
+ } finally {
2703
+ this._resyncing = false;
2704
+ const restoring = "restoring";
2705
+ if (gen === this.generation && this._state === restoring) {
2706
+ this.settleIdle();
2707
+ }
2708
+ }
2709
+ }
2511
2710
  // ── Create / rename / delete conversation ─────────────────────
2512
2711
  /**
2513
2712
  * Create a conversation and make it active.
@@ -2882,10 +3081,14 @@ export {
2882
3081
  StreamAbortedError,
2883
3082
  StreamManager,
2884
3083
  ToolRegistry,
3084
+ VOICE_POLISH_MODES,
2885
3085
  generateId,
2886
3086
  isEmbeddedResource,
3087
+ isVoiceLLMMode,
3088
+ isVoicePolishMode,
2887
3089
  mapSseToChat,
2888
3090
  parseEmbeddedResource,
3091
+ parseVoicePolishFrame,
2889
3092
  replayEvents,
2890
3093
  streamJobSSE,
2891
3094
  translateDelta