@space3-npm/cybersoul-client 1.2.3 → 1.2.5

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.js CHANGED
@@ -142,15 +142,22 @@ export class CyberSoulClient {
142
142
  }).catch((e) => console.error("Failed to update dynamic context", e)); // non-blocking error handler
143
143
  }
144
144
  normalizeRequestTypes(requestTypes) {
145
- if (!requestTypes || requestTypes.length === 0) {
146
- return [InteractRequestType.AUTO];
145
+ let normalized = requestTypes;
146
+ if (!normalized || normalized.length === 0) {
147
+ normalized = [InteractRequestType.AUTO, InteractRequestType.TEXT];
148
+ }
149
+ else {
150
+ normalized = [...normalized];
151
+ }
152
+ if (!normalized.includes(InteractRequestType.TEXT)) {
153
+ normalized.push(InteractRequestType.TEXT);
147
154
  }
148
155
  const validRequestTypes = new Set(Object.values(InteractRequestType));
149
- const invalidRequestTypes = requestTypes.filter((type) => !validRequestTypes.has(type));
156
+ const invalidRequestTypes = normalized.filter((type) => !validRequestTypes.has(type));
150
157
  if (invalidRequestTypes.length > 0) {
151
158
  throw new Error(`Invalid requestTypes: ${invalidRequestTypes.join(", ")}. Allowed values: ${Object.values(InteractRequestType).join(", ")}`);
152
159
  }
153
- return requestTypes;
160
+ return normalized;
154
161
  }
155
162
  buildStateContextPrompt(state, localContext) {
156
163
  const dyn = state.dynamic_context || {};
@@ -178,7 +185,7 @@ Current time: ${new Date(currentTimeMs).toLocaleString("zh-CN", { timeZone: "Asi
178
185
  let isOutdated = false;
179
186
  if (dyn.lastInteractionAt) {
180
187
  const elapsedHours = (currentTimeMs - new Date(dyn.lastInteractionAt).getTime()) / (1000 * 60 * 60);
181
- if (elapsedHours > 2) {
188
+ if (elapsedHours > 1) {
182
189
  isOutdated = true;
183
190
  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!`);
184
191
  }
@@ -190,15 +197,15 @@ Current time: ${new Date(currentTimeMs).toLocaleString("zh-CN", { timeZone: "Asi
190
197
  if (state.active_event) {
191
198
  contextParts.push(`Active Event: ${state.active_event.title} (${state.active_event.narrative_context})`);
192
199
  }
193
- if (state.next_event) {
200
+ /* if (localContext) {
201
+ contextParts.push(`Additional Context: ${localContext}`);
202
+ }
203
+ */ if (state.next_event) {
194
204
  contextParts.push(`Next Event: ${state.next_event.title} at ${state.next_event.start_time} (in ${state.next_event.time_until_mins} mins)`);
195
205
  }
196
206
  if (state.active_wardrobe) {
197
207
  contextParts.push(`Wardrobe: ${state.active_wardrobe.name || state.active_wardrobe.id || "Current"}`);
198
208
  }
199
- if (localContext) {
200
- contextParts.push(`Additional Context: ${localContext}`);
201
- }
202
209
  if (state.core_memory) {
203
210
  let memoryLines = ["[CORE MEMORY]"];
204
211
  const mem = state.core_memory;
@@ -226,7 +233,8 @@ Occupation: ${basicInfo?.occupation || "Unknown"}
226
233
  Age/Gender: ${basicInfo?.age || "Unknown"} / ${basicInfo?.gender || "Unknown"}
227
234
  Comm Style: ${psychological?.communicationStyle || "Unknown"}
228
235
  Hobbies: ${(psychological?.hobbies || []).join(", ") || "Unknown"}
229
- Traits/Boundaries: ${(psychological?.traits || []).join(", ") || "Unknown"} / ${(psychological?.boundaries || []).join(", ") || "Unknown"}`);
236
+ Traits/Boundaries: ${(psychological?.traits || []).join(", ") || "Unknown"} / ${(psychological?.boundaries || []).join(", ") || "Unknown"}
237
+ Preferences/Habits: ${(psychological?.preferences || []).join(", ") || "Unknown"}`);
230
238
  // CURIOSITY DRIVE: Find what's missing, but ONLY IF we are on generally warm speaking terms
231
239
  // Paradox avoidance: A cold/angry character shouldn't enthusiastically fish for hobbies.
232
240
  if (temperature >= 40 && stage !== "COLD" && stage !== "STRANGER") {
@@ -291,7 +299,9 @@ ${scenarioContext}
291
299
  }
292
300
  return undefined;
293
301
  }
294
- getImageSchemaParams() {
302
+ getImageSchemaParams(allowed) {
303
+ if (!allowed)
304
+ return `"imageParams": null`;
295
305
  return `"imageParams": {
296
306
  "mode": "structured | full-prompt (use 'full-prompt' for highly dynamic actions)",
297
307
  "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).",
@@ -334,7 +344,9 @@ ${scenarioContext}
334
344
  * Returns the JSON schema snippet for voiceArgs to embed in the LLM output schema.
335
345
  * Built from dynamic_params when available, otherwise falls back to static defaults.
336
346
  */
337
- getVoiceSchemaFromState(state) {
347
+ getVoiceSchemaFromState(state, allowed) {
348
+ if (!allowed)
349
+ return `"voiceArgs": null`;
338
350
  const dynamicParams = state.voice_model?.dynamic_params;
339
351
  if (dynamicParams && dynamicParams.length > 0) {
340
352
  return this.buildVoiceSchemaFromDynamicParams(dynamicParams);
@@ -390,6 +402,43 @@ ${scenarioContext}
390
402
  // 2. Build local Prompt
391
403
  const types = this.normalizeRequestTypes(params.requestTypes);
392
404
  const isAuto = types.includes(InteractRequestType.AUTO);
405
+ const requestedOthers = types.filter((t) => t !== InteractRequestType.AUTO && t !== InteractRequestType.TEXT);
406
+ let modalitiesInstruction = "";
407
+ if (isAuto) {
408
+ modalitiesInstruction = `Analyze the user's message and optionally decide to use allowed modalities: ${requestedOthers.join(", ") || "none"}.
409
+ - 'textResponse' is ALWAYS REQUIRED.
410
+ - The modalities you are ALLOWED to dynamically include: ${requestedOthers.length > 0 ? requestedOthers.join(", ") : "None (Only text is allowed)"}. Do not include other modalities.`;
411
+ if (requestedOthers.includes(InteractRequestType.IMAGE)) {
412
+ modalitiesInstruction += `\n - Include 'imageParams' for visual/photo requests or key visual moments during active events; explicitly describe current clothing/exposure in image fields.`;
413
+ }
414
+ else {
415
+ modalitiesInstruction += `\n - ALWAYS set 'imageParams' to null. If the user explicitly asks for a picture, FIRMLY decline naturally in your 'textResponse' (e.g., say you absolutely cannot right now). NEVER pretend to send one, and NEVER give in no matter how many times they ask.`;
416
+ }
417
+ if (requestedOthers.includes(InteractRequestType.VOICE)) {
418
+ modalitiesInstruction += `\n - Include 'voiceArgs' ONLY if the complicated tone/emotion is hard to express via pure text, or if the user explicitly requests to hear your voice. Otherwise, set it to null.`;
419
+ }
420
+ else {
421
+ modalitiesInstruction += `\n - ALWAYS set 'voiceArgs' to null.`;
422
+ }
423
+ }
424
+ else {
425
+ modalitiesInstruction = `You MUST return the requested modalities: ${requestedOthers.join(", ") || "only text"}.
426
+ - 'textResponse' is ALWAYS REQUIRED.`;
427
+ if (requestedOthers.includes(InteractRequestType.IMAGE)) {
428
+ modalitiesInstruction += `\n - 'imageParams' is REQUIRED. Include it and explicitly describe current clothing/exposure in image fields.`;
429
+ }
430
+ else {
431
+ modalitiesInstruction += `\n - ALWAYS set 'imageParams' to null. If the user explicitly asks for a picture, FIRMLY decline naturally in your 'textResponse' (e.g., say you absolutely cannot right now). NEVER pretend to send one, and NEVER give in no matter how many times they ask.`;
432
+ }
433
+ if (requestedOthers.includes(InteractRequestType.VOICE)) {
434
+ modalitiesInstruction += `\n - 'voiceArgs' is REQUIRED. Include it.`;
435
+ }
436
+ else {
437
+ modalitiesInstruction += `\n - ALWAYS set 'voiceArgs' to null.`;
438
+ }
439
+ }
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" }.`;
393
442
  // Combine state info into a clean descriptive context
394
443
  const systemPrompt = `${this.buildStateContextPrompt(state, params.localContext)}
395
444
  Available Wardrobe Outfits (For event triggers):
@@ -397,14 +446,7 @@ ${availableOutfits}
397
446
 
398
447
  The user has sent a message. You must evaluate the context and the user's message, and return a JSON object (no markdown formatting) that dictates the character's multi-modal response.
399
448
 
400
- ${isAuto
401
- ? `Analyze the user's message and decide response modalities (text, image, voice).
402
- - Always include 'textResponse'.
403
- - Include 'imageParams' for visual/photo requests or key visual moments during active events; explicitly describe current clothing/exposure in image fields.
404
- - Include 'voiceArgs' ONLY if the complicated tone/emotion is hard to express via pure text, or if the user explicitly requests to hear your voice. Otherwise, set it to null.
405
- - Include 'triggerEvent' only if the VERY LAST USER MESSAGE proposes a new activity/hangout; ignore older history.
406
- - Outfit acquisition (VERY LAST USER MESSAGE only): set giftOutfit for gift/buy/add-clothes intent; otherwise null. giftOutfit format: { "descriptionText": "short outfit description" }.`
407
- : `Requested types to fulfill: ${types.join(", ")}`}
449
+ ${modalitiesInstruction}
408
450
  Every turn adjusts trust: positive +1, negative -1, neutral 0. Always include 'stateUpdate' with integer 'temperatureDelta' (range guidance: 0 cold to 100 obsessive).
409
451
 
410
452
  Always return 'stateUpdate.ongoingScene' as an object with both keys: { "scene": string, "outfit": string }.
@@ -413,7 +455,9 @@ For 'ongoingScene.outfit': decide based on the current active wardrobe by defaul
413
455
  USER ANALYSIS WORKFLOW:
414
456
  - Extract from VERY LAST USER MESSAGE only.
415
457
  - Add only explicit new user facts from this turn (no inference).
416
- - Categories: 'realName', 'occupation', 'age', 'gender', 'hobby', 'trait', 'communicationStyle', 'boundary'.
458
+ - For 'preference', only capture explicit statements (e.g., "I like/love/dislike/hate...").
459
+ - For 'boundary', only capture explicit rejections or limitations (e.g., "Don't talk about X", "I won't do Y").
460
+ - Categories: 'realName', 'occupation', 'age', 'gender', 'hobby', 'trait', 'communicationStyle', 'boundary', 'preference'.
417
461
  - Keep nicknames in stateUpdate; do not place them in newFactsLearned.
418
462
  - If no new fact is explicit, set userAnalysis to null.
419
463
 
@@ -427,13 +471,13 @@ Output JSON Schema:
427
471
  "textResponse": "Spoken dialogue ONLY. Never include actions or parentheses.",
428
472
  "stateUpdate": { "temperatureDelta": 1, "userNickname": "How character addresses user", "agentNickname": "How user addresses character", "talkingStyle": "Current speaking style", "ongoingScene": { "scene": "Current physical scene/activity", "outfit": "Current outfit wording; use 'naked' when applicable" } },
429
473
  "giftOutfit": { "descriptionText": "Concise description of the newly acquired outfit to add into wardrobe." },
430
- "userAnalysis": { "newFactsLearned": [{ "category": "realName|occupation|age|gender|hobby|trait|communicationStyle|boundary", "value": "explicit new user fact from VERY LAST USER MESSAGE" }] },
474
+ "userAnalysis": { "newFactsLearned": [{ "category": "realName|occupation|age|gender|hobby|trait|communicationStyle|boundary|preference", "value": "explicit new user fact from VERY LAST USER MESSAGE" }] },
431
475
  "isEndTurn": false,
432
476
  "triggerEvent": {
433
477
  ${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
434
478
  },
435
- ${this.getImageSchemaParams()},
436
- ${this.getVoiceSchemaFromState(state)}
479
+ ${this.getImageSchemaParams(requestedOthers.includes(InteractRequestType.IMAGE))},
480
+ ${this.getVoiceSchemaFromState(state, requestedOthers.includes(InteractRequestType.VOICE))}
437
481
  }
438
482
  Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent", "giftOutfit", or "userAnalysis" are not needed, set them to null. "stateUpdate" cannot be null. Return valid raw JSON only.`;
439
483
  const transcript = this.buildHistoryTranscript(params.history, state);
@@ -452,7 +496,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
452
496
  // console.debug("[CyberSoulClient] Raw LLM Response:", rawLlmResponse);
453
497
  let parsedIntent;
454
498
  try {
455
- parsedIntent = robustJsonParse(rawLlmResponse, "Dispatcher fallback");
499
+ parsedIntent = robustJsonParse(rawLlmResponse, "Dispatcher fallback", { textResponse: "", actionText: "", isEndTurn: false });
456
500
  }
457
501
  catch (e) {
458
502
  console.warn("[CyberSoulClient] JSON parse failed, falling back to raw text:", e);
@@ -499,8 +543,8 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
499
543
  parsedIntent.giftOutfit.descriptionText.trim().length > 0) {
500
544
  mediaTasks.push(this.giftOutfit(parsedIntent.giftOutfit.descriptionText.trim()).catch((e) => console.error("[CyberSoulClient] Auto giftOutfit failed:", e)));
501
545
  }
502
- const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) ||
503
- (isAuto && !!parsedIntent.imageParams);
546
+ const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) &&
547
+ (!isAuto || !!parsedIntent.imageParams);
504
548
  if (shouldGenerateImage) {
505
549
  const imagePayload = parsedIntent.imageParams && typeof parsedIntent.imageParams === "object"
506
550
  ? parsedIntent.imageParams
@@ -512,8 +556,8 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
512
556
  finalImageUrl = res.image_url;
513
557
  }).catch(e => console.error("[CyberSoulClient] Image generation failed:", e)));
514
558
  }
515
- const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) ||
516
- (isAuto && !!parsedIntent.voiceArgs);
559
+ const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) &&
560
+ (!isAuto || !!parsedIntent.voiceArgs);
517
561
  if (shouldGenerateVoice) {
518
562
  const normalizedVoiceArgs = parsedIntent.voiceArgs && typeof parsedIntent.voiceArgs === "object"
519
563
  ? parsedIntent.voiceArgs
@@ -654,7 +698,7 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
654
698
  You are an AI image prompt director. Analyze the scene description according to the character's relationship stage and emotional inertia to determine the best image generation parameters.
655
699
  Output strictly valid JSON ONLY. No markdown, no conversational filler. Return exactly matching this schema:
656
700
  {
657
- ${this.getImageSchemaParams()}
701
+ ${this.getImageSchemaParams(true)}
658
702
  }`;
659
703
  const transcript = this.buildHistoryTranscript(params.interactParams?.history, state);
660
704
  const promptMessages = [
@@ -689,7 +733,7 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
689
733
  You are a voice acting director. ${this.getVoiceDirectorInstruction(state)}
690
734
  Output strictly valid JSON ONLY. No markdown, no conversational filler. Return exactly matching this schema:
691
735
  {
692
- ${this.getVoiceSchemaFromState(state)}
736
+ ${this.getVoiceSchemaFromState(state, true)}
693
737
  }`;
694
738
  const transcript = this.buildHistoryTranscript(params.interactParams?.history, state);
695
739
  const promptMessages = [
@@ -842,6 +886,7 @@ Output requirements:
842
886
  traits: [],
843
887
  communicationStyle: "",
844
888
  boundaries: [],
889
+ preferences: [],
845
890
  }
846
891
  };
847
892
  const systemPrompt = `You are an AI Memory Consolidation Engine for a virtual companion.
@@ -855,7 +900,7 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
855
900
  5. **Limit:** Maximum 10 items per array.
856
901
 
857
902
  **Rules for UserCodex:**
858
- 1. **Deduplicate & Consolidate:** Remove duplicate hobbies, traits, and boundaries. Combine related points into concise descriptors.
903
+ 1. **Deduplicate & Consolidate:** Remove duplicate hobbies, traits, boundaries, and preferences. Combine related points into concise descriptors.
859
904
  2. **Update Facts:** If the new events contain updated basic info (like new realName, different occupation), update it. Otherwise keep the existing info.
860
905
  3. **Keep it Clean:** Maximum 15 items per array.
861
906
 
@@ -885,7 +930,8 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
885
930
  "hobbies": ["string"],
886
931
  "traits": ["string"],
887
932
  "communicationStyle": "string",
888
- "boundaries": ["string"]
933
+ "boundaries": ["string"],
934
+ "preferences": ["string"]
889
935
  }
890
936
  }
891
937
  }
package/dist/types.d.ts CHANGED
@@ -82,7 +82,7 @@ export interface DispatcherIntent {
82
82
  } | null;
83
83
  userAnalysis?: {
84
84
  newFactsLearned: {
85
- category: "realName" | "occupation" | "age" | "gender" | "hobby" | "trait" | "communicationStyle" | "boundary";
85
+ category: "realName" | "occupation" | "age" | "gender" | "hobby" | "trait" | "communicationStyle" | "boundary" | "preference";
86
86
  value: string;
87
87
  }[];
88
88
  };
@@ -129,6 +129,7 @@ export interface UserCodex {
129
129
  traits: string[];
130
130
  communicationStyle: string;
131
131
  boundaries: string[];
132
+ preferences?: string[];
132
133
  };
133
134
  familiarityScore?: number;
134
135
  }
@@ -1 +1 @@
1
- export declare function robustJsonParse<T>(jsonString: string, contextMessage?: string): T;
1
+ export declare function robustJsonParse<T>(jsonString: string, contextMessage?: string, fallbackTemplate?: Record<string, any>): T;
@@ -1,4 +1,4 @@
1
- export function robustJsonParse(jsonString, contextMessage = 'throwing original error') {
1
+ export function robustJsonParse(jsonString, contextMessage = 'throwing original error', fallbackTemplate) {
2
2
  let cleanJson = jsonString.trim();
3
3
  // 0. Inject missing colons between string keys and string values (e.g. "key""value" -> "key":"value")
4
4
  // Only insert the colon if we match a likely key (alphanumeric/hyphen) followed by quotes, handling smart quotes.
@@ -119,6 +119,50 @@ export function robustJsonParse(jsonString, contextMessage = 'throwing original
119
119
  }
120
120
  }
121
121
  }
122
+ // FINAL FALLBACK: Regex extraction of requested fields if fallbackTemplate is provided
123
+ if (fallbackTemplate) {
124
+ console.warn(`[robustJsonParse] Regex fallback using template for: ${contextMessage}`);
125
+ const extractedObj = { ...fallbackTemplate };
126
+ let extractedAny = false;
127
+ for (const key of Object.keys(fallbackTemplate)) {
128
+ // 1. Try to extract string values handling escaped characters like \" and \n
129
+ const stringMatch = cleanJson.match(new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`));
130
+ if (stringMatch) {
131
+ try {
132
+ extractedObj[key] = JSON.parse(`"${stringMatch[1]}"`);
133
+ }
134
+ catch (err) {
135
+ extractedObj[key] = stringMatch[1];
136
+ }
137
+ extractedAny = true;
138
+ continue;
139
+ }
140
+ // 2. Try to extract booleans, numbers, or null
141
+ const primitiveMatch = cleanJson.match(new RegExp(`"${key}"\\s*:\\s*([a-zA-Z0-9_.-]+)`));
142
+ if (primitiveMatch) {
143
+ const val = primitiveMatch[1];
144
+ if (val === 'true') {
145
+ extractedObj[key] = true;
146
+ extractedAny = true;
147
+ }
148
+ else if (val === 'false') {
149
+ extractedObj[key] = false;
150
+ extractedAny = true;
151
+ }
152
+ else if (val === 'null') {
153
+ extractedObj[key] = null;
154
+ extractedAny = true;
155
+ }
156
+ else if (!isNaN(Number(val))) {
157
+ extractedObj[key] = Number(val);
158
+ extractedAny = true;
159
+ }
160
+ }
161
+ }
162
+ if (extractedAny) {
163
+ return extractedObj;
164
+ }
165
+ }
122
166
  console.warn(`Failed to parse Dispatcher Intent: ${contextMessage}. Falling back to plain text.`);
123
167
  throw e;
124
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",