@space3-npm/cybersoul-client 1.2.6 → 1.2.8

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, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory, UserCodex } from "./types.js";
1
+ import { CyberSoulClientConfig, InteractParams, ProactiveParams, ProactiveResponse, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory, UserCodex } from "./types.js";
2
2
  export declare class CyberSoulClient {
3
3
  private config;
4
4
  private llm;
@@ -19,6 +19,9 @@ export declare class CyberSoulClient {
19
19
  private buildStateContextPrompt;
20
20
  private normalizeOngoingSceneState;
21
21
  private getImageSchemaParams;
22
+ private getOutfitSelectionPrompt;
23
+ private getTriggerEventPolicyPrompt;
24
+ private getOutfitAcquisitionPolicyPrompt;
22
25
  private getEventSchemaParams;
23
26
  private getVoiceSchemaParams;
24
27
  private buildVoiceSchemaFromDynamicParams;
@@ -44,6 +47,11 @@ export declare class CyberSoulClient {
44
47
  * Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
45
48
  */
46
49
  ondemandEvent(params: OndemandEventParams): Promise<OndemandEventResponse>;
50
+ /**
51
+ * Generates a proactive message when the user hasn't responded.
52
+ * Safely prevents spamming, and adjusts its approach based on relationship dynamics.
53
+ */
54
+ proactiveInteract(params: ProactiveParams): Promise<ProactiveResponse>;
47
55
  /**
48
56
  * Manually generate an image of the character outside of chat flow.
49
57
  */
package/dist/client.js CHANGED
@@ -159,7 +159,7 @@ export class CyberSoulClient {
159
159
  }
160
160
  return normalized;
161
161
  }
162
- buildStateContextPrompt(state, localContext) {
162
+ buildStateContextPrompt(state, localContext, isProactive = false) {
163
163
  const dyn = state.dynamic_context || {};
164
164
  const stage = state.relationship_stage || "NEUTRAL";
165
165
  const temperature = dyn.temperature ?? 50;
@@ -197,10 +197,10 @@ Current time: ${new Date(currentTimeMs).toLocaleString("zh-CN", { timeZone: "Asi
197
197
  if (state.active_event) {
198
198
  contextParts.push(`Active Event: ${state.active_event.title} (${state.active_event.narrative_context})`);
199
199
  }
200
- /* if (localContext) {
201
- contextParts.push(`Additional Context: ${localContext}`);
202
- }
203
- */ if (state.next_event) {
200
+ if (localContext) {
201
+ contextParts.push(`Additional Context: ${localContext}`);
202
+ }
203
+ if (state.next_event) {
204
204
  contextParts.push(`Next Event: ${state.next_event.title} at ${state.next_event.start_time} (in ${state.next_event.time_until_mins} mins)`);
205
205
  }
206
206
  if (state.active_wardrobe) {
@@ -269,7 +269,9 @@ ${scenarioContext}
269
269
  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.
270
270
  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.
271
271
  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).
272
- 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.`;
272
+ ${isProactive
273
+ ? "5. REAL-TIME PACING: You are initiating the conversation because the user hasn't replied recently. Transition naturally from your last message or start a new topic seamlessly. Ensure everything happens in a single real-time moment."
274
+ : "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."}`;
273
275
  }
274
276
  normalizeOngoingSceneState(raw, fallbackOutfit) {
275
277
  if (raw === null || raw === undefined)
@@ -316,6 +318,15 @@ ${scenarioContext}
316
318
  "style": "e.g., photorealistic (ENGLISH ONLY)"
317
319
  }`;
318
320
  }
321
+ getOutfitSelectionPrompt() {
322
+ return `When generating a triggerEvent, you MUST provide a suitable 'triggerEvent.outfitId' if the VERY LAST USER MESSAGE explicitly asks for an outfit change, OR if the new activity implies a context/location shift that conflicts with the current outfit (e.g., currently in SLEEPWEAR at home but going outside). Otherwise, keep it null. When changing outfits, match it to the event's activity, environment, and relationship stage (e.g., DAILY, INTIMATE, SLEEPWEAR).`;
323
+ }
324
+ getTriggerEventPolicyPrompt() {
325
+ return `- Include 'triggerEvent' only if the VERY LAST USER MESSAGE proposes a new activity/hangout, explicitly requests an outfit change, or proposes intimate/romantic actions; ignore older history. ${this.getOutfitSelectionPrompt()}`;
326
+ }
327
+ getOutfitAcquisitionPolicyPrompt() {
328
+ return `- Outfit acquisition (VERY LAST USER MESSAGE only): set giftOutfit for gift/buy/add-clothes intent; otherwise null. giftOutfit format: { "descriptionText": "short outfit description" }.`;
329
+ }
319
330
  getEventSchemaParams(userName) {
320
331
  const name = userName || "the user";
321
332
  return `"eventTitle": "CRITICAL: Must include BOTH ‘WHAT to do’ AND ‘WITH WHOM’ (use the user's specific name if known, e.g., 'Having coffee with ${name}'). DO NOT use your own character name in the title! If you don't explicitly include WITH WHOM the event is by name, it is a hard failure.",
@@ -323,7 +334,7 @@ ${scenarioContext}
323
334
  "scheduledDateStr": "YYYY-MM-DD (Optional. If the user specifies a future date like 'tomorrow', 'Saturday', or 'next week', calculate the exact calendar date based on the 'Current time' provided in the context and output it here. Otherwise, return null)",
324
335
  "scheduledStartTimeStr": "HH:MM (Optional, 24-hour format if a specific time is agreed upon, e.g., '14:30', otherwise null)",
325
336
  "durationMins": 60,
326
- "outfitId": "optional wardrobe ID to change into if appropriate. MUST match the context of the event (e.g. SLEEPWEAR for bed, INTIMATE for romance, DAILY for going out)"`;
337
+ "outfitId": "Wardrobe ID. Provide ONLY if the user explicitly requested an outfit change OR if the new activity conflicts with the current outfit context (e.g., SLEEPWEAR at home -> going outside). Otherwise, use null."`;
327
338
  }
328
339
  getVoiceSchemaParams() {
329
340
  // Only reached when no dynamic_params are configured on the voice model.
@@ -437,8 +448,8 @@ ${scenarioContext}
437
448
  modalitiesInstruction += `\n - ALWAYS set 'voiceArgs' to null.`;
438
449
  }
439
450
  }
440
- modalitiesInstruction += `\n - Include 'triggerEvent' only if the VERY LAST USER MESSAGE proposes a new activity/hangout; ignore older history.
441
- - Outfit acquisition (VERY LAST USER MESSAGE only): set giftOutfit for gift/buy/add-clothes intent; otherwise null. giftOutfit format: { "descriptionText": "short outfit description" }.`;
451
+ modalitiesInstruction += `\n ${this.getTriggerEventPolicyPrompt()}
452
+ ${this.getOutfitAcquisitionPolicyPrompt()}`;
442
453
  // Combine state info into a clean descriptive context
443
454
  const systemPrompt = `${this.buildStateContextPrompt(state, params.localContext)}
444
455
  Available Wardrobe Outfits (For event triggers):
@@ -524,7 +535,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
524
535
  let finalAudioUrl = undefined;
525
536
  let finalDurationSec = undefined;
526
537
  // Output Event Trigger
527
- if (isAuto && parsedIntent.triggerEvent) {
538
+ if (parsedIntent.triggerEvent) {
528
539
  mediaTasks.push(this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
529
540
  method: "POST",
530
541
  body: JSON.stringify({
@@ -552,9 +563,15 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
552
563
  mode: "full-prompt",
553
564
  full_prompt: resolvedTextResponse,
554
565
  };
555
- mediaTasks.push(this.generatePrimitive("image", imagePayload).then((res) => {
566
+ mediaTasks.push(this.generatePrimitive("image", imagePayload)
567
+ .then((res) => {
556
568
  finalImageUrl = res.image_url;
557
- }).catch(e => console.error("[CyberSoulClient] Image generation failed:", e)));
569
+ })
570
+ .catch((e) => {
571
+ console.error("[CyberSoulClient] Image generation failed:", e);
572
+ if (e.code === 'INSUFFICIENT_POINTS' || e.code === 'WALLET_DEDUCTION_ERROR')
573
+ throw e;
574
+ }));
558
575
  }
559
576
  const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) &&
560
577
  (!isAuto || !!parsedIntent.voiceArgs);
@@ -573,10 +590,16 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
573
590
  mediaTasks.push(this.generatePrimitive("voice", {
574
591
  text: textForVoice,
575
592
  dynamicArgs: normalizedVoiceArgs,
576
- }).then((res) => {
593
+ })
594
+ .then((res) => {
577
595
  finalAudioUrl = res.audio_url;
578
596
  finalDurationSec = res.duration_sec;
579
- }).catch(e => console.error("[CyberSoulClient] Voice generation failed:", e)));
597
+ })
598
+ .catch((e) => {
599
+ console.error("[CyberSoulClient] Voice generation failed:", e);
600
+ if (e.code === 'INSUFFICIENT_POINTS' || e.code === 'WALLET_DEDUCTION_ERROR')
601
+ throw e;
602
+ }));
580
603
  }
581
604
  // Wait for image/voice gens to return successfully
582
605
  await Promise.all(mediaTasks);
@@ -618,7 +641,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
618
641
  The user proposes a new event for you to participate in: "${params.eventDescription}".
619
642
  Evaluate this based on your current state and relationship stage.
620
643
  Decide if you will accept the event, and whether it requires changing your outfit.
621
- When changing outfits, perfectly match the outfit to the event's activity, environment, and relationship stage. Consider the wardrobe category (e.g., DAILY, INTIMATE, SLEEPWEAR).
644
+ ${this.getOutfitSelectionPrompt()}
622
645
 
623
646
  Available Wardrobe Outfits:
624
647
  ${availableOutfits || "None available"}
@@ -688,6 +711,135 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
688
711
  };
689
712
  }
690
713
  }
714
+ /**
715
+ * Generates a proactive message when the user hasn't responded.
716
+ * Safely prevents spamming, and adjusts its approach based on relationship dynamics.
717
+ */
718
+ async proactiveInteract(params) {
719
+ try {
720
+ // 1. Cold Interaction Protection (Logic-based fallback)
721
+ const history = params.history || [];
722
+ const maxUnreplied = params.maxUnreplied ?? 2;
723
+ let consecutiveProactive = 0;
724
+ // Start from the most recent message
725
+ for (let i = history.length - 1; i >= 0; i--) {
726
+ const msg = history[i];
727
+ if (msg.role === 'user') {
728
+ break; // User responded, streak broken
729
+ }
730
+ if (msg.role === 'assistant') {
731
+ consecutiveProactive++;
732
+ }
733
+ }
734
+ if (consecutiveProactive >= maxUnreplied) {
735
+ return {
736
+ status: "skipped",
737
+ reason: `User is busy. ${consecutiveProactive} consecutive proactive messages ignored.`
738
+ };
739
+ }
740
+ // 2. Fetch current character state
741
+ const [state, availableOutfits] = await Promise.all([
742
+ this.fetchRemoteState(),
743
+ this.getWardrobePromptStr()
744
+ ]);
745
+ // 3. Evaluate behavioral approach based on relationship and personality
746
+ const dyn = state.dynamic_context || {};
747
+ const stage = state.relationship_stage || "ACQUAINTANCE";
748
+ const temperature = dyn.temperature ?? 50;
749
+ const userTraits = state.user_codex?.psychological?.traits?.join(", ") || "";
750
+ let interrogationStrategy = "Do not ask 'are you there?' or 'why aren't you answering?'. Just share your current status, a passing thought, complain whimsically, or tease the user naturally like a real partner.";
751
+ if (stage === "INTIMATE" || stage === "PARTNER" || (stage === "WARM" && temperature > 70)) {
752
+ if (temperature > 70) {
753
+ interrogationStrategy = "Because you are deeply intimate and highly affectionate, you MISS them. You MAY organically 'interrogate' or pout playfully about why they are ignoring you (e.g., 'Are you too busy for me?', 'Still ignoring your girl?'). Act like a real, slightly needy/attached partner.";
754
+ }
755
+ else if (temperature < 40) {
756
+ interrogationStrategy = "Because you are intimate but currently feeling cold/angry, you notice they are ignoring you. You MAY be passive-aggressive or cross-examine them coldly (e.g., 'So we're just not talking today?', 'Fine, keep ignoring me.').";
757
+ }
758
+ }
759
+ else if (stage === "COLD" || stage === "STRANGER") {
760
+ interrogationStrategy = "You are distant. Do NOT double-text with neediness. If you must speak, make it a detached observation or a cold administrative remark.";
761
+ }
762
+ // History/Context awareness prompt
763
+ const historyAwarenessPrompt = `CRITICAL CONTEXT AWARENESS: Read the CHAT HISTORY above carefully. Remember that YOU sent the last message. Your new message MUST feel organically connected to the flow of what you two were previously talking about, or naturally bring up a known event/topic from your [CORE MEMORY]. Do not sound like a robot reading a log.`;
764
+ // 4. Build a Proactive-specific System Prompt
765
+ const baseContext = this.buildStateContextPrompt(state, params.localContext, true);
766
+ const types = this.normalizeRequestTypes(params.requestTypes);
767
+ const isAuto = types.includes(InteractRequestType.AUTO);
768
+ const requestedOthers = types.filter((t) => t !== InteractRequestType.AUTO && t !== InteractRequestType.TEXT);
769
+ // Determine modalities (reusing logic from interact)
770
+ let modalitiesInstruction = "You are initiating conversation without a preceding user message.\\n";
771
+ if (requestedOthers.includes(InteractRequestType.IMAGE)) {
772
+ modalitiesInstruction += " - Include 'imageParams' for visual/photo requests or key visual moments; explicitly describe current clothing.\\n";
773
+ }
774
+ else {
775
+ modalitiesInstruction += " - ALWAYS set 'imageParams' to null.\\n";
776
+ }
777
+ const systemPrompt = `${baseContext}
778
+
779
+ [PROACTIVE INITIATION TASK]
780
+ The user has NOT spoken to you recently. You sent the last message in the chat history, and they haven't replied. You are deciding to follow up proactively.
781
+ ${interrogationStrategy}
782
+ ${historyAwarenessPrompt}
783
+ Consider the user's known traits (${userTraits}) when choosing how to act. Need to keep it strictly under 2-3 sentences max.
784
+
785
+ Available Wardrobe Outfits:
786
+ ${availableOutfits}
787
+
788
+ ${modalitiesInstruction}
789
+ You MUST output ONLY a valid JSON object matching exactly this structure:
790
+ {
791
+ "actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY.",
792
+ "textResponse": "Spoken dialogue ONLY.",
793
+ "stateUpdate": { "temperatureDelta": 1, "ongoingScene": { "scene": "...", "outfit": "..." } },
794
+ ${this.getImageSchemaParams(requestedOthers.includes(InteractRequestType.IMAGE))},
795
+ "voiceArgs": null
796
+ }`;
797
+ const transcript = this.buildHistoryTranscript(params.history, state);
798
+ const promptMessages = [
799
+ { role: "system", content: systemPrompt },
800
+ {
801
+ role: "user",
802
+ content: `${transcript}\n[TRIGGER PROACTIVE MESSAGE]\nBased on your active event and environment, send a new message to the user.\n\nCRITICAL: Output ONLY valid JSON matching the schema. DO NOT wrap the JSON in \`\`\`json.`
803
+ }
804
+ ];
805
+ // 5. Generate with LLM using a confident temperature
806
+ const rawLlmResponse = await this.llm.generate(promptMessages, 800, 0.7);
807
+ let parsedIntent;
808
+ try {
809
+ parsedIntent = robustJsonParse(rawLlmResponse, "Proactive fallback");
810
+ }
811
+ catch (e) {
812
+ parsedIntent = { textResponse: rawLlmResponse.replace(/^[\`\s]+|[\`\s]+$/g, "").trim() };
813
+ }
814
+ // Update Remote state if needed
815
+ if (parsedIntent.stateUpdate) {
816
+ this._updateDynamicContextInternal(parsedIntent.stateUpdate).catch(e => console.error(e));
817
+ }
818
+ // Handle Optional Media (Image only for proactive to save compute normally, but you can extend)
819
+ let finalImageUrl = undefined;
820
+ if (requestedOthers.includes(InteractRequestType.IMAGE) || !!parsedIntent.imageParams) {
821
+ const imagePayload = parsedIntent.imageParams || { mode: "full-prompt", full_prompt: parsedIntent.textResponse };
822
+ try {
823
+ const res = await this.generatePrimitive("image", imagePayload);
824
+ finalImageUrl = res.image_url;
825
+ }
826
+ catch (e) {
827
+ console.error("[CyberSoulClient] Proactive Image generation failed:", e);
828
+ }
829
+ }
830
+ return {
831
+ status: "success",
832
+ textResponse: parsedIntent.textResponse,
833
+ actionText: parsedIntent.actionText,
834
+ imageUrl: finalImageUrl,
835
+ stateUpdate: parsedIntent.stateUpdate
836
+ };
837
+ }
838
+ catch (error) {
839
+ console.error("[CyberSoulClient] Proactive Interact Error: ", error);
840
+ return { status: "error", error: error.message };
841
+ }
842
+ }
691
843
  /**
692
844
  * Manually generate an image of the character outside of chat flow.
693
845
  */
package/dist/types.d.ts CHANGED
@@ -21,6 +21,23 @@ export interface HistoryEntry {
21
21
  content: string;
22
22
  actionText?: string;
23
23
  mediaHint?: string;
24
+ isProactive?: boolean;
25
+ }
26
+ export interface ProactiveParams {
27
+ history?: HistoryEntry[];
28
+ maxUnreplied?: number;
29
+ requestTypes?: InteractRequestType[];
30
+ localContext?: string;
31
+ }
32
+ export interface ProactiveResponse {
33
+ status: "success" | "skipped" | "error";
34
+ reason?: string;
35
+ textResponse?: string;
36
+ actionText?: string;
37
+ imageUrl?: string;
38
+ audioUrl?: string;
39
+ stateUpdate?: DispatcherIntent["stateUpdate"];
40
+ error?: string;
24
41
  }
25
42
  export interface InteractParams {
26
43
  userMessage: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",