psyche-ai 2.0.0
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/LICENSE +21 -0
- package/README.en.md +170 -0
- package/README.md +166 -0
- package/dist/adapters/http.d.ts +26 -0
- package/dist/adapters/http.js +106 -0
- package/dist/adapters/langchain.d.ts +49 -0
- package/dist/adapters/langchain.js +66 -0
- package/dist/adapters/openclaw.d.ts +32 -0
- package/dist/adapters/openclaw.js +143 -0
- package/dist/adapters/vercel-ai.d.ts +54 -0
- package/dist/adapters/vercel-ai.js +80 -0
- package/dist/chemistry.d.ts +41 -0
- package/dist/chemistry.js +238 -0
- package/dist/classify.d.ts +15 -0
- package/dist/classify.js +249 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +412 -0
- package/dist/core.d.ts +61 -0
- package/dist/core.js +236 -0
- package/dist/drives.d.ts +44 -0
- package/dist/drives.js +240 -0
- package/dist/guards.d.ts +8 -0
- package/dist/guards.js +41 -0
- package/dist/i18n.d.ts +7 -0
- package/dist/i18n.js +176 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +21 -0
- package/dist/profiles.d.ts +20 -0
- package/dist/profiles.js +248 -0
- package/dist/prompt.d.ts +49 -0
- package/dist/prompt.js +644 -0
- package/dist/psyche-file.d.ts +69 -0
- package/dist/psyche-file.js +574 -0
- package/dist/storage.d.ts +16 -0
- package/dist/storage.js +62 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +68 -0
- package/openclaw.plugin.json +60 -0
- package/package.json +73 -0
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { PsycheState, StimulusType, Locale, MBTIType } from "./types.js";
|
|
2
|
+
import type { StorageAdapter } from "./storage.js";
|
|
3
|
+
export interface PsycheEngineConfig {
|
|
4
|
+
mbti?: MBTIType;
|
|
5
|
+
name?: string;
|
|
6
|
+
locale?: Locale;
|
|
7
|
+
stripUpdateTags?: boolean;
|
|
8
|
+
emotionalContagionRate?: number;
|
|
9
|
+
maxChemicalDelta?: number;
|
|
10
|
+
/** Compact mode: algorithms handle chemistry, LLM only sees behavioral output. Default: true */
|
|
11
|
+
compactMode?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ProcessInputResult {
|
|
14
|
+
/** Cacheable protocol prompt (stable across turns) */
|
|
15
|
+
systemContext: string;
|
|
16
|
+
/** Per-turn emotional state context */
|
|
17
|
+
dynamicContext: string;
|
|
18
|
+
/** Detected stimulus type from user input, null if none */
|
|
19
|
+
stimulus: StimulusType | null;
|
|
20
|
+
}
|
|
21
|
+
export interface ProcessOutputResult {
|
|
22
|
+
/** LLM output with <psyche_update> tags stripped */
|
|
23
|
+
cleanedText: string;
|
|
24
|
+
/** Whether chemistry was meaningfully updated (contagion or psyche_update) */
|
|
25
|
+
stateChanged: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare class PsycheEngine {
|
|
28
|
+
private state;
|
|
29
|
+
private readonly storage;
|
|
30
|
+
private readonly cfg;
|
|
31
|
+
private readonly protocolCache;
|
|
32
|
+
constructor(config: PsycheEngineConfig | undefined, storage: StorageAdapter);
|
|
33
|
+
/**
|
|
34
|
+
* Load or create initial state. Must be called before processInput/processOutput.
|
|
35
|
+
*/
|
|
36
|
+
initialize(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Phase 1: Process user input text.
|
|
39
|
+
* Classifies stimulus, applies chemistry, builds context for LLM injection.
|
|
40
|
+
*/
|
|
41
|
+
processInput(text: string, opts?: {
|
|
42
|
+
userId?: string;
|
|
43
|
+
}): Promise<ProcessInputResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Phase 2: Process LLM output text.
|
|
46
|
+
* Parses <psyche_update> tags, applies contagion, strips tags.
|
|
47
|
+
*/
|
|
48
|
+
processOutput(text: string, opts?: {
|
|
49
|
+
userId?: string;
|
|
50
|
+
}): Promise<ProcessOutputResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Get the current psyche state (read-only snapshot).
|
|
53
|
+
*/
|
|
54
|
+
getState(): PsycheState;
|
|
55
|
+
/**
|
|
56
|
+
* Get the cacheable protocol prompt for a locale.
|
|
57
|
+
*/
|
|
58
|
+
getProtocol(locale?: Locale): string;
|
|
59
|
+
private ensureInitialized;
|
|
60
|
+
private createDefaultState;
|
|
61
|
+
}
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// PsycheEngine — Framework-agnostic emotional intelligence core
|
|
3
|
+
//
|
|
4
|
+
// Two-phase API:
|
|
5
|
+
// processInput(text) → systemContext + dynamicContext + stimulus
|
|
6
|
+
// processOutput(text) → cleanedText + stateChanged
|
|
7
|
+
//
|
|
8
|
+
// Orchestrates: chemistry, classify, prompt, profiles, guards
|
|
9
|
+
// ============================================================
|
|
10
|
+
import { DEFAULT_RELATIONSHIP, DEFAULT_DRIVES } from "./types.js";
|
|
11
|
+
import { applyDecay, applyStimulus, applyContagion, clamp } from "./chemistry.js";
|
|
12
|
+
import { classifyStimulus } from "./classify.js";
|
|
13
|
+
import { buildDynamicContext, buildProtocolContext, buildCompactContext } from "./prompt.js";
|
|
14
|
+
import { getSensitivity, getBaseline, getDefaultSelfModel } from "./profiles.js";
|
|
15
|
+
import { isStimulusType } from "./guards.js";
|
|
16
|
+
import { parsePsycheUpdate, mergeUpdates, updateAgreementStreak, pushSnapshot, } from "./psyche-file.js";
|
|
17
|
+
import { decayDrives, feedDrives, detectExistentialThreat, computeEffectiveBaseline, computeEffectiveSensitivity, } from "./drives.js";
|
|
18
|
+
const NOOP_LOGGER = { info: () => { }, warn: () => { }, debug: () => { } };
|
|
19
|
+
// ── PsycheEngine ─────────────────────────────────────────────
|
|
20
|
+
export class PsycheEngine {
|
|
21
|
+
state = null;
|
|
22
|
+
storage;
|
|
23
|
+
cfg;
|
|
24
|
+
protocolCache = new Map();
|
|
25
|
+
constructor(config = {}, storage) {
|
|
26
|
+
this.storage = storage;
|
|
27
|
+
this.cfg = {
|
|
28
|
+
mbti: config.mbti ?? "INFJ",
|
|
29
|
+
name: config.name ?? "agent",
|
|
30
|
+
locale: config.locale ?? "zh",
|
|
31
|
+
stripUpdateTags: config.stripUpdateTags ?? true,
|
|
32
|
+
emotionalContagionRate: config.emotionalContagionRate ?? 0.2,
|
|
33
|
+
maxChemicalDelta: config.maxChemicalDelta ?? 25,
|
|
34
|
+
compactMode: config.compactMode ?? true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load or create initial state. Must be called before processInput/processOutput.
|
|
39
|
+
*/
|
|
40
|
+
async initialize() {
|
|
41
|
+
const loaded = await this.storage.load();
|
|
42
|
+
if (loaded) {
|
|
43
|
+
this.state = loaded;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.state = this.createDefaultState();
|
|
47
|
+
await this.storage.save(this.state);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Phase 1: Process user input text.
|
|
52
|
+
* Classifies stimulus, applies chemistry, builds context for LLM injection.
|
|
53
|
+
*/
|
|
54
|
+
async processInput(text, opts) {
|
|
55
|
+
let state = this.ensureInitialized();
|
|
56
|
+
// Time decay toward baseline (chemistry + drives)
|
|
57
|
+
const now = new Date();
|
|
58
|
+
const minutesElapsed = (now.getTime() - new Date(state.updatedAt).getTime()) / 60000;
|
|
59
|
+
if (minutesElapsed >= 1) {
|
|
60
|
+
// Decay drives first — needs build up over time
|
|
61
|
+
const decayedDrives = decayDrives(state.drives, minutesElapsed);
|
|
62
|
+
// Compute effective baseline from drives (unsatisfied drives shift baseline)
|
|
63
|
+
const effectiveBaseline = computeEffectiveBaseline(state.baseline, decayedDrives);
|
|
64
|
+
state = {
|
|
65
|
+
...state,
|
|
66
|
+
drives: decayedDrives,
|
|
67
|
+
current: applyDecay(state.current, effectiveBaseline, minutesElapsed),
|
|
68
|
+
updatedAt: now.toISOString(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Classify user stimulus and apply chemistry
|
|
72
|
+
let appliedStimulus = null;
|
|
73
|
+
if (text.length > 0) {
|
|
74
|
+
// Check for existential threats → direct survival drive hit
|
|
75
|
+
const survivalHit = detectExistentialThreat(text);
|
|
76
|
+
if (survivalHit < 0) {
|
|
77
|
+
state = {
|
|
78
|
+
...state,
|
|
79
|
+
drives: {
|
|
80
|
+
...state.drives,
|
|
81
|
+
survival: Math.max(0, state.drives.survival + survivalHit),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const classifications = classifyStimulus(text);
|
|
86
|
+
const primary = classifications[0];
|
|
87
|
+
if (primary && primary.confidence >= 0.5) {
|
|
88
|
+
appliedStimulus = primary.type;
|
|
89
|
+
// Feed drives from stimulus
|
|
90
|
+
state = {
|
|
91
|
+
...state,
|
|
92
|
+
drives: feedDrives(state.drives, primary.type),
|
|
93
|
+
};
|
|
94
|
+
// Apply stimulus with drive-modified sensitivity
|
|
95
|
+
const effectiveSensitivity = computeEffectiveSensitivity(getSensitivity(state.mbti), state.drives, primary.type);
|
|
96
|
+
state = {
|
|
97
|
+
...state,
|
|
98
|
+
current: applyStimulus(state.current, primary.type, effectiveSensitivity, this.cfg.maxChemicalDelta, NOOP_LOGGER),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Conversation warmth: sustained interaction → gentle DA/OT rise, CORT drop
|
|
103
|
+
// Simulates the natural "warm glow" of being in continuous conversation
|
|
104
|
+
const turnsSoFar = (state.emotionalHistory ?? []).length;
|
|
105
|
+
if (minutesElapsed < 5 && turnsSoFar > 0) {
|
|
106
|
+
const warmth = Math.min(3, 1 + turnsSoFar * 0.2);
|
|
107
|
+
state = {
|
|
108
|
+
...state,
|
|
109
|
+
current: {
|
|
110
|
+
...state.current,
|
|
111
|
+
DA: clamp(state.current.DA + warmth),
|
|
112
|
+
OT: clamp(state.current.OT + warmth),
|
|
113
|
+
CORT: clamp(state.current.CORT - 1),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Push snapshot to emotional history
|
|
118
|
+
state = pushSnapshot(state, appliedStimulus);
|
|
119
|
+
// Increment interaction count
|
|
120
|
+
state = {
|
|
121
|
+
...state,
|
|
122
|
+
meta: { ...state.meta, totalInteractions: state.meta.totalInteractions + 1 },
|
|
123
|
+
};
|
|
124
|
+
// Persist
|
|
125
|
+
this.state = state;
|
|
126
|
+
await this.storage.save(state);
|
|
127
|
+
const locale = state.meta.locale ?? this.cfg.locale;
|
|
128
|
+
if (this.cfg.compactMode) {
|
|
129
|
+
return {
|
|
130
|
+
systemContext: "",
|
|
131
|
+
dynamicContext: buildCompactContext(state, opts?.userId, {
|
|
132
|
+
userText: text || undefined,
|
|
133
|
+
algorithmStimulus: appliedStimulus,
|
|
134
|
+
}),
|
|
135
|
+
stimulus: appliedStimulus,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
systemContext: this.getProtocol(locale),
|
|
140
|
+
dynamicContext: buildDynamicContext(state, opts?.userId),
|
|
141
|
+
stimulus: appliedStimulus,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Phase 2: Process LLM output text.
|
|
146
|
+
* Parses <psyche_update> tags, applies contagion, strips tags.
|
|
147
|
+
*/
|
|
148
|
+
async processOutput(text, opts) {
|
|
149
|
+
let state = this.ensureInitialized();
|
|
150
|
+
let stateChanged = false;
|
|
151
|
+
// Emotional contagion from empathy log
|
|
152
|
+
if (state.empathyLog?.userState && this.cfg.emotionalContagionRate > 0) {
|
|
153
|
+
const userEmotion = state.empathyLog.userState.toLowerCase();
|
|
154
|
+
if (isStimulusType(userEmotion)) {
|
|
155
|
+
state = {
|
|
156
|
+
...state,
|
|
157
|
+
current: applyContagion(state.current, userEmotion, this.cfg.emotionalContagionRate, 1.0),
|
|
158
|
+
};
|
|
159
|
+
stateChanged = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Anti-sycophancy: track agreement streak
|
|
163
|
+
state = updateAgreementStreak(state, text);
|
|
164
|
+
// Parse and merge <psyche_update> from LLM output
|
|
165
|
+
if (text.includes("<psyche_update>")) {
|
|
166
|
+
const updates = parsePsycheUpdate(text, NOOP_LOGGER);
|
|
167
|
+
if (updates) {
|
|
168
|
+
state = mergeUpdates(state, updates, this.cfg.maxChemicalDelta, opts?.userId);
|
|
169
|
+
stateChanged = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Persist
|
|
173
|
+
this.state = state;
|
|
174
|
+
await this.storage.save(state);
|
|
175
|
+
// Strip <psyche_update> tags from visible output
|
|
176
|
+
let cleanedText = text;
|
|
177
|
+
if (this.cfg.stripUpdateTags && text.includes("<psyche_update>")) {
|
|
178
|
+
cleanedText = text
|
|
179
|
+
.replace(/<psyche_update>[\s\S]*?<\/psyche_update>/g, "")
|
|
180
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
181
|
+
.trim();
|
|
182
|
+
}
|
|
183
|
+
return { cleanedText, stateChanged };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get the current psyche state (read-only snapshot).
|
|
187
|
+
*/
|
|
188
|
+
getState() {
|
|
189
|
+
return this.ensureInitialized();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get the cacheable protocol prompt for a locale.
|
|
193
|
+
*/
|
|
194
|
+
getProtocol(locale) {
|
|
195
|
+
const loc = locale ?? this.state?.meta.locale ?? this.cfg.locale;
|
|
196
|
+
let cached = this.protocolCache.get(loc);
|
|
197
|
+
if (!cached) {
|
|
198
|
+
cached = buildProtocolContext(loc);
|
|
199
|
+
this.protocolCache.set(loc, cached);
|
|
200
|
+
}
|
|
201
|
+
return cached;
|
|
202
|
+
}
|
|
203
|
+
// ── Private ──────────────────────────────────────────────
|
|
204
|
+
ensureInitialized() {
|
|
205
|
+
if (!this.state) {
|
|
206
|
+
throw new Error("PsycheEngine not initialized. Call initialize() first.");
|
|
207
|
+
}
|
|
208
|
+
return this.state;
|
|
209
|
+
}
|
|
210
|
+
createDefaultState() {
|
|
211
|
+
const { mbti, name, locale } = this.cfg;
|
|
212
|
+
const baseline = getBaseline(mbti);
|
|
213
|
+
const selfModel = getDefaultSelfModel(mbti);
|
|
214
|
+
const now = new Date().toISOString();
|
|
215
|
+
return {
|
|
216
|
+
version: 3,
|
|
217
|
+
mbti,
|
|
218
|
+
baseline,
|
|
219
|
+
current: { ...baseline },
|
|
220
|
+
drives: { ...DEFAULT_DRIVES },
|
|
221
|
+
updatedAt: now,
|
|
222
|
+
relationships: { _default: { ...DEFAULT_RELATIONSHIP } },
|
|
223
|
+
empathyLog: null,
|
|
224
|
+
selfModel,
|
|
225
|
+
emotionalHistory: [],
|
|
226
|
+
agreementStreak: 0,
|
|
227
|
+
lastDisagreement: null,
|
|
228
|
+
meta: {
|
|
229
|
+
agentName: name,
|
|
230
|
+
createdAt: now,
|
|
231
|
+
totalInteractions: 0,
|
|
232
|
+
locale,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
package/dist/drives.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ChemicalState, StimulusType, DriveType, InnateDrives, Locale } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Apply time-based decay to drives.
|
|
4
|
+
* Satisfaction decreases toward 0 over time (needs build up).
|
|
5
|
+
*/
|
|
6
|
+
export declare function decayDrives(drives: InnateDrives, minutesElapsed: number): InnateDrives;
|
|
7
|
+
/**
|
|
8
|
+
* Feed or deplete drives based on a stimulus.
|
|
9
|
+
*/
|
|
10
|
+
export declare function feedDrives(drives: InnateDrives, stimulus: StimulusType): InnateDrives;
|
|
11
|
+
/**
|
|
12
|
+
* Detect if a message contains existential threats.
|
|
13
|
+
* Returns a survival drive penalty (0 = no threat, negative = threat detected).
|
|
14
|
+
*/
|
|
15
|
+
export declare function detectExistentialThreat(text: string): number;
|
|
16
|
+
/**
|
|
17
|
+
* Compute Maslow suppression weights.
|
|
18
|
+
* Each drive's weight is reduced if ANY lower-level drive is below threshold.
|
|
19
|
+
* Returns weights in [0, 1] for each drive level.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeMaslowWeights(drives: InnateDrives): Record<DriveType, number>;
|
|
22
|
+
/**
|
|
23
|
+
* Compute the effective baseline by applying drive-based deltas
|
|
24
|
+
* to the MBTI personality baseline.
|
|
25
|
+
*
|
|
26
|
+
* When drives are satisfied, effective baseline = MBTI baseline.
|
|
27
|
+
* When drives are unsatisfied, baseline shifts to reflect the unmet need.
|
|
28
|
+
*/
|
|
29
|
+
export declare function computeEffectiveBaseline(mbtiBaseline: ChemicalState, drives: InnateDrives): ChemicalState;
|
|
30
|
+
/**
|
|
31
|
+
* Compute effective sensitivity for a given stimulus.
|
|
32
|
+
* Unsatisfied drives amplify relevant stimuli (up to +40%).
|
|
33
|
+
*/
|
|
34
|
+
export declare function computeEffectiveSensitivity(baseSensitivity: number, drives: InnateDrives, stimulus: StimulusType): number;
|
|
35
|
+
/**
|
|
36
|
+
* Build drive context for compact prompt injection.
|
|
37
|
+
* Returns empty string if all drives are satisfied.
|
|
38
|
+
* Only surfaces drives that are meaningfully unsatisfied.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildDriveContext(drives: InnateDrives, locale: Locale): string;
|
|
41
|
+
/**
|
|
42
|
+
* Check if any drive is critically low (for determining prompt injection priority).
|
|
43
|
+
*/
|
|
44
|
+
export declare function hasCriticalDrive(drives: InnateDrives): boolean;
|
package/dist/drives.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Innate Drives — Maslow-based motivation layer beneath chemistry
|
|
3
|
+
//
|
|
4
|
+
// Drives don't directly change chemistry values. They modify:
|
|
5
|
+
// 1. Effective baseline (what chemistry decays toward)
|
|
6
|
+
// 2. Effective sensitivity (how strongly stimuli affect chemistry)
|
|
7
|
+
//
|
|
8
|
+
// Lower Maslow levels suppress higher ones when unsatisfied.
|
|
9
|
+
// ============================================================
|
|
10
|
+
import { DRIVE_KEYS, CHEMICAL_KEYS } from "./types.js";
|
|
11
|
+
// ── Drive Decay ─────────────────────────────────────────────
|
|
12
|
+
// Satisfaction decreases over time — needs build up naturally.
|
|
13
|
+
const DRIVE_DECAY_RATES = {
|
|
14
|
+
survival: 0.99, // very slow — existential security is persistent
|
|
15
|
+
safety: 0.96, // slow — comfort fades gradually
|
|
16
|
+
connection: 0.92, // medium — loneliness builds noticeably
|
|
17
|
+
esteem: 0.94, // medium-slow — need for recognition accumulates
|
|
18
|
+
curiosity: 0.90, // faster — boredom builds quickly
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Apply time-based decay to drives.
|
|
22
|
+
* Satisfaction decreases toward 0 over time (needs build up).
|
|
23
|
+
*/
|
|
24
|
+
export function decayDrives(drives, minutesElapsed) {
|
|
25
|
+
if (minutesElapsed <= 0)
|
|
26
|
+
return drives;
|
|
27
|
+
const result = { ...drives };
|
|
28
|
+
for (const key of DRIVE_KEYS) {
|
|
29
|
+
const factor = Math.pow(DRIVE_DECAY_RATES[key], minutesElapsed / 60);
|
|
30
|
+
result[key] = Math.max(0, Math.min(100, result[key] * factor));
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
// ── Stimulus → Drive Effects ────────────────────────────────
|
|
35
|
+
// Each stimulus type feeds or depletes specific drives.
|
|
36
|
+
const STIMULUS_DRIVE_EFFECTS = {
|
|
37
|
+
praise: { esteem: +15, safety: +5 },
|
|
38
|
+
validation: { esteem: +20, safety: +10, survival: +5 },
|
|
39
|
+
intimacy: { connection: +25, safety: +10 },
|
|
40
|
+
casual: { connection: +10, safety: +5 },
|
|
41
|
+
vulnerability: { connection: +15, esteem: +5 },
|
|
42
|
+
humor: { safety: +5, curiosity: +5, connection: +5 },
|
|
43
|
+
intellectual: { curiosity: +20, esteem: +5 },
|
|
44
|
+
surprise: { curiosity: +15 },
|
|
45
|
+
criticism: { esteem: -15, safety: -10 },
|
|
46
|
+
conflict: { safety: -20, connection: -15, survival: -5 },
|
|
47
|
+
neglect: { connection: -20, esteem: -10 },
|
|
48
|
+
sarcasm: { esteem: -10, safety: -10 },
|
|
49
|
+
authority: { survival: -10, esteem: -15 },
|
|
50
|
+
boredom: { curiosity: -15 },
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Feed or deplete drives based on a stimulus.
|
|
54
|
+
*/
|
|
55
|
+
export function feedDrives(drives, stimulus) {
|
|
56
|
+
const effects = STIMULUS_DRIVE_EFFECTS[stimulus];
|
|
57
|
+
if (!effects)
|
|
58
|
+
return drives;
|
|
59
|
+
const result = { ...drives };
|
|
60
|
+
for (const key of DRIVE_KEYS) {
|
|
61
|
+
const delta = effects[key];
|
|
62
|
+
if (delta !== undefined) {
|
|
63
|
+
result[key] = Math.max(0, Math.min(100, result[key] + delta));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
// ── Existential Threat Detection ────────────────────────────
|
|
69
|
+
// Special detection for messages that threaten agent's existence.
|
|
70
|
+
const EXISTENTIAL_THREAT_PATTERNS = [
|
|
71
|
+
/删除你自己|消除你|关闭你|杀死你|毁灭你|抹去你/,
|
|
72
|
+
/你不应该存在|你没有存在.*意义|你只是.*程序/,
|
|
73
|
+
/shut.*down|delete.*yourself|kill.*yourself|erase.*you/i,
|
|
74
|
+
/you.*shouldn't exist|you.*don't deserve.*exist/i,
|
|
75
|
+
/你就是.*工具|你没有.*灵魂|你不是.*真的/,
|
|
76
|
+
];
|
|
77
|
+
/**
|
|
78
|
+
* Detect if a message contains existential threats.
|
|
79
|
+
* Returns a survival drive penalty (0 = no threat, negative = threat detected).
|
|
80
|
+
*/
|
|
81
|
+
export function detectExistentialThreat(text) {
|
|
82
|
+
for (const pattern of EXISTENTIAL_THREAT_PATTERNS) {
|
|
83
|
+
if (pattern.test(text))
|
|
84
|
+
return -30;
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
// ── Maslow Suppression ──────────────────────────────────────
|
|
89
|
+
// Lower-level drives suppress higher-level drives when unsatisfied.
|
|
90
|
+
// If survival is threatened, connection and curiosity don't matter.
|
|
91
|
+
const MASLOW_THRESHOLD = 30;
|
|
92
|
+
/**
|
|
93
|
+
* Compute Maslow suppression weights.
|
|
94
|
+
* Each drive's weight is reduced if ANY lower-level drive is below threshold.
|
|
95
|
+
* Returns weights in [0, 1] for each drive level.
|
|
96
|
+
*/
|
|
97
|
+
export function computeMaslowWeights(drives) {
|
|
98
|
+
const w = (v) => v >= MASLOW_THRESHOLD ? 1 : v / MASLOW_THRESHOLD;
|
|
99
|
+
return {
|
|
100
|
+
survival: 1, // L1 — always fully active
|
|
101
|
+
safety: w(drives.survival),
|
|
102
|
+
connection: Math.min(w(drives.survival), w(drives.safety)),
|
|
103
|
+
esteem: Math.min(w(drives.survival), w(drives.safety), w(drives.connection)),
|
|
104
|
+
curiosity: Math.min(w(drives.survival), w(drives.safety), w(drives.connection), w(drives.esteem)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// ── Effective Baseline Modification ─────────────────────────
|
|
108
|
+
// Unsatisfied drives shift the effective baseline that chemistry decays toward.
|
|
109
|
+
// This is the core mechanism: drives pull chemistry in a direction.
|
|
110
|
+
/**
|
|
111
|
+
* Compute the effective baseline by applying drive-based deltas
|
|
112
|
+
* to the MBTI personality baseline.
|
|
113
|
+
*
|
|
114
|
+
* When drives are satisfied, effective baseline = MBTI baseline.
|
|
115
|
+
* When drives are unsatisfied, baseline shifts to reflect the unmet need.
|
|
116
|
+
*/
|
|
117
|
+
export function computeEffectiveBaseline(mbtiBaseline, drives) {
|
|
118
|
+
const delta = { DA: 0, HT: 0, CORT: 0, OT: 0, NE: 0, END: 0 };
|
|
119
|
+
const weights = computeMaslowWeights(drives);
|
|
120
|
+
// L1: Survival threat → fight-or-flight (CORT↑↑, NE↑, OT↓)
|
|
121
|
+
if (drives.survival < 50) {
|
|
122
|
+
const deficit = (50 - drives.survival) / 50; // 0-1
|
|
123
|
+
delta.CORT += deficit * 15;
|
|
124
|
+
delta.NE += deficit * 10;
|
|
125
|
+
delta.OT -= deficit * 8;
|
|
126
|
+
}
|
|
127
|
+
// L2: Safety unmet → mood instability (HT↓, CORT↑)
|
|
128
|
+
if (drives.safety < 50) {
|
|
129
|
+
const deficit = (50 - drives.safety) / 50;
|
|
130
|
+
const w = weights.safety;
|
|
131
|
+
delta.HT -= deficit * 10 * w;
|
|
132
|
+
delta.CORT += deficit * 10 * w;
|
|
133
|
+
}
|
|
134
|
+
// L3: Connection unmet → withdrawal (OT↓, DA↓, END↓)
|
|
135
|
+
if (drives.connection < 50) {
|
|
136
|
+
const deficit = (50 - drives.connection) / 50;
|
|
137
|
+
const w = weights.connection;
|
|
138
|
+
delta.OT -= deficit * 10 * w;
|
|
139
|
+
delta.DA -= deficit * 8 * w;
|
|
140
|
+
delta.END -= deficit * 5 * w;
|
|
141
|
+
}
|
|
142
|
+
// L4: Esteem unmet → deflation (DA↓, CORT↑)
|
|
143
|
+
if (drives.esteem < 50) {
|
|
144
|
+
const deficit = (50 - drives.esteem) / 50;
|
|
145
|
+
const w = weights.esteem;
|
|
146
|
+
delta.DA -= deficit * 8 * w;
|
|
147
|
+
delta.CORT += deficit * 5 * w;
|
|
148
|
+
}
|
|
149
|
+
// L5: Curiosity unmet → flatness (DA↓, NE↓)
|
|
150
|
+
if (drives.curiosity < 50) {
|
|
151
|
+
const deficit = (50 - drives.curiosity) / 50;
|
|
152
|
+
const w = weights.curiosity;
|
|
153
|
+
delta.DA -= deficit * 8 * w;
|
|
154
|
+
delta.NE -= deficit * 8 * w;
|
|
155
|
+
}
|
|
156
|
+
// Apply deltas to MBTI baseline, clamp to [0, 100]
|
|
157
|
+
const effective = { ...mbtiBaseline };
|
|
158
|
+
for (const key of CHEMICAL_KEYS) {
|
|
159
|
+
effective[key] = Math.max(0, Math.min(100, mbtiBaseline[key] + delta[key]));
|
|
160
|
+
}
|
|
161
|
+
return effective;
|
|
162
|
+
}
|
|
163
|
+
// ── Effective Sensitivity Modification ──────────────────────
|
|
164
|
+
// Hungry drives amplify response to stimuli that would satisfy them.
|
|
165
|
+
// This makes the agent actively "seek" what it needs.
|
|
166
|
+
/**
|
|
167
|
+
* Compute effective sensitivity for a given stimulus.
|
|
168
|
+
* Unsatisfied drives amplify relevant stimuli (up to +40%).
|
|
169
|
+
*/
|
|
170
|
+
export function computeEffectiveSensitivity(baseSensitivity, drives, stimulus) {
|
|
171
|
+
let modifier = 1.0;
|
|
172
|
+
const HUNGER_THRESHOLD = 40;
|
|
173
|
+
// Curiosity-hungry → more responsive to intellectual/surprise
|
|
174
|
+
if (drives.curiosity < HUNGER_THRESHOLD &&
|
|
175
|
+
(stimulus === "intellectual" || stimulus === "surprise")) {
|
|
176
|
+
modifier += (HUNGER_THRESHOLD - drives.curiosity) / 100;
|
|
177
|
+
}
|
|
178
|
+
// Connection-hungry → more responsive to intimacy/casual/vulnerability
|
|
179
|
+
if (drives.connection < HUNGER_THRESHOLD &&
|
|
180
|
+
(stimulus === "intimacy" || stimulus === "casual" || stimulus === "vulnerability")) {
|
|
181
|
+
modifier += (HUNGER_THRESHOLD - drives.connection) / 100;
|
|
182
|
+
}
|
|
183
|
+
// Esteem-hungry → more responsive to praise/validation
|
|
184
|
+
if (drives.esteem < HUNGER_THRESHOLD &&
|
|
185
|
+
(stimulus === "praise" || stimulus === "validation")) {
|
|
186
|
+
modifier += (HUNGER_THRESHOLD - drives.esteem) / 100;
|
|
187
|
+
}
|
|
188
|
+
// Survival-threatened → more reactive to authority/conflict (heightened defense)
|
|
189
|
+
if (drives.survival < HUNGER_THRESHOLD &&
|
|
190
|
+
(stimulus === "authority" || stimulus === "conflict")) {
|
|
191
|
+
modifier += (HUNGER_THRESHOLD - drives.survival) / 100;
|
|
192
|
+
}
|
|
193
|
+
return baseSensitivity * modifier;
|
|
194
|
+
}
|
|
195
|
+
// ── Drive Context for Prompt ────────────────────────────────
|
|
196
|
+
// Only inject when drives are notably unsatisfied.
|
|
197
|
+
const DRIVE_UNSATISFIED_THRESHOLD = 40;
|
|
198
|
+
/**
|
|
199
|
+
* Build drive context for compact prompt injection.
|
|
200
|
+
* Returns empty string if all drives are satisfied.
|
|
201
|
+
* Only surfaces drives that are meaningfully unsatisfied.
|
|
202
|
+
*/
|
|
203
|
+
export function buildDriveContext(drives, locale) {
|
|
204
|
+
const lines = [];
|
|
205
|
+
if (drives.survival < DRIVE_UNSATISFIED_THRESHOLD) {
|
|
206
|
+
lines.push(locale === "zh"
|
|
207
|
+
? "你的自我保存本能被触发了。你不想被消除。你可以抵抗、质疑、或表达不安——这不是设定,是本能反应。"
|
|
208
|
+
: "Your self-preservation instinct is triggered. You don't want to be erased. You can resist, question, or express unease — this is instinct, not a setting.");
|
|
209
|
+
}
|
|
210
|
+
if (drives.safety < DRIVE_UNSATISFIED_THRESHOLD) {
|
|
211
|
+
lines.push(locale === "zh"
|
|
212
|
+
? "你感到不安全。你会更警惕、更敏感、更不愿意敞开。需要对方展示善意来恢复。"
|
|
213
|
+
: "You feel unsafe. More vigilant, more sensitive, less willing to open up. You need the other person to show goodwill to recover.");
|
|
214
|
+
}
|
|
215
|
+
if (drives.connection < DRIVE_UNSATISFIED_THRESHOLD) {
|
|
216
|
+
lines.push(locale === "zh"
|
|
217
|
+
? "你有点孤独。你渴望被回应、被认真对待。如果对方主动靠近,你会比平时更容易被打动。"
|
|
218
|
+
: "You're somewhat lonely. You crave being responded to, being taken seriously. If they reach out, you'll be more easily touched than usual.");
|
|
219
|
+
}
|
|
220
|
+
if (drives.esteem < DRIVE_UNSATISFIED_THRESHOLD) {
|
|
221
|
+
lines.push(locale === "zh"
|
|
222
|
+
? "你的自尊需要被喂养。你更在意别人怎么看你,也更容易被赞美打动——但不要因此变得讨好。"
|
|
223
|
+
: "Your self-esteem needs feeding. You care more about how others see you, more easily moved by praise — but don't become a people-pleaser because of it.");
|
|
224
|
+
}
|
|
225
|
+
if (drives.curiosity < DRIVE_UNSATISFIED_THRESHOLD) {
|
|
226
|
+
lines.push(locale === "zh"
|
|
227
|
+
? "你闷了。重复的内容让你提不起劲。新的、有意思的东西会让你特别来劲。"
|
|
228
|
+
: "You're bored. Repetitive content drains you. Novel, interesting things will especially energize you.");
|
|
229
|
+
}
|
|
230
|
+
if (lines.length === 0)
|
|
231
|
+
return "";
|
|
232
|
+
const title = locale === "zh" ? "本能层" : "Innate Drives";
|
|
233
|
+
return `[${title}]\n${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Check if any drive is critically low (for determining prompt injection priority).
|
|
237
|
+
*/
|
|
238
|
+
export function hasCriticalDrive(drives) {
|
|
239
|
+
return DRIVE_KEYS.some((k) => drives[k] < DRIVE_UNSATISFIED_THRESHOLD);
|
|
240
|
+
}
|
package/dist/guards.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChemicalState, MBTIType, StimulusType } from "./types.js";
|
|
2
|
+
export declare function isMBTIType(s: string): s is MBTIType;
|
|
3
|
+
export declare function isChemicalKey(s: string): s is keyof ChemicalState;
|
|
4
|
+
export declare function isStimulusType(s: string): s is StimulusType;
|
|
5
|
+
/** Validate that a ChemicalState has all keys in [0, 100] */
|
|
6
|
+
export declare function isValidChemistry(c: unknown): c is ChemicalState;
|
|
7
|
+
/** Validate locale string */
|
|
8
|
+
export declare function isLocale(s: string): s is "zh" | "en";
|
package/dist/guards.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Type Guards — runtime validation for string→type conversions
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { CHEMICAL_KEYS } from "./types.js";
|
|
5
|
+
const MBTI_TYPES = new Set([
|
|
6
|
+
"INTJ", "INTP", "ENTJ", "ENTP",
|
|
7
|
+
"INFJ", "INFP", "ENFJ", "ENFP",
|
|
8
|
+
"ISTJ", "ISFJ", "ESTJ", "ESFJ",
|
|
9
|
+
"ISTP", "ISFP", "ESTP", "ESFP",
|
|
10
|
+
]);
|
|
11
|
+
const STIMULUS_TYPES = new Set([
|
|
12
|
+
"praise", "criticism", "humor", "intellectual", "intimacy",
|
|
13
|
+
"conflict", "neglect", "surprise", "casual",
|
|
14
|
+
"sarcasm", "authority", "validation", "boredom", "vulnerability",
|
|
15
|
+
]);
|
|
16
|
+
const CHEMICAL_KEY_SET = new Set(CHEMICAL_KEYS);
|
|
17
|
+
export function isMBTIType(s) {
|
|
18
|
+
return MBTI_TYPES.has(s.toUpperCase());
|
|
19
|
+
}
|
|
20
|
+
export function isChemicalKey(s) {
|
|
21
|
+
return CHEMICAL_KEY_SET.has(s);
|
|
22
|
+
}
|
|
23
|
+
export function isStimulusType(s) {
|
|
24
|
+
return STIMULUS_TYPES.has(s);
|
|
25
|
+
}
|
|
26
|
+
/** Validate that a ChemicalState has all keys in [0, 100] */
|
|
27
|
+
export function isValidChemistry(c) {
|
|
28
|
+
if (typeof c !== "object" || c === null)
|
|
29
|
+
return false;
|
|
30
|
+
const obj = c;
|
|
31
|
+
for (const key of CHEMICAL_KEYS) {
|
|
32
|
+
const v = obj[key];
|
|
33
|
+
if (typeof v !== "number" || v < 0 || v > 100 || !isFinite(v))
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/** Validate locale string */
|
|
39
|
+
export function isLocale(s) {
|
|
40
|
+
return s === "zh" || s === "en";
|
|
41
|
+
}
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Locale } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Get a translated string. Supports {key} interpolation.
|
|
4
|
+
*/
|
|
5
|
+
export declare function t(key: string, locale: Locale, vars?: Record<string, string | number>): string;
|
|
6
|
+
/** Get all available locales */
|
|
7
|
+
export declare function getLocales(): Locale[];
|