@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.cjs +253 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +189 -2
- package/dist/index.d.ts +189 -2
- package/dist/index.js +249 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|