@space3-npm/cybersoul-client 1.4.3 → 1.4.5

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
@@ -1,4 +1,4 @@
1
- import { CyberSoulClientConfig, InteractParams, ProactiveParams, ProactiveResponse, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory, UserCodex, HistoryEntry, LikedPicture } from "./types.js";
1
+ import { CyberSoulClientConfig, InteractParams, ProactiveParams, ProactiveResponse, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory, UserCodex, HistoryEntry, LikedPicture, PersistedDynamicContext } from "./types.js";
2
2
  export declare class CyberSoulClient {
3
3
  private config;
4
4
  private llm;
@@ -14,6 +14,18 @@ export declare class CyberSoulClient {
14
14
  private fetchRemoteState;
15
15
  private getWardrobePromptStr;
16
16
  private generatePrimitive;
17
+ /**
18
+ * PATCH the backend dynamic context. The server applies stage-based
19
+ * dampening, familiarity soft-caps, hard floor, and stage re-evaluation,
20
+ * then returns the *authoritative* persisted `temperature` and
21
+ * `relationshipStage`. We surface those so callers (and ultimately the UI)
22
+ * can avoid recomputing the delta locally — local math would diverge from
23
+ * the server because the LLM-supplied `temperatureDelta` is just raw intent.
24
+ *
25
+ * Returns `null` when there's nothing to send, or when the request fails
26
+ * (failure is non-fatal for the chat turn; callers must treat `null` as
27
+ * "no fresh server snapshot available").
28
+ */
17
29
  private _updateDynamicContextInternal;
18
30
  private normalizeRequestTypes;
19
31
  private getElapsedTimeInfo;
@@ -42,6 +54,22 @@ export declare class CyberSoulClient {
42
54
  * If the payload is already the inner args object (no voiceArgs wrapper), uses it as-is.
43
55
  */
44
56
  private extractVoiceArgsFromLlmResponse;
57
+ /**
58
+ * Strip content the TTS engine can't speak naturally:
59
+ * - Stage-direction wrappers like (smiles), (挑眉), [pauses], 【动作】, *grins*
60
+ * — these slip through despite prompt instructions and the engine will
61
+ * literally read the brackets/asterisks if left in.
62
+ * - Emoji and emoji-component codepoints (Extended_Pictographic plus the
63
+ * ZWJ / variation-selector / skin-tone / regional-indicator scaffolding
64
+ * that builds composite emoji). TTS providers either read these aloud
65
+ * as the literal Unicode name ("face with tears of joy") or produce a
66
+ * glitchy artifact, both of which sound wrong.
67
+ *
68
+ * Collapses runs of whitespace introduced by removals and trims the result.
69
+ * Returns "" if everything gets stripped — callers should fall back to a
70
+ * neutral placeholder (e.g. "...") so the TTS call still has valid input.
71
+ */
72
+ private sanitizeTextForVoice;
45
73
  private formatHistoryEntries;
46
74
  private buildHistoryTranscript;
47
75
  interact(params: InteractParams): Promise<InteractResponse>;
@@ -79,8 +107,10 @@ export declare class CyberSoulClient {
79
107
  getState(): Promise<CharacterState>;
80
108
  /**
81
109
  * Updates the character's relationship temperature or mood.
110
+ * Returns the server-authoritative post-write `{ temperature, relationshipStage }`
111
+ * snapshot (or `null` if there was nothing to send / the request failed).
82
112
  */
83
- updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<void>;
113
+ updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<PersistedDynamicContext | null>;
84
114
  /**
85
115
  * Gift a new outfit to the character's wardrobe inventory.
86
116
  */
package/dist/client.js CHANGED
@@ -13,7 +13,7 @@ export class CyberSoulClient {
13
13
  this.requestTimeoutMs = config.requestTimeoutMs ?? 120000;
14
14
  this.maxRetries = Math.max(0, config.maxRetries ?? 1);
15
15
  // Setup Provider
16
- this.llm = new GenericLLMProvider(config.llmConfig, config.backendUrl, config.characterKey);
16
+ this.llm = new GenericLLMProvider(config.llmConfig, config.backendUrl, config.characterKey, config.fetchImpl);
17
17
  }
18
18
  /**
19
19
  * Internal wrapper for fetch that automatically injects the backend URL and Character Auth token.
@@ -33,7 +33,8 @@ export class CyberSoulClient {
33
33
  const controller = new AbortController();
34
34
  const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
35
35
  try {
36
- const response = await fetch(url, {
36
+ const fetchFn = this.config.fetchImpl ?? fetch;
37
+ const response = await fetchFn(url, {
37
38
  ...options,
38
39
  headers,
39
40
  signal: controller.signal,
@@ -115,9 +116,21 @@ export class CyberSoulClient {
115
116
  }
116
117
  return res.json();
117
118
  }
119
+ /**
120
+ * PATCH the backend dynamic context. The server applies stage-based
121
+ * dampening, familiarity soft-caps, hard floor, and stage re-evaluation,
122
+ * then returns the *authoritative* persisted `temperature` and
123
+ * `relationshipStage`. We surface those so callers (and ultimately the UI)
124
+ * can avoid recomputing the delta locally — local math would diverge from
125
+ * the server because the LLM-supplied `temperatureDelta` is just raw intent.
126
+ *
127
+ * Returns `null` when there's nothing to send, or when the request fails
128
+ * (failure is non-fatal for the chat turn; callers must treat `null` as
129
+ * "no fresh server snapshot available").
130
+ */
118
131
  async _updateDynamicContextInternal(stateUpdate, userAnalysis) {
119
132
  if (!stateUpdate && !userAnalysis)
120
- return;
133
+ return null;
121
134
  // Map TS schema intent (temperatureDelta) to match Backend payload schema (temperature)
122
135
  const payload = { ...stateUpdate };
123
136
  if (userAnalysis) {
@@ -131,10 +144,39 @@ export class CyberSoulClient {
131
144
  const normalizedOngoingScene = this.normalizeOngoingSceneState(payload.ongoingScene);
132
145
  payload.ongoingScene = normalizedOngoingScene || null;
133
146
  }
134
- await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
135
- method: "PATCH",
136
- body: JSON.stringify(payload),
137
- }).catch((e) => console.error("Failed to update dynamic context", e)); // non-blocking error handler
147
+ let res;
148
+ try {
149
+ res = await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
150
+ method: "PATCH",
151
+ body: JSON.stringify(payload),
152
+ });
153
+ }
154
+ catch (e) {
155
+ console.error("Failed to update dynamic context", e);
156
+ return null;
157
+ }
158
+ if (!res.ok) {
159
+ console.error(`Failed to update dynamic context: HTTP ${res.status}`);
160
+ return null;
161
+ }
162
+ try {
163
+ const body = (await res.json());
164
+ const temperature = typeof body.dynamicContext?.temperature === "number" &&
165
+ Number.isFinite(body.dynamicContext.temperature)
166
+ ? body.dynamicContext.temperature
167
+ : undefined;
168
+ const relationshipStage = typeof body.relationshipStage === "string"
169
+ ? body.relationshipStage
170
+ : undefined;
171
+ if (temperature === undefined && relationshipStage === undefined) {
172
+ return null;
173
+ }
174
+ return { temperature, relationshipStage };
175
+ }
176
+ catch (e) {
177
+ console.error("Failed to parse dynamic-context PATCH response", e);
178
+ return null;
179
+ }
138
180
  }
139
181
  normalizeRequestTypes(requestTypes) {
140
182
  let normalized = requestTypes;
@@ -408,6 +450,32 @@ ${isProactive
408
450
  }
409
451
  return payload;
410
452
  }
453
+ /**
454
+ * Strip content the TTS engine can't speak naturally:
455
+ * - Stage-direction wrappers like (smiles), (挑眉), [pauses], 【动作】, *grins*
456
+ * — these slip through despite prompt instructions and the engine will
457
+ * literally read the brackets/asterisks if left in.
458
+ * - Emoji and emoji-component codepoints (Extended_Pictographic plus the
459
+ * ZWJ / variation-selector / skin-tone / regional-indicator scaffolding
460
+ * that builds composite emoji). TTS providers either read these aloud
461
+ * as the literal Unicode name ("face with tears of joy") or produce a
462
+ * glitchy artifact, both of which sound wrong.
463
+ *
464
+ * Collapses runs of whitespace introduced by removals and trims the result.
465
+ * Returns "" if everything gets stripped — callers should fall back to a
466
+ * neutral placeholder (e.g. "...") so the TTS call still has valid input.
467
+ */
468
+ sanitizeTextForVoice(text) {
469
+ if (typeof text !== "string")
470
+ return "";
471
+ return text
472
+ // (parens), (全角), [brackets], 【全角】, *asterisks*
473
+ .replace(/[\((\[【\*].*?[\))\]】\*]/g, "")
474
+ // emoji + ZWJ + variation selectors + skin-tone modifiers + regional indicators
475
+ .replace(/[\p{Extended_Pictographic}\u200D\uFE0F\uFE0E\u{1F3FB}-\u{1F3FF}\u{1F1E6}-\u{1F1FF}]/gu, "")
476
+ .replace(/\s+/g, " ")
477
+ .trim();
478
+ }
411
479
  formatHistoryEntries(history, userName, agentName, promptDirective = "") {
412
480
  const contextLines = [];
413
481
  for (let i = 0; i < history.length; i++) {
@@ -574,9 +642,15 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
574
642
  };
575
643
  }
576
644
  // console.debug("[CyberSoulClient] Parsed Intent:", parsedIntent);
577
- // 4. Update Backend State async
645
+ // 4. Update Backend State async (in parallel with media generation
646
+ // below). We keep the promise so we can resolve the
647
+ // server-authoritative `temperature` / `relationshipStage` and
648
+ // return it in the final response — clients cannot reproduce the
649
+ // server's stage dampening + soft caps locally, so this is the only
650
+ // reliable source of truth.
651
+ let persistedStatePromise = Promise.resolve(null);
578
652
  if (parsedIntent && (parsedIntent.stateUpdate || parsedIntent.userAnalysis)) {
579
- this._updateDynamicContextInternal(parsedIntent.stateUpdate, parsedIntent.userAnalysis);
653
+ persistedStatePromise = this._updateDynamicContextInternal(parsedIntent.stateUpdate, parsedIntent.userAnalysis);
580
654
  }
581
655
  const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
582
656
  parsedIntent.textResponse.trim().length > 0
@@ -642,12 +716,8 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
642
716
  const normalizedVoiceArgs = parsedIntent.voiceArgs && typeof parsedIntent.voiceArgs === "object"
643
717
  ? parsedIntent.voiceArgs
644
718
  : {};
645
- let textForVoice = resolvedTextResponse;
646
- // One final bulletproof regex wash to strip (smiles) and *laughs* just in case the LLM disobeys
647
- if (typeof textForVoice === "string") {
648
- textForVoice = textForVoice.replace(/[\((\[【\*].*?[\))\]】\*]/g, '').trim();
649
- }
650
- if (typeof textForVoice !== "string" || textForVoice.trim().length === 0) {
719
+ let textForVoice = this.sanitizeTextForVoice(resolvedTextResponse);
720
+ if (textForVoice.length === 0) {
651
721
  textForVoice = "...";
652
722
  }
653
723
  mediaTasks.push(this.generatePrimitive("voice", {
@@ -666,6 +736,11 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
666
736
  }
667
737
  // Wait for image/voice gens to return successfully
668
738
  await Promise.all(mediaTasks);
739
+ // Await the dynamic-context PATCH alongside media so the final
740
+ // response carries the server's authoritative temperature/stage.
741
+ // This adds at most ~1 small request to the critical path; in
742
+ // practice the PATCH usually resolves before media generation.
743
+ const persistedDynamicContext = (await persistedStatePromise) ?? undefined;
669
744
  return {
670
745
  status: "success",
671
746
  textResponse: resolvedTextResponse || "...",
@@ -678,6 +753,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
678
753
  stateUpdate: parsedIntent.stateUpdate,
679
754
  userAnalysis: parsedIntent.userAnalysis,
680
755
  isEndTurn: parsedIntent.isEndTurn,
756
+ persistedDynamicContext,
681
757
  };
682
758
  }
683
759
  catch (error) {
@@ -886,9 +962,11 @@ You MUST output ONLY a valid JSON object matching exactly this structure:
886
962
  reason: parsedIntent.skipReason || "Character decided to skip proactive message based on mood/stage."
887
963
  };
888
964
  }
889
- // Update Remote state if needed
965
+ // Update Remote state if needed (capture promise for authoritative
966
+ // server snapshot — see notes in interact()).
967
+ let persistedStatePromise = Promise.resolve(null);
890
968
  if (parsedIntent.stateUpdate) {
891
- this._updateDynamicContextInternal(parsedIntent.stateUpdate).catch(e => console.error(e));
969
+ persistedStatePromise = this._updateDynamicContextInternal(parsedIntent.stateUpdate);
892
970
  }
893
971
  const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
894
972
  parsedIntent.textResponse.trim().length > 0
@@ -915,12 +993,14 @@ You MUST output ONLY a valid JSON object matching exactly this structure:
915
993
  console.error("[CyberSoulClient] Proactive Image generation failed:", e);
916
994
  }
917
995
  }
996
+ const persistedDynamicContext = (await persistedStatePromise) ?? undefined;
918
997
  return {
919
998
  status: "success",
920
999
  textResponse: parsedIntent.textResponse,
921
1000
  actionText: parsedIntent.actionText,
922
1001
  imageUrl: finalImageUrl,
923
- stateUpdate: parsedIntent.stateUpdate
1002
+ stateUpdate: parsedIntent.stateUpdate,
1003
+ persistedDynamicContext,
924
1004
  };
925
1005
  }
926
1006
  catch (error) {
@@ -994,7 +1074,7 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
994
1074
  dynamicArgs = {};
995
1075
  }
996
1076
  const res = await this.generatePrimitive("voice", {
997
- text: params.text,
1077
+ text: this.sanitizeTextForVoice(params.text) || "...",
998
1078
  dynamicArgs,
999
1079
  });
1000
1080
  return {
@@ -1010,6 +1090,8 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1010
1090
  }
1011
1091
  /**
1012
1092
  * Updates the character's relationship temperature or mood.
1093
+ * Returns the server-authoritative post-write `{ temperature, relationshipStage }`
1094
+ * snapshot (or `null` if there was nothing to send / the request failed).
1013
1095
  */
1014
1096
  async updateDynamicContext(stateUpdate, userAnalysis) {
1015
1097
  return this._updateDynamicContextInternal(stateUpdate, userAnalysis);
@@ -3,8 +3,10 @@ export declare class GenericLLMProvider implements BaseLLMProvider {
3
3
  private config;
4
4
  private backendApiUrl;
5
5
  private backendAuthToken?;
6
+ private fetchImpl?;
6
7
  private static templateCache;
7
- constructor(config: GenericLLMConfig, backendApiUrl: string, backendAuthToken?: string | undefined);
8
+ constructor(config: GenericLLMConfig, backendApiUrl: string, backendAuthToken?: string | undefined, fetchImpl?: typeof fetch | undefined);
9
+ private get fetchFn();
8
10
  private fetchTemplate;
9
11
  private extractResponse;
10
12
  generate(messages: {
@@ -2,11 +2,16 @@ export class GenericLLMProvider {
2
2
  config;
3
3
  backendApiUrl;
4
4
  backendAuthToken;
5
+ fetchImpl;
5
6
  static templateCache = new Map();
6
- constructor(config, backendApiUrl, backendAuthToken) {
7
+ constructor(config, backendApiUrl, backendAuthToken, fetchImpl) {
7
8
  this.config = config;
8
9
  this.backendApiUrl = backendApiUrl;
9
10
  this.backendAuthToken = backendAuthToken;
11
+ this.fetchImpl = fetchImpl;
12
+ }
13
+ get fetchFn() {
14
+ return this.fetchImpl ?? fetch;
10
15
  }
11
16
  async fetchTemplate() {
12
17
  const cacheKey = `${this.config.provider}:${this.config.model}`;
@@ -24,7 +29,7 @@ export class GenericLLMProvider {
24
29
  provider: this.config.provider,
25
30
  model: this.config.model
26
31
  });
27
- const resp = await fetch(`${this.backendApiUrl}/api/v1/cyber-soul/llm-models/template?${qs.toString()}`, {
32
+ const resp = await this.fetchFn(`${this.backendApiUrl}/api/v1/cyber-soul/llm-models/template?${qs.toString()}`, {
28
33
  headers
29
34
  });
30
35
  if (!resp.ok) {
@@ -66,7 +71,7 @@ export class GenericLLMProvider {
66
71
  if (!payload.messages || (Array.isArray(payload.messages) && payload.messages.length === 0)) {
67
72
  payload.messages = messages;
68
73
  }
69
- const response = await fetch(template.apiUrl, {
74
+ const response = await this.fetchFn(template.apiUrl, {
70
75
  method: 'POST',
71
76
  headers,
72
77
  body: JSON.stringify(payload)
package/dist/types.d.ts CHANGED
@@ -10,6 +10,15 @@ export interface CyberSoulClientConfig {
10
10
  llmConfig: GenericLLMConfig;
11
11
  requestTimeoutMs?: number;
12
12
  maxRetries?: number;
13
+ /**
14
+ * Optional fetch override. When provided, the client uses this in
15
+ * place of the global `fetch` for every HTTP call (backend + LLM
16
+ * provider). Intended for environments where the global fetch is
17
+ * suspended by the host platform — e.g. React Native on Samsung
18
+ * BBA / Doze — and a native HTTP path must be used instead. Must
19
+ * conform to the standard `fetch` signature.
20
+ */
21
+ fetchImpl?: typeof fetch;
13
22
  }
14
23
  export declare enum InteractRequestType {
15
24
  AUTO = "auto",
@@ -32,6 +41,18 @@ export interface InteractMetadata {
32
41
  triggerEvent?: DispatcherIntent["triggerEvent"];
33
42
  likePreviousPicture?: boolean;
34
43
  }
44
+ /**
45
+ * Server-authoritative snapshot returned by PATCH /characters/dynamic-context
46
+ * after the backend applies stage dampening, familiarity soft caps, hard
47
+ * floors, rounding, and stage re-evaluation. Use this instead of recomputing
48
+ * the delta on the client.
49
+ */
50
+ export interface PersistedDynamicContext {
51
+ /** Persisted absolute temperature (0-100), post all server-side adjustments. */
52
+ temperature?: number;
53
+ /** Persisted relationship stage label after re-evaluation. */
54
+ relationshipStage?: string;
55
+ }
35
56
  export interface ProactiveParams {
36
57
  history?: HistoryEntry[];
37
58
  maxUnreplied?: number;
@@ -47,6 +68,8 @@ export interface ProactiveResponse {
47
68
  imageUrl?: string;
48
69
  audioUrl?: string;
49
70
  stateUpdate?: DispatcherIntent["stateUpdate"];
71
+ /** Server-authoritative post-write snapshot (see PersistedDynamicContext). */
72
+ persistedDynamicContext?: PersistedDynamicContext;
50
73
  error?: string;
51
74
  }
52
75
  export interface InteractParams {
@@ -105,6 +128,8 @@ export interface InteractResponse {
105
128
  stateUpdate?: DispatcherIntent["stateUpdate"];
106
129
  userAnalysis?: DispatcherIntent["userAnalysis"];
107
130
  isEndTurn?: boolean;
131
+ /** Server-authoritative post-write snapshot (see PersistedDynamicContext). */
132
+ persistedDynamicContext?: PersistedDynamicContext;
108
133
  error?: string;
109
134
  }
110
135
  export interface OngoingSceneState {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",