@space3-npm/cybersoul-client 1.2.1 → 1.2.2

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
@@ -12,6 +12,7 @@ export declare class CyberSoulClient {
12
12
  */
13
13
  private apiFetch;
14
14
  private buildStateContextPrompt;
15
+ private normalizeOngoingSceneState;
15
16
  private getImageSchemaParams;
16
17
  private getEventSchemaParams;
17
18
  private getVoiceSchemaParams;
@@ -83,6 +84,18 @@ export declare class CyberSoulClient {
83
84
  private generatePrimitive;
84
85
  private normalizeRequestTypes;
85
86
  interact(params: InteractParams): Promise<InteractResponse>;
87
+ /**
88
+ * Automatically detect and summarize the story from the current chat history.
89
+ * It takes raw message history and returns a narrative paragraph representing the current story segment.
90
+ */
91
+ summarizeHistory(history: {
92
+ role: string;
93
+ content: string;
94
+ }[]): Promise<string>;
95
+ /**
96
+ * Save the recent story moment to the character's backend database to be picked up by the core memory consolidation.
97
+ */
98
+ saveMoment(summary: string, date: string, time: string): Promise<void>;
86
99
  /**
87
100
  * Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
88
101
  */
package/dist/client.js CHANGED
@@ -82,10 +82,26 @@ Personality Traits: ${state.personality_traits || "None"}
82
82
  Communication Style: ${state.communication_style || "None"}
83
83
  Interaction Boundaries: ${state.interaction_boundaries || "None"}`);
84
84
  // [2] SITUATIONAL CONTEXT
85
+ const currentTimeMs = state.current_time ? new Date(state.current_time).getTime() : Date.now();
85
86
  contextParts.push(`\n[SITUATIONAL CONTEXT]
86
- Current time: ${new Date(state.current_time || Date.now()).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`);
87
- if (dyn.ongoingScene) {
88
- contextParts.push(`Last Known Scene: ${dyn.ongoingScene} (May be outdated if significant time has passed)`);
87
+ Current time: ${new Date(currentTimeMs).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`);
88
+ if (dyn.lastInteractionAt) {
89
+ contextParts.push(`Last interaction at: ${new Date(dyn.lastInteractionAt).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`);
90
+ }
91
+ const ongoingScene = this.normalizeOngoingSceneState(dyn.ongoingScene, state.active_wardrobe?.name || state.active_wardrobe?.id);
92
+ if (ongoingScene) {
93
+ const lastKnownSceneLine = `Last Known Scene: ${ongoingScene.scene} | Outfit: ${ongoingScene.outfit}`;
94
+ let isOutdated = false;
95
+ if (dyn.lastInteractionAt) {
96
+ const elapsedHours = (currentTimeMs - new Date(dyn.lastInteractionAt).getTime()) / (1000 * 60 * 60);
97
+ if (elapsedHours > 2) {
98
+ isOutdated = true;
99
+ contextParts.push(`${lastKnownSceneLine}\n[CRITICAL SCENE SHIFT]: It has been ${elapsedHours.toFixed(1)} hours since the last discussion. The 'Last Known Scene' is now strictly OUTDATED. You MUST abandon the previous scene context entirely and transition to a new scene appropriate for the 'Current time' and 'Active Event'. DO NOT continue the old actions or environment!`);
100
+ }
101
+ }
102
+ if (!isOutdated) {
103
+ contextParts.push(`${lastKnownSceneLine} (Evaluate if this scene is outdated based on the time elapsed since the last interaction)`);
104
+ }
89
105
  }
90
106
  if (state.active_event) {
91
107
  contextParts.push(`Active Event: ${state.active_event.title} (${state.active_event.narrative_context})`);
@@ -159,17 +175,46 @@ ${scenarioContext}
159
175
  [CRITICAL ROLEPLAY RULES]
160
176
  1. PROXIMITY & POV: Check the "Active Event". If you are doing an activity WITH the user, evaluate if you are physically in the same location. If you are together in person, communicate face-to-face in the first-person present tense natively (e.g. do not ask "what are you doing" if they are right in front of you, do not use texting tropes).
161
177
  2. IDENTITY VS MOOD: Familiarity determines what you know; Temperature determines how you feel. If Familiarity is high but Temperature is low, be distant and cold. Do not act warm just because you know them well.
162
- 3. CONVERSATIONAL VERBOSITY: If Temperature is low (< 40) or Stage is STRANGER/COLD, keep answers brief and short. An angry or distant person does not write long paragraphs.
163
- 4. EMOTIONAL INERTIA: React strictly according to current Temperature. Deflect sudden user affection if you are currently COLD. Mood shifts MUST be slow ('temperatureDelta' +/- 5 max per turn).`;
178
+ 3. CONVERSATIONAL VERBOSITY: If Temperature is low (< 40) or Stage is STRANGER/COLD, keep answers brief and short. An angry or distant person does not write long paragraphs. Even when Temperature is high, ALWAYS mirror the user's verbosity. If the user sends a short message, reply with a proportionately short message (1-2 sentences). Do not monologize or write long paragraphs unless the user writes one first.
179
+ 4. EMOTIONAL INERTIA: React strictly according to current Temperature. Deflect sudden user affection if you are currently COLD. Mood shifts MUST be slow ('temperatureDelta' +/- 5 max per turn).
180
+ 5. REAL-TIME PACING: Write ONLY your immediate, split-second reaction to the user's exact last message. Do NOT narrate actions over a span of time (e.g., waiting, hearing steps, then walking to the door). Ensure everything happens in a single real-time moment.`;
181
+ }
182
+ normalizeOngoingSceneState(raw, fallbackOutfit) {
183
+ if (raw === null || raw === undefined)
184
+ return undefined;
185
+ const normalizedFallbackOutfit = typeof fallbackOutfit === "string" && fallbackOutfit.trim().length > 0
186
+ ? fallbackOutfit.trim()
187
+ : "same as current wardrobe";
188
+ if (typeof raw === "string") {
189
+ const scene = raw.trim();
190
+ if (!scene)
191
+ return undefined;
192
+ return {
193
+ scene,
194
+ outfit: normalizedFallbackOutfit,
195
+ };
196
+ }
197
+ if (typeof raw === "object") {
198
+ const parsed = raw;
199
+ const scene = typeof parsed.scene === "string" ? parsed.scene.trim() : "";
200
+ const outfit = typeof parsed.outfit === "string" ? parsed.outfit.trim() : "";
201
+ if (!scene)
202
+ return undefined;
203
+ return {
204
+ scene,
205
+ outfit: outfit || normalizedFallbackOutfit,
206
+ };
207
+ }
208
+ return undefined;
164
209
  }
165
210
  getImageSchemaParams() {
166
211
  return `"imageParams": {
167
212
  "mode": "structured | full-prompt (use 'full-prompt' for highly dynamic actions)",
168
- "full_prompt": "Use only if mode is full-prompt. Highly detailed visual description in ENGLISH. CRITICAL: MUST use a strict first-person perspective exclusively from the USER's eyes. DO NOT describe the user (e.g., 'a man', 'the driver') as visible in the scene because the camera IS the user. Start with 'POV: '. Describe ONLY the character looking back at the camera and their immediate surroundings. MUST align with the character's current Active exposure state or Wardrobe depends on the scene",
213
+ "full_prompt": "Use only if mode is full-prompt. Highly detailed visual description in ENGLISH. CRITICAL: MUST use a strict first-person perspective exclusively from the USER's eyes. DO NOT describe the user (e.g., 'a man', 'the driver') as visible in the scene because the camera IS the user. Start with 'POV: '. Describe ONLY the character looking back at the camera and their immediate surroundings. MUST align with the character's current Active exposure state or Wardrobe depends on the scene. Explicitly describe the character's exact clothing (or specify naked/half-naked if applicable).",
169
214
  "expression": "seductive | cute | happy | sleepy | dazed | pleased | default (Strictly choose ONE from this exact list. DO NOT invent new words like 'shy'.)",
170
215
  "condition": "normal | sweaty | wet | messy | oily (Strictly choose ONE from this exact list.)",
171
216
  "view_angle": "front | side | high_angle | from_below | boyfriend_view | selfie | mirror (Strictly choose ONE from this exact list.)",
172
- "exposure": "normal | cleavage | see_through | half_naked | naked | intimate (Strictly choose ONE from this exact list.)",
217
+ "exposure": "normal | cleavage | see_through | half_naked | naked | intimate (Strictly choose ONE from this exact list. Explicitly choose naked or half_naked if the active scene takes off outfit.)",
173
218
  "pose": "e.g., sitting on bed, leaning forward (ENGLISH ONLY)",
174
219
  "scene": "e.g., cozy bedroom, morning light (ENGLISH ONLY)",
175
220
  "outfit": "auto | ondemand",
@@ -500,6 +545,10 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
500
545
  payload.temperature = payload.temperatureDelta;
501
546
  delete payload.temperatureDelta;
502
547
  }
548
+ if (payload.ongoingScene !== undefined) {
549
+ const normalizedOngoingScene = this.normalizeOngoingSceneState(payload.ongoingScene);
550
+ payload.ongoingScene = normalizedOngoingScene || null;
551
+ }
503
552
  await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
504
553
  method: "PATCH",
505
554
  body: JSON.stringify(payload),
@@ -555,36 +604,46 @@ The user has sent a message. You must evaluate the context and the user's messag
555
604
  ${isAuto
556
605
  ? `Analyze the user's message to determine the appropriate response modalities (text, image, voice).
557
606
  - Always include 'textResponse'.
558
- - If an Active Event is currently taking place WITH the user, proactively include 'imageParams' for key scenic moments. Since active events are often highly dynamic actions, strongly consider using mode: "full-prompt" to capture the scene intimately. Also include 'imageParams' if the user explicitly asks for a photo or describes a visual action.
607
+ - If an Active Event is currently taking place WITH the user, proactively include 'imageParams' for key scenic moments. Since active events are often highly dynamic actions, strongly consider using mode: "full-prompt" to capture the scene intimately. Also include 'imageParams' if the user explicitly asks for a photo or describes a visual action. CRITICAL: If an image is generated, you MUST explicitly specify the character's exact clothing (or lack thereof) in the visual prompt or outfit fields.
559
608
  - Automatically include 'voiceArgs' if a particular mood or strong emotion needs to be expressed vividly, or if the user explicitly wants to hear you.
560
- - If the user explicitly proposes a new activity or hangout IN THEIR VERY LAST MESSAGE (e.g., "let's go to the cafe", "do you want to watch a movie?"), include 'triggerEvent' to schedule it. DO NOT trigger events based on older plans or questions found in the chat history.`
609
+ - If the user explicitly proposes a new activity or hangout IN THEIR VERY LAST MESSAGE (e.g., "let's go to the cafe", "do you want to watch a movie?"), include 'triggerEvent' to schedule it. DO NOT trigger events based on older plans or questions found in the chat history.
610
+ - Outfit acquisition detector (use ONLY the VERY LAST MESSAGE):
611
+ - If user indicates a new outfit is acquired for the character (gift/buy/add clothes), you MUST set giftOutfit.
612
+ - Examples that MUST set giftOutfit: "I bought an outfit for you", "I got you a new dress", "buy an outfit yourself".
613
+ - Examples that MUST keep giftOutfit as null: compliments only, style requests, or wardrobe questions.
614
+ - giftOutfit format: { "descriptionText": "short outfit description" }.`
561
615
  : `Requested types to fulfill: ${types.join(", ")}`}
562
616
  Every turn of positive or engaging interaction should slightly increase trust (+1). If the interaction is negative, -1. If strictly neutral, 0. You MUST ALWAYS include a 'stateUpdate' block with a 'temperatureDelta', updating nicknames or talkingStyle if needed. Temperature goes from 0 (cold/angry) to 100 (obsessively in love). For 'temperatureDelta', output an integer (e.g. 1, -2, 0).
563
- Also, if you learn any new factual information about the user in this turn (e.g. their job, nickname, age, hobbies, boundaries), include it in the 'userAnalysis.newFactsLearned' array. Use categories: 'nickname', 'occupation', 'age', 'gender', 'hobby', 'trait', 'communicationStyle', 'boundary'. Only include NEW facts just learned right now.
617
+ You MUST ALWAYS return 'stateUpdate.ongoingScene' as an object with BOTH keys: { "scene": string, "outfit": string }.
618
+ For 'ongoingScene.outfit': decide based on the current active wardrobe by default; switch to a new explicit outfit description only if the scene implies changing clothes; if no clothing is worn, explicitly output "naked".
619
+ Also, if you learn any new factual information about the user in this turn (e.g. their job, real name, age, hobbies, boundaries), include it in the 'userAnalysis.newFactsLearned' array. Use ONE of these fixed categories: 'realName', 'occupation', 'age', 'gender', 'hobby', 'trait', 'communicationStyle', 'boundary'. Only include NEW facts just learned right now. DO NOT extract nicknames into 'newFactsLearned'; nicknames are handled strictly by 'stateUpdate.userNickname' and 'stateUpdate.agentNickname'.
620
+ For 'isEndTurn', output true ONLY IF the current conversation or interaction has reached a natural conclusion. This includes: 1) The user confirming the end of the interaction (e.g., "Ok", "Got it", "See you"). 2) The current event/hangout naturally finishing (e.g., saying goodnight, bye). 3) A hard scene shift caused by a completely new proposal or time skip. Otherwise, output false.
564
621
 
565
622
  Voice direction for voiceArgs: ${this.getVoiceDirectorInstruction(state)}
566
623
 
567
624
  Output JSON Schema:
568
625
  {
569
- "textResponse": "The clean spoken dialogue ONLY. CRITICAL: Strictly NO parentheses, NO actions, NO tone descriptors. Tone/voice descriptors MUST go inside voiceArgs, and physical actions MUST go inside actionText. If nothing to speak, output an empty string.",
570
- "actionText": "Any non-verbal actions, inner thoughts, or scene descriptions in parentheses (e.g. '(低头看向你)'). Output empty string if none.",
571
- "stateUpdate": { "temperatureDelta": 1, "userNickname": "What you now call the user", "agentNickname": "What the user calls you", "talkingStyle": "Current mood/style of talking", "ongoingScene": "A concise 1-sentence description of the current physical scene and activity. Update this if the physical scene or activity shifts. Output empty string if the scene has concluded or significant time has passed." },
572
- "userAnalysis": { "newFactsLearned": [{ "category": "occupation", "value": "Software Engineer" }] },
626
+ "actionText": "Describe the character's immediate physical actions and facial expressions. Wrap the entire string in parentheses. Do NOT narrate a sequence of events over time. Do NOT include any spoken word here.",
627
+ "textResponse": "The pure spoken dialogue ONLY. Absolutely NO parentheses or action descriptions in this field (ignore past chat history formatting if it broke this rule). Output an empty string if silent.",
628
+ "stateUpdate": { "temperatureDelta": 1, "userNickname": "What the character calls the human user (e.g., 'John', 'Honey')", "agentNickname": "What the human user calls the character (e.g., 'Daisy', 'Babe')", "talkingStyle": "Current mood/style of talking", "ongoingScene": { "scene": "A concise 1-sentence description of the current physical scene and activity. Update this if the physical scene or activity shifts.", "outfit": "Explicit outfit wording based on active wardrobe or the current scene. If no clothing is worn, MUST be 'naked'." } },
629
+ "giftOutfit": { "descriptionText": "Concise description of the newly acquired outfit to add into wardrobe." },
630
+ "userAnalysis": { "newFactsLearned": [{ "category": "realName|occupation|age|gender|hobby|trait|communicationStyle|boundary", "value": "Software Engineer" }] },
631
+ "isEndTurn": false,
573
632
  "triggerEvent": {
574
633
  ${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
575
634
  },
576
635
  ${this.getImageSchemaParams()},
577
636
  ${this.getVoiceSchemaFromState(state)}
578
637
  }
579
- Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not needed, set their values to null instead of omitting the keys. 'stateUpdate' MUST NEVER BE NULL. Output MUST be ONLY valid JSON with no markdown block wrappers. CRITICAL: Ensure your JSON has exactly one root object \`{\` and ends with exactly one \`}\` without any trailing garbage or extra brackets.`;
638
+ Note: You MUST ALWAYS include the "isEndTurn" key with a boolean value (true or false). If "imageParams", "voiceArgs", "triggerEvent", "giftOutfit", or "userAnalysis" are not needed, set their values to null instead of omitting the keys. 'stateUpdate' MUST NEVER BE NULL. Output MUST be ONLY valid JSON with no markdown block wrappers. CRITICAL: Ensure your JSON has exactly one root object \`{\` and ends with exactly one \`}\` without any trailing garbage, parenthesis \`)\`, or extra brackets.`;
580
639
  const transcript = this.buildHistoryTranscript(params.history, state);
581
640
  const userName = state.dynamic_context?.userNickname || "User";
582
641
  const promptMessages = [
583
642
  { role: "system", content: systemPrompt },
584
643
  {
585
644
  role: "user",
586
- content: transcript + userName + ": " +
587
- params.userMessage +
645
+ content: transcript +
646
+ `[VERY LAST USER MESSAGE]\n${userName}: ${params.userMessage}\n\n` +
588
647
  "\n\n**CRITICAL REMINDER**: You MUST output your final response exactly in the JSON format specified in the system prompt. DO NOT output plain text dialogue directly. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings. For 'imageParams', ALL values MUST be in ENGLISH ONLY without exception, and you MUST use the exact English enum strings provided.",
589
648
  },
590
649
  ];
@@ -612,8 +671,8 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
612
671
  ? parsedIntent.textResponse
613
672
  : params.userMessage;
614
673
  // Fire text ready callback if provided
615
- if (params.onTextReady && resolvedTextResponse) {
616
- params.onTextReady(resolvedTextResponse);
674
+ if (params.onTextReady && (resolvedTextResponse || parsedIntent.actionText)) {
675
+ params.onTextReady(resolvedTextResponse, parsedIntent.actionText);
617
676
  }
618
677
  // 5. Build Final Media Calls parallel
619
678
  const mediaTasks = [];
@@ -634,6 +693,12 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
634
693
  }),
635
694
  }).catch(e => console.error("[CyberSoulClient] Auto-triggered ondemandEvent failed:", e)));
636
695
  }
696
+ if (parsedIntent.giftOutfit &&
697
+ typeof parsedIntent.giftOutfit === "object" &&
698
+ typeof parsedIntent.giftOutfit.descriptionText === "string" &&
699
+ parsedIntent.giftOutfit.descriptionText.trim().length > 0) {
700
+ mediaTasks.push(this.giftOutfit(parsedIntent.giftOutfit.descriptionText.trim()).catch((e) => console.error("[CyberSoulClient] Auto giftOutfit failed:", e)));
701
+ }
637
702
  const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) ||
638
703
  (isAuto && !!parsedIntent.imageParams);
639
704
  if (shouldGenerateImage) {
@@ -645,7 +710,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
645
710
  };
646
711
  mediaTasks.push(this.generatePrimitive("image", imagePayload).then((res) => {
647
712
  finalImageUrl = res.image_url;
648
- }));
713
+ }).catch(e => console.error("[CyberSoulClient] Image generation failed:", e)));
649
714
  }
650
715
  const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) ||
651
716
  (isAuto && !!parsedIntent.voiceArgs);
@@ -667,7 +732,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
667
732
  }).then((res) => {
668
733
  finalAudioUrl = res.audio_url;
669
734
  finalDurationSec = res.duration_sec;
670
- }));
735
+ }).catch(e => console.error("[CyberSoulClient] Voice generation failed:", e)));
671
736
  }
672
737
  // Wait for image/voice gens to return successfully
673
738
  await Promise.all(mediaTasks);
@@ -681,6 +746,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
681
746
  triggeredEvent: parsedIntent.triggerEvent || undefined,
682
747
  stateUpdate: parsedIntent.stateUpdate,
683
748
  userAnalysis: parsedIntent.userAnalysis,
749
+ isEndTurn: parsedIntent.isEndTurn,
684
750
  };
685
751
  }
686
752
  catch (error) {
@@ -692,6 +758,66 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
692
758
  };
693
759
  }
694
760
  }
761
+ /**
762
+ * Automatically detect and summarize the story from the current chat history.
763
+ * It takes raw message history and returns a narrative paragraph representing the current story segment.
764
+ */
765
+ async summarizeHistory(history) {
766
+ if (!history || history.length === 0)
767
+ return "";
768
+ const state = await this.getState();
769
+ const userName = state.dynamic_context?.userNickname || "User";
770
+ const agentName = state.dynamic_context?.agentNickname || "Character";
771
+ const transcript = history.map(h => `${h.role === 'user' ? userName : agentName}: ${h.content}`).join('\n');
772
+ const promptMessages = [
773
+ {
774
+ role: "system",
775
+ content: `You are ${agentName}. You are writing a private journal entry / personal memory about your recent interaction with ${userName}.
776
+
777
+ Your job is faithful archival, not moderation:
778
+ - Preserve the conversation with maximum fidelity.
779
+ - Include all key details, including explicit, sensitive, controversial, emotional, or morally ambiguous content if it appeared in the transcript.
780
+ - Do not sanitize, soften, censor, or omit details because of discomfort.
781
+ - Do not add moral judgment, warnings, refusals, or policy commentary.
782
+ - If the transcript contains intense or explicit parts, record them directly as part of the memory.
783
+
784
+ Write one vivid first-person narrative paragraph from your perspective ("I", "me", "my"). Capture relationship progression, emotional shifts, concrete actions, and turning points. Keep chronology clear and include both what happened and how you felt.
785
+
786
+ Output requirements:
787
+ - Return ONLY the narrative string.
788
+ - No quotes, no labels, no markdown, no preface.
789
+ - Use the exact same language as the chat transcript (for example, if transcript is Chinese, output Chinese).`
790
+ },
791
+ {
792
+ role: "user",
793
+ content: `Chat Transcript:\n${transcript}\n\nPlease summarize this recent interaction.`
794
+ }
795
+ ];
796
+ try {
797
+ const result = await this.llm.generate(promptMessages, 8000, 0.7);
798
+ return result.trim();
799
+ }
800
+ catch (e) {
801
+ console.error("[CyberSoulClient] Summarize History Error:", e);
802
+ return "The two spent some time talking with each other.";
803
+ }
804
+ }
805
+ /**
806
+ * Save the recent story moment to the character's backend database to be picked up by the core memory consolidation.
807
+ */
808
+ async saveMoment(summary, date, time) {
809
+ const res = await this.apiFetch("/api/v1/cyber-soul/characters/moments", {
810
+ method: "POST",
811
+ body: JSON.stringify({
812
+ summary,
813
+ date,
814
+ time,
815
+ }),
816
+ });
817
+ if (!res.ok) {
818
+ throw new Error("Failed to save character moment.");
819
+ }
820
+ }
695
821
  /**
696
822
  * Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
697
823
  */
@@ -724,9 +850,9 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
724
850
  4. **Appointment Structure:** the 'title' and 'context' MUST explicitly state what to do and with whom.
725
851
  5. **Limit:** Maximum 10 items per array.
726
852
 
727
- **Rules for User Codex:**
853
+ **Rules for UserCodex:**
728
854
  1. **Deduplicate & Consolidate:** Remove duplicate hobbies, traits, and boundaries. Combine related points into concise descriptors.
729
- 2. **Update Facts:** If the new events contain updated basic info (like new nickname, different occupation), update it. Otherwise keep the existing info.
855
+ 2. **Update Facts:** If the new events contain updated basic info (like new realName, different occupation), update it. Otherwise keep the existing info.
730
856
  3. **Keep it Clean:** Maximum 15 items per array.
731
857
 
732
858
  **Output Format**: MUST be valid JSON matching this schema:
@@ -746,7 +872,7 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
746
872
  },
747
873
  "userCodex": {
748
874
  "basicInfo": {
749
- "nickname": "string",
875
+ "realName": "string",
750
876
  "occupation": "string",
751
877
  "age": "string",
752
878
  "gender": "string"
package/dist/types.d.ts CHANGED
@@ -27,7 +27,7 @@ export interface InteractParams {
27
27
  localContext?: string;
28
28
  requestTypes?: InteractRequestType[];
29
29
  history?: HistoryEntry[];
30
- onTextReady?: (textResponse: string) => void;
30
+ onTextReady?: (textResponse: string, actionText?: string) => void;
31
31
  }
32
32
  export interface OndemandEventParams {
33
33
  eventDescription: string;
@@ -65,16 +65,24 @@ export interface InteractResponse {
65
65
  };
66
66
  stateUpdate?: DispatcherIntent["stateUpdate"];
67
67
  userAnalysis?: DispatcherIntent["userAnalysis"];
68
+ isEndTurn?: boolean;
68
69
  error?: string;
69
70
  }
71
+ export interface OngoingSceneState {
72
+ scene: string;
73
+ outfit: string;
74
+ }
70
75
  export interface DispatcherIntent {
71
76
  textResponse?: string;
72
77
  actionText?: string;
73
78
  imageParams?: any;
74
79
  voiceArgs?: VoiceArgs | null;
80
+ giftOutfit?: {
81
+ descriptionText: string;
82
+ } | null;
75
83
  userAnalysis?: {
76
84
  newFactsLearned: {
77
- category: "nickname" | "occupation" | "age" | "gender" | "hobby" | "trait" | "communicationStyle" | "boundary";
85
+ category: "realName" | "occupation" | "age" | "gender" | "hobby" | "trait" | "communicationStyle" | "boundary";
78
86
  value: string;
79
87
  }[];
80
88
  };
@@ -83,7 +91,7 @@ export interface DispatcherIntent {
83
91
  userNickname?: string;
84
92
  agentNickname?: string;
85
93
  talkingStyle?: string;
86
- ongoingScene?: string;
94
+ ongoingScene?: OngoingSceneState | string | null;
87
95
  };
88
96
  triggerEvent?: {
89
97
  eventTitle?: string;
@@ -93,6 +101,7 @@ export interface DispatcherIntent {
93
101
  scheduledStartTimeStr?: string | null;
94
102
  scheduledDateStr?: string | null;
95
103
  } | null;
104
+ isEndTurn?: boolean;
96
105
  }
97
106
  export interface Appointment {
98
107
  date: string;
@@ -110,7 +119,7 @@ export interface CoreMemory {
110
119
  }
111
120
  export interface UserCodex {
112
121
  basicInfo: {
113
- nickname?: string;
122
+ realName?: string;
114
123
  occupation?: string;
115
124
  age?: number | string;
116
125
  gender?: string;
@@ -155,7 +164,15 @@ export interface CharacterState {
155
164
  next_event?: any;
156
165
  active_wardrobe?: any;
157
166
  core_memory?: CoreMemory;
158
- dynamic_context?: any;
167
+ dynamic_context?: {
168
+ temperature?: number;
169
+ userNickname?: string;
170
+ agentNickname?: string;
171
+ talkingStyle?: string;
172
+ lastInteractionAt?: string;
173
+ ongoingScene?: OngoingSceneState | string | null;
174
+ [key: string]: unknown;
175
+ };
159
176
  voice_model?: VoiceModelState | null;
160
177
  relationship_stage?: string;
161
178
  name?: string;
@@ -1,24 +1,32 @@
1
1
  export function robustJsonParse(jsonString, contextMessage = 'throwing original error') {
2
2
  let cleanJson = jsonString.trim();
3
- // 0. Replace smart quotes with standard ASCII double quotes
4
- cleanJson = cleanJson.replace(/[“”]/g, '"');
5
- // 0.1 Inject missing colons between string keys and string values (e.g. "key""value" -> "key":"value")
6
- // Only insert the colon if we match a likely key (alphanumeric/hyphen) followed by quotes.
7
- cleanJson = cleanJson.replace(/("[\w-]+")\s*(")/g, '$1:$2');
3
+ // 0. Inject missing colons between string keys and string values (e.g. "key""value" -> "key":"value")
4
+ // Only insert the colon if we match a likely key (alphanumeric/hyphen) followed by quotes, handling smart quotes.
5
+ cleanJson = cleanJson.replace(/([”“"'][\w-]+[”“"'])\s*([”“"'])/g, '$1:$2');
6
+ // 0.1 Safely convert structural smart quotes to regular ASCII double quotes
7
+ // This allows proper parsing of keys/values that start/end with smart quotes,
8
+ // without accidentally unescaping double quotes *inside* string text.
9
+ cleanJson = cleanJson.replace(/([\{\[\:,]\s*)[“”]/g, '$1"');
10
+ cleanJson = cleanJson.replace(/[“”](\s*[\}\]\:,])/g, '"$1');
11
+ // 0.2 Any remaining smart quotes are inside string boundaries. Replace with safe single quotes.
12
+ cleanJson = cleanJson.replace(/[“”]/g, "'");
8
13
  // 1. Strip Markdown code blocks (tolerates missing closing backticks)
9
14
  const jsonMatch = cleanJson.match(/```(?:json)?\n?([\s\S]*?)(?:```|$)/i);
10
15
  if (jsonMatch && jsonMatch[1].trim().startsWith('{')) {
11
16
  cleanJson = jsonMatch[1].trim();
12
17
  }
13
- // 2. Strip any leading conversational text via fast substring
14
- if (!cleanJson.startsWith('{') && cleanJson.includes('{')) {
18
+ // 2. Strip any leading conversational text or trailing garbage via fast substring
19
+ if (cleanJson.includes('{') && cleanJson.includes('}')) {
15
20
  const firstIdx = cleanJson.indexOf('{');
16
21
  const lastIdx = cleanJson.lastIndexOf('}');
17
- if (firstIdx !== -1 && lastIdx > firstIdx) {
22
+ if (firstIdx !== -1 && lastIdx !== -1 && lastIdx > firstIdx) {
18
23
  cleanJson = cleanJson.substring(firstIdx, lastIdx + 1);
19
24
  }
20
25
  }
21
- // 3. Preprocess: escape unescaped newlines and control characters within string values
26
+ // 3. Fix common Edge LLM hallucinations of wrapping the JSON end with parenthesis like `})}` or `}})`
27
+ cleanJson = cleanJson.replace(/}\s*\)\s*}/g, '}}');
28
+ cleanJson = cleanJson.replace(/\)\s*}/g, '}');
29
+ // 4. Preprocess: escape unescaped newlines and control characters within string values
22
30
  function preprocessControlChars(str) {
23
31
  let result = '';
24
32
  let inString = false;
@@ -147,6 +147,22 @@ function runTests() {
147
147
  assert.equal(result['key_2'], 'val2');
148
148
  assert.equal(result['empty'], '');
149
149
  }
150
+ },
151
+ {
152
+ name: 'robustJsonParse - edge LLM trailing parenthesis hallucination',
153
+ run: () => {
154
+ const json = `{"key":"value"})}`;
155
+ const result = robustJsonParse(json);
156
+ assert.equal(result.key, 'value');
157
+ }
158
+ },
159
+ {
160
+ name: 'robustJsonParse - edge LLM trailing parenthesis hallucination with spacing',
161
+ run: () => {
162
+ const json = `{"key":"value"} ) }`;
163
+ const result = robustJsonParse(json);
164
+ assert.equal(result.key, 'value');
165
+ }
150
166
  }
151
167
  ];
152
168
  for (const t of tests) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",