@space3-npm/cybersoul-client 1.4.23 → 1.4.25

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
@@ -83,6 +83,15 @@ export declare class CyberSoulClient {
83
83
  * reply is already in flight.
84
84
  */
85
85
  private buildMediaError;
86
+ /**
87
+ * Shared giftOutfit handler for `interact()` / `proactiveInteract()`.
88
+ * Validates the LLM's `giftOutfit` intent, performs the wardrobe write,
89
+ * fires the `onOutfitGifted` callback, and resolves to the
90
+ * [OutfitGiftedPayload] (or `undefined` when there was nothing to gift
91
+ * or the write failed). Failures are swallowed (logged) so a wardrobe
92
+ * hiccup never aborts the chat turn.
93
+ */
94
+ private processGiftOutfit;
86
95
  private formatHistoryEntries;
87
96
  private buildHistoryTranscript;
88
97
  interact(params: InteractParams): Promise<InteractResponse>;
@@ -144,8 +153,11 @@ export declare class CyberSoulClient {
144
153
  updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<PersistedDynamicContext | null>;
145
154
  /**
146
155
  * Gift a new outfit to the character's wardrobe inventory.
156
+ * Returns the number of wardrobe items the backend created (the
157
+ * backend may expand a single description into multiple items), or
158
+ * `undefined` when the server did not report a count.
147
159
  */
148
- giftOutfit(descriptionText: string): Promise<void>;
160
+ giftOutfit(descriptionText: string): Promise<number | undefined>;
149
161
  /**
150
162
  * Bootstrap character profile from OpenClaw workspace files.
151
163
  */
package/dist/client.js CHANGED
@@ -479,10 +479,14 @@ ${isProactive
479
479
  return `When generating a triggerEvent, you MUST provide a suitable 'triggerEvent.outfitId' if the VERY LAST USER MESSAGE explicitly asks for an outfit change, OR if the new activity implies a context/location shift that conflicts with the current outfit (e.g., currently in SLEEPWEAR at home but going outside). Otherwise, keep it null. When changing outfits, match it to the event's activity, environment, and relationship stage (e.g., CASUAL, COSTUME, INTIMATE, SLEEPWEAR, etc.).`;
480
480
  }
481
481
  getTriggerEventPolicyPrompt() {
482
- return `- Include 'triggerEvent' only if the VERY LAST USER MESSAGE proposes a new activity/hangout AND you accept the invitation, explicitly requests an outfit change AND you agree, or proposes intimate/romantic actions AND you agree; ignore older history. DO NOT include it if you decline or reject the proposal. ${this.getOutfitSelectionPrompt()}`;
482
+ return `- Include 'triggerEvent' only if the VERY LAST USER MESSAGE proposes a new activity/hangout AND you accept the invitation, explicitly requests an outfit change AND you agree, or proposes intimate/romantic actions AND you agree; ignore older history. DO NOT include it if you decline or reject the proposal.
483
+ REPETITION GATE (hard): Prior assistant turns that already auto-triggered an event are tagged with a [Triggered Event: ...] marker in '[CHAT HISTORY]'. If such a marker already exists for the SAME activity the VERY LAST USER MESSAGE is referring to (e.g. it is just acknowledging, hurrying, confirming, or continuing an already-accepted outing), set 'triggerEvent' to null. Only emit a NEW 'triggerEvent' when the user proposes a genuinely DIFFERENT activity that has not already been triggered. Do NOT re-trigger the same event just because the conversation continues. ${this.getOutfitSelectionPrompt()}`;
483
484
  }
484
485
  getOutfitAcquisitionPolicyPrompt() {
485
- return `- Outfit acquisition (VERY LAST USER MESSAGE only): set giftOutfit for gift/buy/add-clothes intent; otherwise null. giftOutfit format: { "descriptionText": "short outfit description" }.`;
486
+ return `- Outfit acquisition (giftOutfit): set 'giftOutfit' to { "descriptionText": "short outfit description" } when a genuinely NEW outfit (one that is NOT already in the Available Wardrobe) is obtained THIS turn, triggered by EITHER:
487
+ (a) USER-GIFTED: the VERY LAST USER MESSAGE expresses gift/buy/add-clothes intent for you (e.g. "I bought you a dress", "here, wear this new outfit", "adding some lingerie to your closet").
488
+ (b) CHARACTER-ACQUIRED: the conversation or active event naturally leads YOU to acquire a new outfit you don't already own (e.g. you went shopping, received/made clothes, or the scene requires changing into a brand-new outfit that is absent from your Available Wardrobe).
489
+ Keep 'descriptionText' to a concise English-or-matching-language description of the single new outfit. Otherwise set 'giftOutfit' to null. Do NOT fire it for outfits already present in the Available Wardrobe, and do NOT fire it just because you changed into an existing outfit.`;
486
490
  }
487
491
  getEventSchemaParams(userName) {
488
492
  const name = userName || "the user";
@@ -607,6 +611,45 @@ ${isProactive
607
611
  affected,
608
612
  };
609
613
  }
614
+ /**
615
+ * Shared giftOutfit handler for `interact()` / `proactiveInteract()`.
616
+ * Validates the LLM's `giftOutfit` intent, performs the wardrobe write,
617
+ * fires the `onOutfitGifted` callback, and resolves to the
618
+ * [OutfitGiftedPayload] (or `undefined` when there was nothing to gift
619
+ * or the write failed). Failures are swallowed (logged) so a wardrobe
620
+ * hiccup never aborts the chat turn.
621
+ */
622
+ async processGiftOutfit(giftOutfitIntent, onOutfitGifted) {
623
+ if (!giftOutfitIntent ||
624
+ typeof giftOutfitIntent !== "object" ||
625
+ typeof giftOutfitIntent.descriptionText !== "string" ||
626
+ giftOutfitIntent.descriptionText.trim().length === 0) {
627
+ return undefined;
628
+ }
629
+ const outfitDescription = giftOutfitIntent.descriptionText.trim();
630
+ try {
631
+ const count = await this.giftOutfit(outfitDescription);
632
+ const giftedOutfit = {
633
+ descriptionText: outfitDescription,
634
+ };
635
+ if (typeof count === "number") {
636
+ giftedOutfit.count = count;
637
+ }
638
+ if (onOutfitGifted) {
639
+ try {
640
+ onOutfitGifted(giftedOutfit);
641
+ }
642
+ catch (cbErr) {
643
+ console.warn("[CyberSoulClient] onOutfitGifted callback threw:", cbErr);
644
+ }
645
+ }
646
+ return giftedOutfit;
647
+ }
648
+ catch (e) {
649
+ console.error("[CyberSoulClient] giftOutfit failed:", e);
650
+ return undefined;
651
+ }
652
+ }
610
653
  formatHistoryEntries(history, userName, agentName, promptDirective = "") {
611
654
  const contextLines = [];
612
655
  for (let i = 0; i < history.length; i++) {
@@ -623,7 +666,8 @@ ${isProactive
623
666
  const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
624
667
  const action = msg.actionText ? ` (${msg.actionText})` : "";
625
668
  const media = msg.mediaHint ? ` [${msg.mediaHint}]` : "";
626
- contextLines.push(`${speaker}:${action} ${content}${media}`);
669
+ const event = msg.eventHint ? ` [Triggered Event: ${msg.eventHint}]` : "";
670
+ contextLines.push(`${speaker}:${action} ${content}${media}${event}`);
627
671
  }
628
672
  return contextLines.join('\n');
629
673
  }
@@ -846,6 +890,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
846
890
  let finalAudioUrl = undefined;
847
891
  let finalAudioMediaId = undefined;
848
892
  let finalDurationSec = undefined;
893
+ let giftedOutfit = undefined;
849
894
  // Partial-failure capture: text was already produced and emitted
850
895
  // via [onTextReady], so a wallet / insufficient-points failure on
851
896
  // image or voice MUST NOT abort the whole turn. We collect the
@@ -886,7 +931,10 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
886
931
  typeof parsedIntent.giftOutfit === "object" &&
887
932
  typeof parsedIntent.giftOutfit.descriptionText === "string" &&
888
933
  parsedIntent.giftOutfit.descriptionText.trim().length > 0) {
889
- mediaTasks.push(this.giftOutfit(parsedIntent.giftOutfit.descriptionText.trim()).catch((e) => console.error("[CyberSoulClient] Auto giftOutfit failed:", e)));
934
+ mediaTasks.push(this.processGiftOutfit(parsedIntent.giftOutfit, params.onOutfitGifted).then((result) => {
935
+ if (result)
936
+ giftedOutfit = result;
937
+ }));
890
938
  }
891
939
  const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) &&
892
940
  (!isAuto || !!parsedIntent.imageParams);
@@ -989,6 +1037,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
989
1037
  isEndTurn: parsedIntent.isEndTurn,
990
1038
  persistedDynamicContext,
991
1039
  mediaError,
1040
+ giftedOutfit,
992
1041
  };
993
1042
  }
994
1043
  catch (error) {
@@ -1190,6 +1239,7 @@ Modalities:
1190
1239
  ? "'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."
1191
1240
  : "ALWAYS set 'imageParams' to null."}
1192
1241
  - ALWAYS set 'voiceArgs' to null.
1242
+ ${this.getOutfitAcquisitionPolicyPrompt()}
1193
1243
 
1194
1244
  Output ONLY a valid JSON object matching exactly this structure (no markdown wrappers).
1195
1245
  If "shouldSkipProactive" is true, set "skipReason" to one short sentence and set every other field to null.
@@ -1200,6 +1250,7 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1200
1250
  "actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY.",
1201
1251
  "textResponse": "Spoken dialogue ONLY.",
1202
1252
  "stateUpdate": { "temperatureDelta": 0, "ongoingScene": { "scene": "...", "outfit": "..." } },
1253
+ "giftOutfit": { "descriptionText": "Concise description of the newly acquired outfit to add into wardrobe." },
1203
1254
  ${this.getImageSchemaParams(imageAllowed)},
1204
1255
  "voiceArgs": null
1205
1256
  }`;
@@ -1265,6 +1316,11 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1265
1316
  stateUpdate: parsedIntent.stateUpdate,
1266
1317
  });
1267
1318
  }
1319
+ // Outfit acquisition: the character may decide, on its own, to pick
1320
+ // up a brand-new outfit while reaching out. Fire-and-capture in
1321
+ // parallel with image generation; surface it via callback + the
1322
+ // final response so upstream can show "New outfit added".
1323
+ const giftOutfitPromise = this.processGiftOutfit(parsedIntent.giftOutfit, params.onOutfitGifted);
1268
1324
  let finalImageUrl;
1269
1325
  let finalImageMediaId;
1270
1326
  let proactiveMediaError = null;
@@ -1300,6 +1356,7 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1300
1356
  }
1301
1357
  }
1302
1358
  const persistedDynamicContext = (await persistedStatePromise) ?? undefined;
1359
+ const giftedOutfit = (await giftOutfitPromise) ?? undefined;
1303
1360
  const proactiveMediaErrorEnv = proactiveMediaError
1304
1361
  ? this.buildMediaError(proactiveMediaError, proactiveAffected)
1305
1362
  : undefined;
@@ -1312,6 +1369,7 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1312
1369
  stateUpdate: parsedIntent.stateUpdate,
1313
1370
  persistedDynamicContext,
1314
1371
  mediaError: proactiveMediaErrorEnv,
1372
+ giftedOutfit,
1315
1373
  };
1316
1374
  }
1317
1375
  catch (error) {
@@ -1435,6 +1493,9 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1435
1493
  }
1436
1494
  /**
1437
1495
  * Gift a new outfit to the character's wardrobe inventory.
1496
+ * Returns the number of wardrobe items the backend created (the
1497
+ * backend may expand a single description into multiple items), or
1498
+ * `undefined` when the server did not report a count.
1438
1499
  */
1439
1500
  async giftOutfit(descriptionText) {
1440
1501
  const res = await this.apiFetch("/api/v1/cyber-soul/characters/gift-outfit", {
@@ -1443,6 +1504,18 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1443
1504
  });
1444
1505
  if (!res.ok)
1445
1506
  throw new Error("Failed to gift outfit");
1507
+ try {
1508
+ const body = (await res.json());
1509
+ return typeof body.count === "number" && Number.isFinite(body.count)
1510
+ ? body.count
1511
+ : undefined;
1512
+ }
1513
+ catch {
1514
+ // The gift already succeeded server-side (res.ok); a missing/
1515
+ // unparseable count is non-fatal — report "unknown" rather than
1516
+ // fabricating a number.
1517
+ return undefined;
1518
+ }
1446
1519
  }
1447
1520
  /**
1448
1521
  * Bootstrap character profile from OpenClaw workspace files.
package/dist/types.d.ts CHANGED
@@ -31,6 +31,14 @@ export interface HistoryEntry {
31
31
  content: string;
32
32
  actionText?: string;
33
33
  mediaHint?: string;
34
+ /**
35
+ * Marker for assistant turns that already auto-triggered an event
36
+ * (e.g. an outing/hangout the character accepted). Surfaced in the
37
+ * transcript as a `[Triggered Event: ...]` tag so the dispatcher's
38
+ * trigger-event repetition gate can avoid re-triggering the same
39
+ * activity on later turns. Mirrors how `mediaHint` tags past media.
40
+ */
41
+ eventHint?: string;
34
42
  isProactive?: boolean;
35
43
  timestamp?: string | number | Date;
36
44
  }
@@ -80,6 +88,26 @@ export interface MediaReadyPayload {
80
88
  /** Voice only — TTS duration in seconds when known. */
81
89
  durationSec?: number;
82
90
  }
91
+ /**
92
+ * Payload delivered by [InteractParams.onOutfitGifted] /
93
+ * [ProactiveParams.onOutfitGifted] when a new outfit is successfully
94
+ * added to the character's wardrobe during a turn. Fires for BOTH
95
+ * trigger paths: (a) the user explicitly gifts/buys an outfit, and
96
+ * (b) the conversation or an active event leads the character to
97
+ * acquire a brand-new outfit. Lets upstream consumers (e.g.
98
+ * cybersoul-chat) render a system message like
99
+ * "New outfit added to wardrobe".
100
+ */
101
+ export interface OutfitGiftedPayload {
102
+ /** Human-readable description of the newly acquired outfit. */
103
+ descriptionText: string;
104
+ /**
105
+ * Number of wardrobe items the backend created for this gift, when
106
+ * the server reported it. Omitted when the count is unknown — never
107
+ * fabricated.
108
+ */
109
+ count?: number;
110
+ }
83
111
  export interface ProactiveParams {
84
112
  history?: HistoryEntry[];
85
113
  maxUnreplied?: number;
@@ -95,6 +123,13 @@ export interface ProactiveParams {
95
123
  onStateReady?: (persisted: PersistedDynamicContext) => void;
96
124
  /** Fires per modality as each media task settles successfully. */
97
125
  onMediaReady?: (payload: MediaReadyPayload) => void;
126
+ /**
127
+ * Fires when an outfit has been successfully added to the wardrobe
128
+ * during this turn (user-initiated gift OR character-initiated
129
+ * acquisition). Lets the UI render a system message like
130
+ * "New outfit added to wardrobe" in real time.
131
+ */
132
+ onOutfitGifted?: (payload: OutfitGiftedPayload) => void;
98
133
  }
99
134
  export interface ProactiveResponse {
100
135
  status: "success" | "skipped" | "error";
@@ -113,6 +148,10 @@ export interface ProactiveResponse {
113
148
  * can still render the text reply and explain the missing media
114
149
  * without losing the conversation. See [InteractMediaError]. */
115
150
  mediaError?: InteractMediaError;
151
+ /** Set when an outfit was successfully added to the wardrobe this turn.
152
+ * Mirrors the [ProactiveParams.onOutfitGifted] callback for consumers
153
+ * that only read the final response. See [OutfitGiftedPayload]. */
154
+ giftedOutfit?: OutfitGiftedPayload;
116
155
  error?: string;
117
156
  }
118
157
  export interface InteractParams {
@@ -132,6 +171,13 @@ export interface InteractParams {
132
171
  onStateReady?: (persisted: PersistedDynamicContext) => void;
133
172
  /** Fires per modality as each media task settles successfully. */
134
173
  onMediaReady?: (payload: MediaReadyPayload) => void;
174
+ /**
175
+ * Fires when an outfit has been successfully added to the wardrobe
176
+ * during this turn (user-initiated gift OR character-initiated
177
+ * acquisition). Lets the UI render a system message like
178
+ * "New outfit added to wardrobe" in real time.
179
+ */
180
+ onOutfitGifted?: (payload: OutfitGiftedPayload) => void;
135
181
  }
136
182
  export interface OndemandEventParams {
137
183
  eventDescription: string;
@@ -173,6 +219,10 @@ export interface InteractResponse {
173
219
  imageMediaId?: string;
174
220
  audioUrl?: string;
175
221
  audioMediaId?: string;
222
+ /** Set when an outfit was successfully added to the wardrobe this turn.
223
+ * Mirrors the [InteractParams.onOutfitGifted] callback for consumers
224
+ * that only read the final response. See [OutfitGiftedPayload]. */
225
+ giftedOutfit?: OutfitGiftedPayload;
176
226
  likePreviousPicture?: boolean;
177
227
  durationSec?: number;
178
228
  triggeredEvent?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.23",
3
+ "version": "1.4.25",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",