@space3-npm/cybersoul-client 1.4.12 → 1.4.15

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/client.d.ts CHANGED
@@ -70,6 +70,14 @@ export declare class CyberSoulClient {
70
70
  * neutral placeholder (e.g. "...") so the TTS call still has valid input.
71
71
  */
72
72
  private sanitizeTextForVoice;
73
+ /**
74
+ * Build the in-band `mediaError` envelope from the first typed media
75
+ * failure captured during `interact()` / `proactiveInteract()`. Keeps
76
+ * the conversion in one place so both call sites stay consistent and
77
+ * the SDK never re-throws on a partial media failure once the text
78
+ * reply is already in flight.
79
+ */
80
+ private buildMediaError;
73
81
  private formatHistoryEntries;
74
82
  private buildHistoryTranscript;
75
83
  interact(params: InteractParams): Promise<InteractResponse>;
package/dist/client.js CHANGED
@@ -34,7 +34,11 @@ export class CyberSoulClient {
34
34
  const controller = new AbortController();
35
35
  const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
36
36
  try {
37
- const fetchFn = this.config.fetchImpl ?? fetch;
37
+ // NOTE: When no custom fetchImpl is provided, fall back to the global
38
+ // `fetch` bound to `globalThis`. Browsers throw "Illegal invocation"
39
+ // if the global `fetch` is invoked while detached from its Window
40
+ // receiver (e.g. via a captured reference).
41
+ const fetchFn = this.config.fetchImpl ?? fetch.bind(globalThis);
38
42
  const response = await fetchFn(url, {
39
43
  ...options,
40
44
  headers,
@@ -515,6 +519,35 @@ ${isProactive
515
519
  .replace(/\s+/g, " ")
516
520
  .trim();
517
521
  }
522
+ /**
523
+ * Build the in-band `mediaError` envelope from the first typed media
524
+ * failure captured during `interact()` / `proactiveInteract()`. Keeps
525
+ * the conversion in one place so both call sites stay consistent and
526
+ * the SDK never re-throws on a partial media failure once the text
527
+ * reply is already in flight.
528
+ */
529
+ buildMediaError(err, affected) {
530
+ if (err instanceof CyberSoulInsufficientPointsError) {
531
+ return {
532
+ kind: "insufficient-points",
533
+ code: err.code,
534
+ message: err.message,
535
+ affected,
536
+ };
537
+ }
538
+ if (err instanceof CyberSoulWalletError) {
539
+ return {
540
+ kind: "wallet",
541
+ message: err.message,
542
+ affected,
543
+ };
544
+ }
545
+ return {
546
+ kind: "unknown",
547
+ message: err.message,
548
+ affected,
549
+ };
550
+ }
518
551
  formatHistoryEntries(history, userName, agentName, promptDirective = "") {
519
552
  const contextLines = [];
520
553
  for (let i = 0; i < history.length; i++) {
@@ -712,6 +745,27 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
712
745
  let finalAudioUrl = undefined;
713
746
  let finalAudioMediaId = undefined;
714
747
  let finalDurationSec = undefined;
748
+ // Partial-failure capture: text was already produced and emitted
749
+ // via [onTextReady], so a wallet / insufficient-points failure on
750
+ // image or voice MUST NOT abort the whole turn. We collect the
751
+ // affected modalities + first typed error and surface them in-band
752
+ // through `InteractResponse.mediaError`. The caller (MessageBus /
753
+ // UI) decides how to message the user without losing the reply.
754
+ const mediaErrorAffected = [];
755
+ let firstMediaError = null;
756
+ const captureMediaError = (modality, e) => {
757
+ if (!(e instanceof CyberSoulError))
758
+ return;
759
+ if (!(e instanceof CyberSoulInsufficientPointsError) &&
760
+ !(e instanceof CyberSoulWalletError)) {
761
+ return;
762
+ }
763
+ if (!mediaErrorAffected.includes(modality)) {
764
+ mediaErrorAffected.push(modality);
765
+ }
766
+ if (!firstMediaError)
767
+ firstMediaError = e;
768
+ };
715
769
  // Output Event Trigger
716
770
  if (parsedIntent.triggerEvent) {
717
771
  mediaTasks.push(this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
@@ -747,9 +801,11 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
747
801
  finalImageMediaId = res.id;
748
802
  })
749
803
  .catch((e) => {
750
- console.error("[CyberSoulClient] Image generation failed:", e);
751
- if (e.code === 'INSUFFICIENT_POINTS' || e.code === 'WALLET_DEDUCTION_ERROR')
752
- throw e;
804
+ if (!(e instanceof CyberSoulInsufficientPointsError) &&
805
+ !(e instanceof CyberSoulWalletError)) {
806
+ console.error("[CyberSoulClient] Image generation failed:", e);
807
+ }
808
+ captureMediaError("image", e);
753
809
  }));
754
810
  }
755
811
  const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) &&
@@ -772,9 +828,11 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
772
828
  finalDurationSec = res.duration_sec;
773
829
  })
774
830
  .catch((e) => {
775
- console.error("[CyberSoulClient] Voice generation failed:", e);
776
- if (e.code === 'INSUFFICIENT_POINTS' || e.code === 'WALLET_DEDUCTION_ERROR')
777
- throw e;
831
+ if (!(e instanceof CyberSoulInsufficientPointsError) &&
832
+ !(e instanceof CyberSoulWalletError)) {
833
+ console.error("[CyberSoulClient] Voice generation failed:", e);
834
+ }
835
+ captureMediaError("voice", e);
778
836
  }));
779
837
  }
780
838
  // Wait for image/voice gens to return successfully
@@ -784,6 +842,9 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
784
842
  // This adds at most ~1 small request to the critical path; in
785
843
  // practice the PATCH usually resolves before media generation.
786
844
  const persistedDynamicContext = (await persistedStatePromise) ?? undefined;
845
+ const mediaError = firstMediaError
846
+ ? this.buildMediaError(firstMediaError, mediaErrorAffected)
847
+ : undefined;
787
848
  return {
788
849
  status: "success",
789
850
  textResponse: resolvedTextResponse || "...",
@@ -799,6 +860,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
799
860
  userAnalysis: parsedIntent.userAnalysis,
800
861
  isEndTurn: parsedIntent.isEndTurn,
801
862
  persistedDynamicContext,
863
+ mediaError,
802
864
  };
803
865
  }
804
866
  catch (error) {
@@ -1040,6 +1102,8 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1040
1102
  }
1041
1103
  let finalImageUrl;
1042
1104
  let finalImageMediaId;
1105
+ let proactiveMediaError = null;
1106
+ const proactiveAffected = [];
1043
1107
  if (parsedIntent.imageParams) {
1044
1108
  try {
1045
1109
  const res = await this.generatePrimitive("image", parsedIntent.imageParams);
@@ -1047,10 +1111,20 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1047
1111
  finalImageMediaId = res.id;
1048
1112
  }
1049
1113
  catch (e) {
1050
- console.error("[CyberSoulClient] Proactive Image generation failed:", e);
1114
+ if (e instanceof CyberSoulInsufficientPointsError ||
1115
+ e instanceof CyberSoulWalletError) {
1116
+ proactiveMediaError = e;
1117
+ proactiveAffected.push("image");
1118
+ }
1119
+ else {
1120
+ console.error("[CyberSoulClient] Proactive Image generation failed:", e);
1121
+ }
1051
1122
  }
1052
1123
  }
1053
1124
  const persistedDynamicContext = (await persistedStatePromise) ?? undefined;
1125
+ const proactiveMediaErrorEnv = proactiveMediaError
1126
+ ? this.buildMediaError(proactiveMediaError, proactiveAffected)
1127
+ : undefined;
1054
1128
  return {
1055
1129
  status: "success",
1056
1130
  textResponse: parsedIntent.textResponse,
@@ -1059,6 +1133,7 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1059
1133
  imageMediaId: finalImageMediaId,
1060
1134
  stateUpdate: parsedIntent.stateUpdate,
1061
1135
  persistedDynamicContext,
1136
+ mediaError: proactiveMediaErrorEnv,
1062
1137
  };
1063
1138
  }
1064
1139
  catch (error) {
@@ -11,7 +11,10 @@ export class GenericLLMProvider {
11
11
  this.fetchImpl = fetchImpl;
12
12
  }
13
13
  get fetchFn() {
14
- return this.fetchImpl ?? fetch;
14
+ // Bind to `globalThis` so the global `fetch` is not invoked detached
15
+ // from its Window receiver (which throws "Illegal invocation" in
16
+ // Chromium-based browsers).
17
+ return this.fetchImpl ?? fetch.bind(globalThis);
15
18
  }
16
19
  async fetchTemplate() {
17
20
  const cacheKey = `${this.config.provider}:${this.config.model}`;
package/dist/types.d.ts CHANGED
@@ -72,6 +72,11 @@ export interface ProactiveResponse {
72
72
  stateUpdate?: DispatcherIntent["stateUpdate"];
73
73
  /** Server-authoritative post-write snapshot (see PersistedDynamicContext). */
74
74
  persistedDynamicContext?: PersistedDynamicContext;
75
+ /** Partial-failure descriptor: text was generated successfully but one or
76
+ * more media calls (image/voice) failed. Surfaced in-band so the caller
77
+ * can still render the text reply and explain the missing media
78
+ * without losing the conversation. See [InteractMediaError]. */
79
+ mediaError?: InteractMediaError;
75
80
  error?: string;
76
81
  }
77
82
  export interface InteractParams {
@@ -134,8 +139,30 @@ export interface InteractResponse {
134
139
  isEndTurn?: boolean;
135
140
  /** Server-authoritative post-write snapshot (see PersistedDynamicContext). */
136
141
  persistedDynamicContext?: PersistedDynamicContext;
142
+ /** Partial-failure descriptor: text was generated successfully but one or
143
+ * more media calls (image/voice) failed. Surfaced in-band so the caller
144
+ * can still render the text reply and explain the missing media
145
+ * without losing the conversation. See [InteractMediaError]. */
146
+ mediaError?: InteractMediaError;
137
147
  error?: string;
138
148
  }
149
+ /**
150
+ * Describes a partial-failure during an [interact] / [proactiveInteract]
151
+ * call: the text reply was generated and returned, but image and/or
152
+ * voice generation failed (usually because the user ran out of points
153
+ * mid-turn). Surfaced in-band on the success envelope so callers can
154
+ * render the text response without losing it to an exception.
155
+ */
156
+ export interface InteractMediaError {
157
+ /** Coarse kind so UIs can map to a single user-facing message. */
158
+ kind: "insufficient-points" | "wallet" | "unknown";
159
+ /** Backend machine code when available (e.g. "INSUFFICIENT_POINTS"). */
160
+ code?: string;
161
+ /** Raw error message, for logs / diagnostics. */
162
+ message?: string;
163
+ /** Which media generation calls were affected. */
164
+ affected: Array<"image" | "voice">;
165
+ }
139
166
  export interface OngoingSceneState {
140
167
  scene: string;
141
168
  outfit: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.12",
3
+ "version": "1.4.15",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",