@space3-npm/cybersoul-client 1.4.4 → 1.4.7

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, SupportedLLMModel } 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;
@@ -78,6 +90,7 @@ export declare class CyberSoulClient {
78
90
  interactParams?: InteractParams;
79
91
  }): Promise<{
80
92
  imageUrl: string;
93
+ imageMediaId?: string;
81
94
  }>;
82
95
  /**
83
96
  * Manually synthesize voice audio outside of chat flow.
@@ -87,16 +100,27 @@ export declare class CyberSoulClient {
87
100
  interactParams?: InteractParams;
88
101
  }): Promise<{
89
102
  audioUrl: string;
103
+ audioMediaId?: string;
90
104
  durationSec?: number;
91
105
  }>;
92
106
  /**
93
107
  * Fetches the current dynamic context and daily state.
94
108
  */
95
109
  getState(): Promise<CharacterState>;
110
+ /**
111
+ * List the public LLM models the backend currently supports, including the
112
+ * `customConfigDefinition` schema for each model's `customSettings`.
113
+ *
114
+ * Use this to discover valid `provider` / `model` strings and the keys
115
+ * each model accepts via `llmConfig.customSettings`.
116
+ */
117
+ listSupportedLLMs(): Promise<SupportedLLMModel[]>;
96
118
  /**
97
119
  * Updates the character's relationship temperature or mood.
120
+ * Returns the server-authoritative post-write `{ temperature, relationshipStage }`
121
+ * snapshot (or `null` if there was nothing to send / the request failed).
98
122
  */
99
- updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<void>;
123
+ updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<PersistedDynamicContext | null>;
100
124
  /**
101
125
  * Gift a new outfit to the character's wardrobe inventory.
102
126
  */
package/dist/client.js CHANGED
@@ -116,9 +116,21 @@ export class CyberSoulClient {
116
116
  }
117
117
  return res.json();
118
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
+ */
119
131
  async _updateDynamicContextInternal(stateUpdate, userAnalysis) {
120
132
  if (!stateUpdate && !userAnalysis)
121
- return;
133
+ return null;
122
134
  // Map TS schema intent (temperatureDelta) to match Backend payload schema (temperature)
123
135
  const payload = { ...stateUpdate };
124
136
  if (userAnalysis) {
@@ -132,10 +144,39 @@ export class CyberSoulClient {
132
144
  const normalizedOngoingScene = this.normalizeOngoingSceneState(payload.ongoingScene);
133
145
  payload.ongoingScene = normalizedOngoingScene || null;
134
146
  }
135
- await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
136
- method: "PATCH",
137
- body: JSON.stringify(payload),
138
- }).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
+ }
139
180
  }
140
181
  normalizeRequestTypes(requestTypes) {
141
182
  let normalized = requestTypes;
@@ -601,9 +642,15 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
601
642
  };
602
643
  }
603
644
  // console.debug("[CyberSoulClient] Parsed Intent:", parsedIntent);
604
- // 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);
605
652
  if (parsedIntent && (parsedIntent.stateUpdate || parsedIntent.userAnalysis)) {
606
- this._updateDynamicContextInternal(parsedIntent.stateUpdate, parsedIntent.userAnalysis);
653
+ persistedStatePromise = this._updateDynamicContextInternal(parsedIntent.stateUpdate, parsedIntent.userAnalysis);
607
654
  }
608
655
  const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
609
656
  parsedIntent.textResponse.trim().length > 0
@@ -622,7 +669,9 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
622
669
  // 5. Build Final Media Calls parallel
623
670
  const mediaTasks = [];
624
671
  let finalImageUrl = undefined;
672
+ let finalImageMediaId = undefined;
625
673
  let finalAudioUrl = undefined;
674
+ let finalAudioMediaId = undefined;
626
675
  let finalDurationSec = undefined;
627
676
  // Output Event Trigger
628
677
  if (parsedIntent.triggerEvent) {
@@ -656,6 +705,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
656
705
  mediaTasks.push(this.generatePrimitive("image", imagePayload)
657
706
  .then((res) => {
658
707
  finalImageUrl = res.image_url;
708
+ finalImageMediaId = res.id;
659
709
  })
660
710
  .catch((e) => {
661
711
  console.error("[CyberSoulClient] Image generation failed:", e);
@@ -679,6 +729,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
679
729
  })
680
730
  .then((res) => {
681
731
  finalAudioUrl = res.audio_url;
732
+ finalAudioMediaId = res.id;
682
733
  finalDurationSec = res.duration_sec;
683
734
  })
684
735
  .catch((e) => {
@@ -689,18 +740,26 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
689
740
  }
690
741
  // Wait for image/voice gens to return successfully
691
742
  await Promise.all(mediaTasks);
743
+ // Await the dynamic-context PATCH alongside media so the final
744
+ // response carries the server's authoritative temperature/stage.
745
+ // This adds at most ~1 small request to the critical path; in
746
+ // practice the PATCH usually resolves before media generation.
747
+ const persistedDynamicContext = (await persistedStatePromise) ?? undefined;
692
748
  return {
693
749
  status: "success",
694
750
  textResponse: resolvedTextResponse || "...",
695
751
  actionText: parsedIntent.actionText || "",
696
752
  imageUrl: finalImageUrl,
753
+ imageMediaId: finalImageMediaId,
697
754
  audioUrl: finalAudioUrl,
755
+ audioMediaId: finalAudioMediaId,
698
756
  likePreviousPicture: parsedIntent.likePreviousPicture,
699
757
  durationSec: finalDurationSec,
700
758
  triggeredEvent: parsedIntent.triggerEvent || undefined,
701
759
  stateUpdate: parsedIntent.stateUpdate,
702
760
  userAnalysis: parsedIntent.userAnalysis,
703
761
  isEndTurn: parsedIntent.isEndTurn,
762
+ persistedDynamicContext,
704
763
  };
705
764
  }
706
765
  catch (error) {
@@ -909,9 +968,11 @@ You MUST output ONLY a valid JSON object matching exactly this structure:
909
968
  reason: parsedIntent.skipReason || "Character decided to skip proactive message based on mood/stage."
910
969
  };
911
970
  }
912
- // Update Remote state if needed
971
+ // Update Remote state if needed (capture promise for authoritative
972
+ // server snapshot — see notes in interact()).
973
+ let persistedStatePromise = Promise.resolve(null);
913
974
  if (parsedIntent.stateUpdate) {
914
- this._updateDynamicContextInternal(parsedIntent.stateUpdate).catch(e => console.error(e));
975
+ persistedStatePromise = this._updateDynamicContextInternal(parsedIntent.stateUpdate);
915
976
  }
916
977
  const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
917
978
  parsedIntent.textResponse.trim().length > 0
@@ -929,21 +990,26 @@ You MUST output ONLY a valid JSON object matching exactly this structure:
929
990
  }
930
991
  // Handle Optional Media (Image only for proactive to save compute normally, but you can extend)
931
992
  let finalImageUrl = undefined;
993
+ let finalImageMediaId = undefined;
932
994
  if (parsedIntent.imageParams) {
933
995
  try {
934
996
  const res = await this.generatePrimitive("image", parsedIntent.imageParams);
935
997
  finalImageUrl = res.image_url;
998
+ finalImageMediaId = res.id;
936
999
  }
937
1000
  catch (e) {
938
1001
  console.error("[CyberSoulClient] Proactive Image generation failed:", e);
939
1002
  }
940
1003
  }
1004
+ const persistedDynamicContext = (await persistedStatePromise) ?? undefined;
941
1005
  return {
942
1006
  status: "success",
943
1007
  textResponse: parsedIntent.textResponse,
944
1008
  actionText: parsedIntent.actionText,
945
1009
  imageUrl: finalImageUrl,
946
- stateUpdate: parsedIntent.stateUpdate
1010
+ imageMediaId: finalImageMediaId,
1011
+ stateUpdate: parsedIntent.stateUpdate,
1012
+ persistedDynamicContext,
947
1013
  };
948
1014
  }
949
1015
  catch (error) {
@@ -984,6 +1050,7 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
984
1050
  const res = await this.generatePrimitive("image", imageParams);
985
1051
  return {
986
1052
  imageUrl: res.image_url,
1053
+ imageMediaId: res.id,
987
1054
  };
988
1055
  }
989
1056
  /**
@@ -1022,6 +1089,7 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1022
1089
  });
1023
1090
  return {
1024
1091
  audioUrl: res.audio_url,
1092
+ audioMediaId: res.id,
1025
1093
  durationSec: res.duration_sec,
1026
1094
  };
1027
1095
  }
@@ -1031,8 +1099,30 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1031
1099
  async getState() {
1032
1100
  return this.fetchRemoteState();
1033
1101
  }
1102
+ /**
1103
+ * List the public LLM models the backend currently supports, including the
1104
+ * `customConfigDefinition` schema for each model's `customSettings`.
1105
+ *
1106
+ * Use this to discover valid `provider` / `model` strings and the keys
1107
+ * each model accepts via `llmConfig.customSettings`.
1108
+ */
1109
+ async listSupportedLLMs() {
1110
+ const res = await this.apiFetch("/api/v1/cyber-soul/llm-models");
1111
+ if (!res.ok) {
1112
+ throw new Error(`Failed to list supported LLMs: ${res.status}`);
1113
+ }
1114
+ const body = (await res.json());
1115
+ if (Array.isArray(body))
1116
+ return body;
1117
+ if (body && typeof body === "object" && Array.isArray(body.data)) {
1118
+ return body.data;
1119
+ }
1120
+ throw new Error("Unexpected response shape from /llm-models");
1121
+ }
1034
1122
  /**
1035
1123
  * Updates the character's relationship temperature or mood.
1124
+ * Returns the server-authoritative post-write `{ temperature, relationshipStage }`
1125
+ * snapshot (or `null` if there was nothing to send / the request failed).
1036
1126
  */
1037
1127
  async updateDynamicContext(stateUpdate, userAnalysis) {
1038
1128
  return this._updateDynamicContextInternal(stateUpdate, userAnalysis);
package/dist/types.d.ts CHANGED
@@ -41,6 +41,18 @@ export interface InteractMetadata {
41
41
  triggerEvent?: DispatcherIntent["triggerEvent"];
42
42
  likePreviousPicture?: boolean;
43
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
+ }
44
56
  export interface ProactiveParams {
45
57
  history?: HistoryEntry[];
46
58
  maxUnreplied?: number;
@@ -54,8 +66,12 @@ export interface ProactiveResponse {
54
66
  textResponse?: string;
55
67
  actionText?: string;
56
68
  imageUrl?: string;
69
+ imageMediaId?: string;
57
70
  audioUrl?: string;
71
+ audioMediaId?: string;
58
72
  stateUpdate?: DispatcherIntent["stateUpdate"];
73
+ /** Server-authoritative post-write snapshot (see PersistedDynamicContext). */
74
+ persistedDynamicContext?: PersistedDynamicContext;
59
75
  error?: string;
60
76
  }
61
77
  export interface InteractParams {
@@ -102,7 +118,9 @@ export interface InteractResponse {
102
118
  textResponse: string;
103
119
  actionText?: string;
104
120
  imageUrl?: string;
121
+ imageMediaId?: string;
105
122
  audioUrl?: string;
123
+ audioMediaId?: string;
106
124
  likePreviousPicture?: boolean;
107
125
  durationSec?: number;
108
126
  triggeredEvent?: {
@@ -114,6 +132,8 @@ export interface InteractResponse {
114
132
  stateUpdate?: DispatcherIntent["stateUpdate"];
115
133
  userAnalysis?: DispatcherIntent["userAnalysis"];
116
134
  isEndTurn?: boolean;
135
+ /** Server-authoritative post-write snapshot (see PersistedDynamicContext). */
136
+ persistedDynamicContext?: PersistedDynamicContext;
117
137
  error?: string;
118
138
  }
119
139
  export interface OngoingSceneState {
@@ -287,6 +307,20 @@ export interface IVoiceModel {
287
307
  isPublic: boolean;
288
308
  pointsPerGeneration: number;
289
309
  }
310
+ /**
311
+ * Public LLM model entry returned by `GET /api/v1/cyber-soul/llm-models`.
312
+ *
313
+ * - `provider` is the value to pass as `llmConfig.provider`.
314
+ * - `name` is the value to pass as `llmConfig.model`.
315
+ * - `customConfigDefinition` describes the keys (and their constraints) that
316
+ * the model accepts via `llmConfig.customSettings`.
317
+ */
318
+ export interface SupportedLLMModel {
319
+ id: string;
320
+ name: string;
321
+ provider: string;
322
+ customConfigDefinition: IModelCustomConfigField[];
323
+ }
290
324
  export interface ICharacterProfile {
291
325
  id: string;
292
326
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.4",
3
+ "version": "1.4.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",