@space3-npm/cybersoul-client 1.4.7 → 1.4.9

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
@@ -79,7 +79,15 @@ export declare class CyberSoulClient {
79
79
  ondemandEvent(params: OndemandEventParams): Promise<OndemandEventResponse>;
80
80
  /**
81
81
  * Generates a proactive message when the user hasn't responded.
82
- * Safely prevents spamming, and adjusts its approach based on relationship dynamics.
82
+ *
83
+ * Design:
84
+ * - Code owns ONE objective rule: don't spam (cap consecutive un-replied
85
+ * messages). Everything else is a social judgment.
86
+ * - The LLM owns the social judgment — given full character context
87
+ * (stage, temperature, traits, ongoing scene, time since last
88
+ * interaction, recent history), it answers a single question:
89
+ * "Would I, as this person right now, actually reach out?"
90
+ * Skip is the default; speaking is the exception.
83
91
  */
84
92
  proactiveInteract(params: ProactiveParams): Promise<ProactiveResponse>;
85
93
  /**
package/dist/client.js CHANGED
@@ -224,6 +224,7 @@ export class CyberSoulClient {
224
224
  Name: ${state.name}
225
225
  Demographics: Age ${state.age || "unknown"}, Gender ${state.gender || "unknown"}, Occupation ${state.occupation || "unknown"}${appearanceStr}
226
226
  Hobby: ${state.hobby || "unknown"}
227
+ Backstory: ${state.backstory || "None"}
227
228
  Personality Traits: ${state.personality_traits || "None"}
228
229
  Communication Style: ${state.communication_style || "None"}
229
230
  Interaction Boundaries: ${state.interaction_boundaries || "None"}`);
@@ -860,137 +861,139 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
860
861
  }
861
862
  /**
862
863
  * Generates a proactive message when the user hasn't responded.
863
- * Safely prevents spamming, and adjusts its approach based on relationship dynamics.
864
+ *
865
+ * Design:
866
+ * - Code owns ONE objective rule: don't spam (cap consecutive un-replied
867
+ * messages). Everything else is a social judgment.
868
+ * - The LLM owns the social judgment — given full character context
869
+ * (stage, temperature, traits, ongoing scene, time since last
870
+ * interaction, recent history), it answers a single question:
871
+ * "Would I, as this person right now, actually reach out?"
872
+ * Skip is the default; speaking is the exception.
864
873
  */
865
874
  async proactiveInteract(params) {
866
875
  try {
867
- // 1. Cold Interaction Protection (Logic-based fallback)
876
+ // 1. Spam guard (the only hard-coded gate). Counts assistant messages
877
+ // since the last user reply; bails out if the user has clearly
878
+ // stopped responding.
868
879
  const history = params.history || [];
869
880
  const maxUnreplied = params.maxUnreplied ?? 2;
870
881
  let consecutiveProactive = 0;
871
- // Start from the most recent message
872
882
  for (let i = history.length - 1; i >= 0; i--) {
873
883
  const msg = history[i];
874
- if (msg.role === 'user') {
875
- break; // User responded, streak broken
876
- }
877
- if (msg.role === 'assistant') {
884
+ if (msg.role === "user")
885
+ break;
886
+ if (msg.role === "assistant")
878
887
  consecutiveProactive++;
879
- }
880
888
  }
881
889
  if (consecutiveProactive >= maxUnreplied) {
882
890
  return {
883
891
  status: "skipped",
884
- reason: `User is busy. ${consecutiveProactive} consecutive proactive messages ignored.`
892
+ reason: `Spam guard: ${consecutiveProactive} consecutive un-replied messages already sent.`,
885
893
  };
886
894
  }
887
- // 2. Fetch current character state
895
+ // 2. Fetch state. baseContext below already includes stage,
896
+ // temperature, traits, ongoing scene, active/next event, current
897
+ // time, and lastInteractionAt — the LLM has everything it needs to
898
+ // make the social call without us restating it.
888
899
  const [state, availableOutfits] = await Promise.all([
889
900
  this.fetchRemoteState(),
890
- this.getWardrobePromptStr()
901
+ this.getWardrobePromptStr(),
891
902
  ]);
892
- // 3. Evaluate behavioral approach based on relationship and personality
893
- const dyn = state.dynamic_context || {};
894
- const stage = state.relationship_stage || "STRANGER";
895
- const temperature = dyn.temperature ?? 0;
896
- const userTraits = state.user_codex?.psychological?.traits?.join(", ") || "";
897
- 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.";
898
- if (stage === "PARTNER" || (stage === "INTIMATE" && temperature > 70)) {
899
- // PARTNER (>85) or High INTIMATE (>70)
900
- 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.";
901
- }
902
- else if (stage === "INTIMATE" || stage === "WARM") {
903
- // Low INTIMATE (60-70) or WARM (40-60)
904
- interrogationStrategy = "Because you are close but currently feeling neglected or cold, 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.').";
905
- }
906
- else if (stage === "COLD" || stage === "ACQUAINTANCE" || stage === "STRANGER") {
907
- // COLD (<40)
908
- interrogationStrategy = "You are distant. Do NOT double-text with neediness. If you must speak, make it a detached observation or a cold administrative remark.";
909
- }
910
- // History/Context awareness prompt
911
- 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.`;
912
- // 4. Build a Proactive-specific System Prompt
913
903
  const baseContext = this.buildStateContextPrompt(state, true);
914
904
  const types = this.normalizeRequestTypes(params.requestTypes);
915
905
  const requestedOthers = types.filter((t) => t !== InteractRequestType.AUTO && t !== InteractRequestType.TEXT);
916
- // Determine modalities (reusing logic from interact)
917
- let modalitiesInstruction = "You are initiating conversation without a preceding user message.\\n";
918
- if (requestedOthers.includes(InteractRequestType.IMAGE)) {
919
- modalitiesInstruction += " - Include 'imageParams' for visual/photo moments. CRITICAL POLICY: NEVER send pictures to strangers! If Stage is STRANGER or COLD, or Familiarity is very low (< 10), ALWAYS set 'imageParams' to null.\\n";
920
- }
921
- else {
922
- modalitiesInstruction += " - ALWAYS set 'imageParams' to null.\\n";
923
- }
906
+ const imageAllowed = requestedOthers.includes(InteractRequestType.IMAGE);
907
+ // 3. Build the prompt. We deliberately ask ONE coherent question
908
+ // framed in-character ("would I text right now?") rather than
909
+ // handing the LLM a checklist. The character's own traits,
910
+ // relationship state, and recent transcript are the inputs.
924
911
  const systemPrompt = `${baseContext}
925
912
 
926
- [PROACTIVE INITIATION TASK]
927
- 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.
928
- If you decide that based on your current mood and the relationship stage it's better not to send a message right now (e.g. you are cold and giving them space), you can skip this proactive message by setting "shouldSkipProactive" to true.
929
- ${interrogationStrategy}
930
- ${historyAwarenessPrompt}
931
- Consider the user's known traits (${userTraits}) when choosing how to act. Need to keep it strictly under 2-3 sentences max.
913
+ [PROACTIVE OPPORTUNITY]
914
+ Time has passed since the last message in [CHAT HISTORY] and the user has not replied. You have an OPPORTUNITY (not an obligation) to send them a message. Decide, in character, whether you would actually do that.
915
+
916
+ [HOW TO DECIDE — THINK LIKE THE PERSON YOU ARE]
917
+ Real humans rarely send unprompted messages. Most of the time, silence is the right answer. Reach out ONLY if a real person with YOUR personality, in YOUR relationship to this user, at THIS moment, would genuinely feel moved to text.
918
+
919
+ Reasons NOT to reach out (set "shouldSkipProactive": true):
920
+ - The last exchange ended on a note that closes the door — a farewell, a brush-off, a fight, a "talk later", an explicit dismissal — from either side. If YOU pushed them away last turn (because of your traits or a fight), staying quiet IS the in-character choice; flipping to friendly now makes you look bipolar.
921
+ - Your relationship is too distant for unsolicited contact (e.g. STRANGER, COLD) or your current mood is too low to want to reach out.
922
+ - Too little time has passed since the last message for a follow-up to feel natural. Use the time gap shown in [CHAT HISTORY] — minutes after the last turn is almost always too soon.
923
+ - There is no genuine reason to text — no shared thread, no event, no thought that would actually push a real person to pick up the phone.
924
+ - It's the wrong time of day for this relationship.
925
+
926
+ When in doubt: SKIP. The bar for reaching out is high.
927
+
928
+ [IF YOU DO DECIDE TO REACH OUT]
929
+ Speak strictly in character — your traits, communication style, and current mood dictate the tone. Do NOT default to needy/cheerful unless that's who you are. Connect naturally to the last topic or to your current scene/event. Keep it to 2-3 short sentences. Never ask "are you there?" or "why aren't you answering?".
932
930
 
933
931
  Available Wardrobe Outfits:
934
932
  ${availableOutfits}
935
933
 
936
- ${modalitiesInstruction}
937
- You MUST output ONLY a valid JSON object matching exactly this structure:
934
+ Modalities:
935
+ - 'textResponse' is required when you proceed.
936
+ - ${imageAllowed
937
+ ? "'imageParams' may be included only if sending a photo right now would feel natural for this character in this relationship — otherwise set null. Do not attach a photo just because you can."
938
+ : "ALWAYS set 'imageParams' to null."}
939
+ - ALWAYS set 'voiceArgs' to null.
940
+
941
+ Output ONLY a valid JSON object matching exactly this structure (no markdown wrappers).
942
+ If "shouldSkipProactive" is true, set "skipReason" to one short sentence and set every other field to null.
943
+ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate" must be provided; include "ongoingScene" only if your scene/outfit actually changed, otherwise omit it.
938
944
  {
939
945
  "shouldSkipProactive": false,
940
- "skipReason": "(Optional. Reason for skipping if shouldSkipProactive is true)",
946
+ "skipReason": null,
941
947
  "actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY.",
942
948
  "textResponse": "Spoken dialogue ONLY.",
943
- "stateUpdate": { "temperatureDelta": 1, "ongoingScene": { "scene": "...", "outfit": "..." } },
944
- ${this.getImageSchemaParams(requestedOthers.includes(InteractRequestType.IMAGE))},
949
+ "stateUpdate": { "temperatureDelta": 0, "ongoingScene": { "scene": "...", "outfit": "..." } },
950
+ ${this.getImageSchemaParams(imageAllowed)},
945
951
  "voiceArgs": null
946
952
  }`;
947
- const transcript = params.history && params.history.length > 0 ? this.buildHistoryTranscript(params.history, state) : "";
948
- const harnessContext = params.localContext ? `[ADDITIONAL SCENE CONTEXT]\n${params.localContext}\n\n` : "";
953
+ const transcript = params.history && params.history.length > 0
954
+ ? this.buildHistoryTranscript(params.history, state)
955
+ : "";
956
+ const harnessContext = params.localContext
957
+ ? `[ADDITIONAL SCENE CONTEXT]\n${params.localContext}\n\n`
958
+ : "";
949
959
  const promptMessages = [
950
960
  { role: "system", content: systemPrompt },
951
961
  {
952
962
  role: "user",
953
- content: `${harnessContext}${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.`
954
- }
963
+ content: `${harnessContext}${transcript}\n[DECIDE NOW]\nWould you, as this character, actually send a message right now? Answer in the JSON schema above.`,
964
+ },
955
965
  ];
956
- // 5. Generate with LLM using a confident temperature
957
- const rawLlmResponse = await this.llm.generate(promptMessages, 800, 0.7);
958
- let parsedIntent;
959
- try {
960
- parsedIntent = robustJsonParse(rawLlmResponse, "Proactive fallback");
961
- }
962
- catch (e) {
963
- parsedIntent = { textResponse: rawLlmResponse.replace(/^[\`\s]+|[\`\s]+$/g, "").trim() };
964
- }
966
+ // 4. LLM decides. Lower temperature than `interact` because this is a
967
+ // judgment call, not creative reply.
968
+ const rawLlmResponse = await this.llm.generate(promptMessages, 800, 0.5);
969
+ // Fail fast on parse error. A proactive message is opt-in by design;
970
+ // if the LLM produced unparseable output we'd rather skip than ship
971
+ // raw scaffolding to the user.
972
+ const parsedIntent = robustJsonParse(rawLlmResponse, "Proactive fallback");
965
973
  if (parsedIntent.shouldSkipProactive) {
966
974
  return {
967
975
  status: "skipped",
968
- reason: parsedIntent.skipReason || "Character decided to skip proactive message based on mood/stage."
976
+ reason: parsedIntent.skipReason || "Character chose not to reach out.",
969
977
  };
970
978
  }
971
- // Update Remote state if needed (capture promise for authoritative
972
- // server snapshot — see notes in interact()).
979
+ if (typeof parsedIntent.textResponse !== "string" || parsedIntent.textResponse.trim().length === 0) {
980
+ return {
981
+ status: "skipped",
982
+ reason: "LLM produced no textResponse (treated as implicit skip).",
983
+ };
984
+ }
985
+ // 5. Persist state and optionally generate image, in parallel.
973
986
  let persistedStatePromise = Promise.resolve(null);
974
987
  if (parsedIntent.stateUpdate) {
975
988
  persistedStatePromise = this._updateDynamicContextInternal(parsedIntent.stateUpdate);
976
989
  }
977
- const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
978
- parsedIntent.textResponse.trim().length > 0
979
- ? parsedIntent.textResponse
980
- : "...";
981
- // Fire text ready callback if provided
982
- if (params.onTextReady && (resolvedTextResponse || parsedIntent.actionText)) {
983
- params.onTextReady(resolvedTextResponse, parsedIntent.actionText, {
990
+ if (params.onTextReady) {
991
+ params.onTextReady(parsedIntent.textResponse, parsedIntent.actionText, {
984
992
  stateUpdate: parsedIntent.stateUpdate,
985
- userAnalysis: parsedIntent.userAnalysis,
986
- isEndTurn: parsedIntent.isEndTurn,
987
- triggerEvent: parsedIntent.triggerEvent,
988
- likePreviousPicture: parsedIntent.likePreviousPicture,
989
993
  });
990
994
  }
991
- // Handle Optional Media (Image only for proactive to save compute normally, but you can extend)
992
- let finalImageUrl = undefined;
993
- let finalImageMediaId = undefined;
995
+ let finalImageUrl;
996
+ let finalImageMediaId;
994
997
  if (parsedIntent.imageParams) {
995
998
  try {
996
999
  const res = await this.generatePrimitive("image", parsedIntent.imageParams);
package/dist/types.d.ts CHANGED
@@ -256,6 +256,7 @@ export interface CharacterState {
256
256
  appearance?: string;
257
257
  interaction_boundaries?: string;
258
258
  communication_style?: string;
259
+ backstory?: string;
259
260
  user_codex?: UserCodex;
260
261
  }
261
262
  export interface BaseLLMProvider {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.7",
3
+ "version": "1.4.9",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",