@space3-npm/cybersoul-client 1.4.8 → 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
@@ -861,137 +861,139 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
861
861
  }
862
862
  /**
863
863
  * Generates a proactive message when the user hasn't responded.
864
- * 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.
865
873
  */
866
874
  async proactiveInteract(params) {
867
875
  try {
868
- // 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.
869
879
  const history = params.history || [];
870
880
  const maxUnreplied = params.maxUnreplied ?? 2;
871
881
  let consecutiveProactive = 0;
872
- // Start from the most recent message
873
882
  for (let i = history.length - 1; i >= 0; i--) {
874
883
  const msg = history[i];
875
- if (msg.role === 'user') {
876
- break; // User responded, streak broken
877
- }
878
- if (msg.role === 'assistant') {
884
+ if (msg.role === "user")
885
+ break;
886
+ if (msg.role === "assistant")
879
887
  consecutiveProactive++;
880
- }
881
888
  }
882
889
  if (consecutiveProactive >= maxUnreplied) {
883
890
  return {
884
891
  status: "skipped",
885
- reason: `User is busy. ${consecutiveProactive} consecutive proactive messages ignored.`
892
+ reason: `Spam guard: ${consecutiveProactive} consecutive un-replied messages already sent.`,
886
893
  };
887
894
  }
888
- // 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.
889
899
  const [state, availableOutfits] = await Promise.all([
890
900
  this.fetchRemoteState(),
891
- this.getWardrobePromptStr()
901
+ this.getWardrobePromptStr(),
892
902
  ]);
893
- // 3. Evaluate behavioral approach based on relationship and personality
894
- const dyn = state.dynamic_context || {};
895
- const stage = state.relationship_stage || "STRANGER";
896
- const temperature = dyn.temperature ?? 0;
897
- const userTraits = state.user_codex?.psychological?.traits?.join(", ") || "";
898
- 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.";
899
- if (stage === "PARTNER" || (stage === "INTIMATE" && temperature > 70)) {
900
- // PARTNER (>85) or High INTIMATE (>70)
901
- 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.";
902
- }
903
- else if (stage === "INTIMATE" || stage === "WARM") {
904
- // Low INTIMATE (60-70) or WARM (40-60)
905
- 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.').";
906
- }
907
- else if (stage === "COLD" || stage === "ACQUAINTANCE" || stage === "STRANGER") {
908
- // COLD (<40)
909
- interrogationStrategy = "You are distant. Do NOT double-text with neediness. If you must speak, make it a detached observation or a cold administrative remark.";
910
- }
911
- // History/Context awareness prompt
912
- 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.`;
913
- // 4. Build a Proactive-specific System Prompt
914
903
  const baseContext = this.buildStateContextPrompt(state, true);
915
904
  const types = this.normalizeRequestTypes(params.requestTypes);
916
905
  const requestedOthers = types.filter((t) => t !== InteractRequestType.AUTO && t !== InteractRequestType.TEXT);
917
- // Determine modalities (reusing logic from interact)
918
- let modalitiesInstruction = "You are initiating conversation without a preceding user message.\\n";
919
- if (requestedOthers.includes(InteractRequestType.IMAGE)) {
920
- 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";
921
- }
922
- else {
923
- modalitiesInstruction += " - ALWAYS set 'imageParams' to null.\\n";
924
- }
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.
925
911
  const systemPrompt = `${baseContext}
926
912
 
927
- [PROACTIVE INITIATION TASK]
928
- 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.
929
- 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.
930
- ${interrogationStrategy}
931
- ${historyAwarenessPrompt}
932
- 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?".
933
930
 
934
931
  Available Wardrobe Outfits:
935
932
  ${availableOutfits}
936
933
 
937
- ${modalitiesInstruction}
938
- 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.
939
944
  {
940
945
  "shouldSkipProactive": false,
941
- "skipReason": "(Optional. Reason for skipping if shouldSkipProactive is true)",
946
+ "skipReason": null,
942
947
  "actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY.",
943
948
  "textResponse": "Spoken dialogue ONLY.",
944
- "stateUpdate": { "temperatureDelta": 1, "ongoingScene": { "scene": "...", "outfit": "..." } },
945
- ${this.getImageSchemaParams(requestedOthers.includes(InteractRequestType.IMAGE))},
949
+ "stateUpdate": { "temperatureDelta": 0, "ongoingScene": { "scene": "...", "outfit": "..." } },
950
+ ${this.getImageSchemaParams(imageAllowed)},
946
951
  "voiceArgs": null
947
952
  }`;
948
- const transcript = params.history && params.history.length > 0 ? this.buildHistoryTranscript(params.history, state) : "";
949
- 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
+ : "";
950
959
  const promptMessages = [
951
960
  { role: "system", content: systemPrompt },
952
961
  {
953
962
  role: "user",
954
- 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.`
955
- }
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
+ },
956
965
  ];
957
- // 5. Generate with LLM using a confident temperature
958
- const rawLlmResponse = await this.llm.generate(promptMessages, 800, 0.7);
959
- let parsedIntent;
960
- try {
961
- parsedIntent = robustJsonParse(rawLlmResponse, "Proactive fallback");
962
- }
963
- catch (e) {
964
- parsedIntent = { textResponse: rawLlmResponse.replace(/^[\`\s]+|[\`\s]+$/g, "").trim() };
965
- }
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");
966
973
  if (parsedIntent.shouldSkipProactive) {
967
974
  return {
968
975
  status: "skipped",
969
- reason: parsedIntent.skipReason || "Character decided to skip proactive message based on mood/stage."
976
+ reason: parsedIntent.skipReason || "Character chose not to reach out.",
970
977
  };
971
978
  }
972
- // Update Remote state if needed (capture promise for authoritative
973
- // 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.
974
986
  let persistedStatePromise = Promise.resolve(null);
975
987
  if (parsedIntent.stateUpdate) {
976
988
  persistedStatePromise = this._updateDynamicContextInternal(parsedIntent.stateUpdate);
977
989
  }
978
- const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
979
- parsedIntent.textResponse.trim().length > 0
980
- ? parsedIntent.textResponse
981
- : "...";
982
- // Fire text ready callback if provided
983
- if (params.onTextReady && (resolvedTextResponse || parsedIntent.actionText)) {
984
- params.onTextReady(resolvedTextResponse, parsedIntent.actionText, {
990
+ if (params.onTextReady) {
991
+ params.onTextReady(parsedIntent.textResponse, parsedIntent.actionText, {
985
992
  stateUpdate: parsedIntent.stateUpdate,
986
- userAnalysis: parsedIntent.userAnalysis,
987
- isEndTurn: parsedIntent.isEndTurn,
988
- triggerEvent: parsedIntent.triggerEvent,
989
- likePreviousPicture: parsedIntent.likePreviousPicture,
990
993
  });
991
994
  }
992
- // Handle Optional Media (Image only for proactive to save compute normally, but you can extend)
993
- let finalImageUrl = undefined;
994
- let finalImageMediaId = undefined;
995
+ let finalImageUrl;
996
+ let finalImageMediaId;
995
997
  if (parsedIntent.imageParams) {
996
998
  try {
997
999
  const res = await this.generatePrimitive("image", parsedIntent.imageParams);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.8",
3
+ "version": "1.4.9",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",