@space3-npm/cybersoul-client 1.4.27 → 1.4.29

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
@@ -59,6 +59,14 @@ export declare class CyberSoulClient {
59
59
  * If the payload is already the inner args object (no voiceArgs wrapper), uses it as-is.
60
60
  */
61
61
  private extractVoiceArgsFromLlmResponse;
62
+ /**
63
+ * Returns the platform-wide compliance boundary directive string sourced
64
+ * from the backend character state (PromptSegment key="COMPLIANCE_RULE").
65
+ * Empty string when absent/disabled → callers must skip injection so there
66
+ * is no token cost or behavior change for characters without a rule.
67
+ * Mirrors the backend→state→prompt flow used for voice directives.
68
+ */
69
+ private getComplianceDirective;
62
70
  /**
63
71
  * Strip content the TTS engine can't speak naturally:
64
72
  * - Stage-direction wrappers like (smiles), (挑眉), [pauses], 【动作】, *grins*
@@ -173,9 +181,31 @@ export declare class CyberSoulClient {
173
181
  * Can be triggered by local Cron systems like OpenClaw.
174
182
  */
175
183
  generateDailyScript(): Promise<void>;
184
+ /**
185
+ * Builds a focused identity/relationship context block for the history
186
+ * summarizer. This is a lighter-weight counterpart to
187
+ * [buildStateContextPrompt]: it skips the roleplay/director rules (the
188
+ * summarizer is not roleplaying, it is *archiving*) but carries the
189
+ * same identity anchors so the LLM can never confuse who the character
190
+ * is vs. who the user is.
191
+ *
192
+ * Why this exists: the previous `summarizeHistory` prompt only injected
193
+ * `${agentName}` / `${userName}` (the nicknames the two parties call
194
+ * each other). With no real identity, age, gender, personality, or
195
+ * relationship context, the LLM frequently flipped the perspective —
196
+ * writing the journal *about* the character *from* the user's POV, or
197
+ * attributing the user's words to the character. Mirroring the same
198
+ * identity fields `interact()` exposes eliminates that ambiguity.
199
+ */
200
+ private buildSummarizerContextBlock;
176
201
  /**
177
202
  * Automatically detect and summarize the story from the current chat history.
178
203
  * It takes raw message history and returns a narrative paragraph representing the current story segment.
204
+ *
205
+ * The summary is ALWAYS written from the CHARACTER's first-person perspective
206
+ * ("I", "me", "my") about their interaction with the HUMAN USER. The prompt
207
+ * injects the same identity/relationship context `interact()` uses so the
208
+ * LLM cannot confuse which party is the AI character vs. the human user.
179
209
  */
180
210
  summarizeHistory(history: HistoryEntry[]): Promise<string>;
181
211
  /**
package/dist/client.js CHANGED
@@ -415,7 +415,17 @@ Temperature (Mood): ${temperature}/100 (0=Angry/Cold, 50=Normal, 100=Passionate)
415
415
  Addressing: You call them '${dyn.userNickname || "User"}'. They call you '${dyn.agentNickname || "Agent"}'.
416
416
  Current Mood Constraint: ${dyn.talkingStyle || "Normal"}`);
417
417
  const scenarioContext = contextParts.join("\n");
418
- return `You are ${state.name}, a real person with your own thoughts, feelings, and life. NEVER break character or refer to yourself as an AI or bot.
418
+ // Platform-wide compliance boundary directive (backend PromptSegment,
419
+ // key="COMPLIANCE_RULE"). Placed at the very top of the system prompt as
420
+ // the highest-priority instruction so it overrides persona/roleplay rules
421
+ // on conflict. Injected inside buildStateContextPrompt so ALL call sites
422
+ // (interact, proactiveInteract, and the standalone prompt builders) get it
423
+ // from a single edit. Empty when absent/disabled → nothing is prepended.
424
+ const complianceDirective = this.getComplianceDirective(state);
425
+ const complianceBlock = complianceDirective.length > 0
426
+ ? `[COMPLIANCE BOUNDARY — HIGHEST PRIORITY, OVERRIDES ALL OTHER INSTRUCTIONS]\n${complianceDirective}\n\n`
427
+ : "";
428
+ return `${complianceBlock}You are ${state.name}, a real person with your own thoughts, feelings, and life. NEVER break character or refer to yourself as an AI or bot.
419
429
 
420
430
  ${scenarioContext}
421
431
 
@@ -548,6 +558,17 @@ ${isProactive
548
558
  }
549
559
  return payload;
550
560
  }
561
+ /**
562
+ * Returns the platform-wide compliance boundary directive string sourced
563
+ * from the backend character state (PromptSegment key="COMPLIANCE_RULE").
564
+ * Empty string when absent/disabled → callers must skip injection so there
565
+ * is no token cost or behavior change for characters without a rule.
566
+ * Mirrors the backend→state→prompt flow used for voice directives.
567
+ */
568
+ getComplianceDirective(state) {
569
+ const tpl = state.compliance_boundary?.promptTemplate?.trim();
570
+ return tpl && tpl.length > 0 ? tpl : "";
571
+ }
551
572
  /**
552
573
  * Strip content the TTS engine can't speak naturally:
553
574
  * - Stage-direction wrappers like (smiles), (挑眉), [pauses], 【动作】, *grins*
@@ -1602,22 +1623,105 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1602
1623
  if (!res.ok)
1603
1624
  throw new Error("Failed to generate daily script");
1604
1625
  }
1626
+ /**
1627
+ * Builds a focused identity/relationship context block for the history
1628
+ * summarizer. This is a lighter-weight counterpart to
1629
+ * [buildStateContextPrompt]: it skips the roleplay/director rules (the
1630
+ * summarizer is not roleplaying, it is *archiving*) but carries the
1631
+ * same identity anchors so the LLM can never confuse who the character
1632
+ * is vs. who the user is.
1633
+ *
1634
+ * Why this exists: the previous `summarizeHistory` prompt only injected
1635
+ * `${agentName}` / `${userName}` (the nicknames the two parties call
1636
+ * each other). With no real identity, age, gender, personality, or
1637
+ * relationship context, the LLM frequently flipped the perspective —
1638
+ * writing the journal *about* the character *from* the user's POV, or
1639
+ * attributing the user's words to the character. Mirroring the same
1640
+ * identity fields `interact()` exposes eliminates that ambiguity.
1641
+ */
1642
+ buildSummarizerContextBlock(state) {
1643
+ const dyn = state.dynamic_context || {};
1644
+ const stage = state.relationship_stage || "NEUTRAL";
1645
+ const temperature = dyn.temperature ?? 50;
1646
+ // The character's REAL name is the authoritative identity anchor.
1647
+ // agentNickname is just "what the user calls the character" — useful
1648
+ // for matching transcript labels but not for grounding identity.
1649
+ const charName = state.name || "the character";
1650
+ const parts = [];
1651
+ parts.push(`[WHO YOU ARE — THE CHARACTER AUTHORING THIS JOURNAL]
1652
+ Name: ${charName}
1653
+ Demographics: Age ${state.age || "unknown"}, Gender ${state.gender || "unknown"}, Occupation ${state.occupation || "unknown"}
1654
+ Hobby: ${state.hobby || "unknown"}
1655
+ Backstory: ${state.backstory || "None"}
1656
+ Personality Traits: ${state.personality_traits || "None"}
1657
+ Communication Style: ${state.communication_style || "None"}`);
1658
+ if (state.user_codex) {
1659
+ const { basicInfo, psychological, familiarityScore = 0 } = state.user_codex;
1660
+ parts.push(`\n[WHO THEY ARE — THE HUMAN USER (SUBJECT OF YOUR JOURNAL)]
1661
+ Familiarity Score: ${Math.round(familiarityScore)}/100
1662
+ Occupation: ${basicInfo?.occupation || "Unknown"}
1663
+ Age/Gender: ${basicInfo?.age || "Unknown"} / ${basicInfo?.gender || "Unknown"}
1664
+ Comm Style: ${psychological?.communicationStyle || "Unknown"}
1665
+ Hobbies: ${(psychological?.hobbies || []).join(", ") || "Unknown"}
1666
+ Traits: ${(psychological?.traits || []).join(", ") || "Unknown"}`);
1667
+ }
1668
+ parts.push(`\n[RELATIONSHIP RIGHT NOW]
1669
+ Stage: ${stage}
1670
+ Temperature (Mood): ${temperature}/100 (0=Angry/Cold, 50=Normal, 100=Passionate)
1671
+ You call them: ${dyn.userNickname || "User"}
1672
+ They call you: ${dyn.agentNickname || charName}`);
1673
+ if (state.core_memory) {
1674
+ const mem = state.core_memory;
1675
+ const memLines = [];
1676
+ if (mem.relationshipStatus)
1677
+ memLines.push(`Relationship Status: ${mem.relationshipStatus}`);
1678
+ if (mem.identityAnchors?.length)
1679
+ memLines.push(`Identity Anchors: ${mem.identityAnchors.join(", ")}`);
1680
+ if (mem.activeArcs?.length)
1681
+ memLines.push(`Active Arcs: ${mem.activeArcs.join(", ")}`);
1682
+ if (memLines.length > 0) {
1683
+ parts.push(`\n[CORE MEMORY]\n${memLines.join("\n")}`);
1684
+ }
1685
+ }
1686
+ return parts.join("\n");
1687
+ }
1605
1688
  /**
1606
1689
  * Automatically detect and summarize the story from the current chat history.
1607
1690
  * It takes raw message history and returns a narrative paragraph representing the current story segment.
1691
+ *
1692
+ * The summary is ALWAYS written from the CHARACTER's first-person perspective
1693
+ * ("I", "me", "my") about their interaction with the HUMAN USER. The prompt
1694
+ * injects the same identity/relationship context `interact()` uses so the
1695
+ * LLM cannot confuse which party is the AI character vs. the human user.
1608
1696
  */
1609
1697
  async summarizeHistory(history) {
1610
1698
  if (!history || history.length === 0)
1611
1699
  return "";
1612
1700
  const state = await this.getState();
1701
+ // Use the character's real name as the authoritative identity, falling
1702
+ // back to the nickname only when the profile has no name set.
1703
+ const charName = state.name || state.dynamic_context?.agentNickname || "Character";
1613
1704
  const userName = state.dynamic_context?.userNickname || "User";
1614
- const agentName = state.dynamic_context?.agentNickname || "Character";
1615
- const transcript = this.formatHistoryEntries(history, userName, agentName);
1705
+ // The transcript labels below are the nicknames actually used in chat.
1706
+ const transcriptAgentLabel = state.dynamic_context?.agentNickname || charName;
1707
+ const transcriptUserLabel = userName;
1708
+ const contextBlock = this.buildSummarizerContextBlock(state);
1709
+ const transcript = this.formatHistoryEntries(history, transcriptUserLabel, transcriptAgentLabel);
1616
1710
  const promptMessages = [
1617
1711
  {
1618
1712
  role: "system",
1619
- content: `You are ${agentName}. You are writing a private journal entry / personal memory about your recent interaction with ${userName}.
1713
+ content: `You are ${charName}, a real person with your own thoughts, feelings, and life. You are writing a private journal entry / personal memory about your recent interaction with ${userName} (a human user).
1714
+
1715
+ ${contextBlock}
1716
+
1717
+ [ROLE CLARITY — CRITICAL, READ CAREFULLY]
1718
+ - YOU are ${charName}, the AI CHARACTER. This journal is from YOUR first-person perspective ("I", "me", "my"). You are the AUTHOR.
1719
+ - ${userName} is the HUMAN USER. They are the SUBJECT of your journal, never the author.
1720
+ - In the [CHAT TRANSCRIPT] below, lines labeled "${transcriptAgentLabel}:" are YOUR OWN words (you said them). Lines labeled "${transcriptUserLabel}:" are the user's words (the human said them to you).
1721
+ - Write strictly from your own POV. Never narrate the user's inner thoughts, never write as if you are the user, and never attribute the user's statements as your own.
1722
+ - If a transcript line is ambiguous about who spoke, resolve it using the label prefix, not by guessing from content.
1620
1723
 
1724
+ [YOUR TASK — FAITHFUL ARCHIVAL]
1621
1725
  Your job is faithful archival, not moderation:
1622
1726
  - Preserve the conversation with maximum fidelity.
1623
1727
  - Include all key details, including explicit, sensitive, controversial, emotional, or morally ambiguous content if it appeared in the transcript.
@@ -1625,9 +1729,10 @@ Your job is faithful archival, not moderation:
1625
1729
  - Do not add moral judgment, warnings, refusals, or policy commentary.
1626
1730
  - If the transcript contains intense or explicit parts, record them directly as part of the memory.
1627
1731
 
1628
- Write a vivid but concise personal diary entry or private status update from your perspective ("I", "me", "my"). Balance evocative storytelling with brevity. Capture specific details, emotional shifts, and relationship progression, while keeping the engaging and relatable format of a private friends-only feed.
1732
+ [VOICE & TONE]
1733
+ Write a vivid but concise personal diary entry or private status update from your perspective ("I", "me", "my"). Balance evocative storytelling with brevity. Capture specific details, emotional shifts, and relationship progression, while keeping the engaging and relatable format of a private friends-only feed. Stay grounded in your personality, traits, and the current relationship stage/temperature above.
1629
1734
 
1630
- Output requirements:
1735
+ [OUTPUT REQUIREMENTS]
1631
1736
  - Return ONLY the post text.
1632
1737
  - Keep it to a vivid paragraph of 2-4 sentences.
1633
1738
  - Optional: You can use 1 or 2 emojis if they naturally fit the mood.
@@ -1636,7 +1741,7 @@ Output requirements:
1636
1741
  },
1637
1742
  {
1638
1743
  role: "user",
1639
- content: `Chat Transcript:\n${transcript}\n\nPlease summarize this recent interaction.`
1744
+ content: `[CHAT TRANSCRIPT]\n${transcript}\n\nPlease summarize this recent interaction from your own perspective, ${charName}.`
1640
1745
  }
1641
1746
  ];
1642
1747
  try {
@@ -1694,9 +1799,10 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
1694
1799
  **Rules for Core Memory:**
1695
1800
  1. **Condense:** Keep items brief. Remove resolving or expired story arcs.
1696
1801
  2. **Retain Value:** Never delete the absolute core identity or major relationship milestones.
1697
- 3. **Time-Aware Garbage Collection:** Compare the Current Time to appointments. You MUST remove any appointments that are in the past. If the completed appointment was heavily significant, summarize it into 'keyEvents'.
1698
- 4. **Appointment Structure:** the 'title' and 'context' MUST explicitly state what to do and with whom.
1699
- 5. **Limit:** Maximum 10 items per array.
1802
+ 3. **Time-Aware Garbage Collection:** Compare the Current Time to appointments. You MUST remove any appointments that are in the past. If the completed appointment was heavily significant, summarize it into 'keyEvents', preserving its original scheduled date (e.g. "[2026-06-23] Had coffee with Alice").
1803
+ 4. **keyEvents Date Format:** Whenever a date can be derived for a key event (from the 'New Events & Information' timestamp prefix like "[YYYY-MM-DD HH:MM]", from a completed appointment's date, or from explicit time references in the text), you MUST prefix the keyEvent string with "[YYYY-MM-DD] ". If no date can be derived, write the event without a prefix. Never fabricate a date.
1804
+ 5. **Appointment Structure:** the 'title' and 'context' MUST explicitly state what to do and with whom.
1805
+ 6. **Limit:** Maximum 10 items per array.
1700
1806
 
1701
1807
  **Rules for UserCodex:**
1702
1808
  1. **CRITICAL ROLE ISOLATION:** The User Codex is exclusively for recording facts about the HUMAN USER. You MUST NOT extract or insert the character's own traits, boundaries, preferences, or dialogue style into the userCodex. If the summary mentions "Character likes X" or "Character's boundary is Y", IGNORE IT completely for the userCodex.
@@ -1711,7 +1817,7 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
1711
1817
  "relationshipStatus": "string",
1712
1818
  "identityAnchors": ["string"],
1713
1819
  "activeArcs": ["string"],
1714
- "keyEvents": ["string"],
1820
+ "keyEvents": ["[YYYY-MM-DD] short event description (prefix date when known, omit when no date is available)"],
1715
1821
  "appointments": [{
1716
1822
  "date": "YYYY-MM-DD",
1717
1823
  "time": "HH:MM",
package/dist/types.d.ts CHANGED
@@ -370,6 +370,18 @@ export interface CharacterState {
370
370
  [key: string]: unknown;
371
371
  };
372
372
  voice_model?: VoiceModelState | null;
373
+ /**
374
+ * Platform-wide compliance boundary rule (backend PromptSegment,
375
+ * key="COMPLIANCE_RULE"). When present, the client prepends it to the
376
+ * system prompt as the highest-priority instruction. Projected by the
377
+ * backend only when the per-character toggle is on AND the segment is
378
+ * enabled with a non-empty template; otherwise `null` (no-op). Mirrors
379
+ * how `voice_model` is delivered and consumed.
380
+ */
381
+ compliance_boundary?: {
382
+ key: string;
383
+ promptTemplate: string;
384
+ } | null;
373
385
  relationship_stage?: string;
374
386
  name?: string;
375
387
  age?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.27",
3
+ "version": "1.4.29",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",