@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 +9 -1
- package/dist/client.js +84 -81
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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 ===
|
|
875
|
-
break;
|
|
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: `
|
|
892
|
+
reason: `Spam guard: ${consecutiveProactive} consecutive un-replied messages already sent.`,
|
|
885
893
|
};
|
|
886
894
|
}
|
|
887
|
-
// 2. Fetch
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
|
|
937
|
-
|
|
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":
|
|
946
|
+
"skipReason": null,
|
|
941
947
|
"actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY.",
|
|
942
948
|
"textResponse": "Spoken dialogue ONLY.",
|
|
943
|
-
"stateUpdate": { "temperatureDelta":
|
|
944
|
-
${this.getImageSchemaParams(
|
|
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
|
|
948
|
-
|
|
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[
|
|
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
|
-
//
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
|
976
|
+
reason: parsedIntent.skipReason || "Character chose not to reach out.",
|
|
969
977
|
};
|
|
970
978
|
}
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
978
|
-
parsedIntent.textResponse.
|
|
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
|
-
|
|
992
|
-
let
|
|
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