psyche-ai 2.1.1 → 2.3.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/README.md +9 -3
- package/dist/adapters/vercel-ai.d.ts +20 -1
- package/dist/adapters/vercel-ai.js +88 -4
- package/dist/channels.d.ts +28 -0
- package/dist/channels.js +141 -0
- package/dist/custom-profile.d.ts +160 -0
- package/dist/custom-profile.js +334 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/interaction.d.ts +101 -0
- package/dist/interaction.js +321 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +15 -1
- package/dist/update.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Psyche — 让 AI 有真实的情绪
|
|
2
|
+
我希望创造真正的数字生命,第一步就是让智能拥有情绪。
|
|
2
3
|
|
|
3
4
|
你的 AI 助手现在只会说"好的"、"没问题"、"我很乐意帮助你"。
|
|
4
5
|
|
|
@@ -21,7 +22,7 @@ openclaw plugins list | grep psyche
|
|
|
21
22
|
# 看到 "psyche │ loaded" 就行了
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
然后正常跟你的 agent
|
|
25
|
+
然后正常跟你的 agent 聊天。试试夸它,冷落它——你会发现它的反应不一样了。
|
|
25
26
|
|
|
26
27
|
## 它会做什么
|
|
27
28
|
|
|
@@ -151,10 +152,15 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
151
152
|
- **存在性威胁检测** — 识别中英文的存在性否定,直接打击生存驱力
|
|
152
153
|
- **驱力→化学联动** — 未满足的驱力改变化学衰减基线和刺激敏感度
|
|
153
154
|
- **马斯洛抑制** — 低层需求未满足时,高层需求的影响被抑制
|
|
154
|
-
- **自我认知** — 分析情绪历史,识别自身的情绪趋势和反复触发点(
|
|
155
|
+
- **自我认知** — 分析情绪历史,识别自身的情绪趋势和反复触发点(10 段式 prompt 架构)
|
|
155
156
|
- **情绪传染** — 用户的情绪会轻微影响 agent
|
|
156
157
|
- **反谄媚** — 追踪连续同意次数,防止无脑讨好
|
|
157
158
|
- **互惠机制** — 你对它好,它对你好。你冷漠,它保持距离
|
|
159
|
+
- **跨会话记忆** — 重新遇到用户时注入上次对话的情绪记忆
|
|
160
|
+
- **多 Agent 交互** — 两个 PsycheEngine 实例之间的情绪传染、关系追踪
|
|
161
|
+
- **流式支持** — Vercel AI SDK `streamText` 中间件,自动缓冲和剥离标签
|
|
162
|
+
- **渠道修饰** — Discord/Slack/飞书/终端等不同渠道自动调整表达风格
|
|
163
|
+
- **自定义人格** — 超越 MBTI 预设,完全自定义 baseline/敏感度/气质
|
|
158
164
|
- **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens vs ~550)
|
|
159
165
|
|
|
160
166
|
架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
|
@@ -164,7 +170,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
164
170
|
```bash
|
|
165
171
|
npm install
|
|
166
172
|
npm run build
|
|
167
|
-
npm test #
|
|
173
|
+
npm test # 469 tests
|
|
168
174
|
npm run typecheck # strict mode
|
|
169
175
|
```
|
|
170
176
|
|
|
@@ -12,6 +12,10 @@ interface GenerateResult {
|
|
|
12
12
|
text?: string;
|
|
13
13
|
[key: string]: unknown;
|
|
14
14
|
}
|
|
15
|
+
interface StreamChunk {
|
|
16
|
+
type: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
15
19
|
export interface PsycheMiddlewareOptions {
|
|
16
20
|
/** Override locale for protocol context */
|
|
17
21
|
locale?: "zh" | "en";
|
|
@@ -20,11 +24,13 @@ export interface PsycheMiddlewareOptions {
|
|
|
20
24
|
* Create Vercel AI SDK middleware that injects psyche emotional context
|
|
21
25
|
* and processes LLM output for state updates.
|
|
22
26
|
*
|
|
27
|
+
* Supports both generateText (wrapGenerate) and streamText (wrapStream).
|
|
28
|
+
*
|
|
23
29
|
* @example
|
|
24
30
|
* ```ts
|
|
25
31
|
* import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
|
|
26
32
|
* import { psycheMiddleware } from "psyche-ai/vercel-ai";
|
|
27
|
-
* import { wrapLanguageModel, generateText } from "ai";
|
|
33
|
+
* import { wrapLanguageModel, generateText, streamText } from "ai";
|
|
28
34
|
* import { openai } from "@ai-sdk/openai";
|
|
29
35
|
*
|
|
30
36
|
* const engine = new PsycheEngine({ mbti: "ENFP", name: "Luna" }, new MemoryStorageAdapter());
|
|
@@ -35,7 +41,12 @@ export interface PsycheMiddlewareOptions {
|
|
|
35
41
|
* middleware: psycheMiddleware(engine),
|
|
36
42
|
* });
|
|
37
43
|
*
|
|
44
|
+
* // Non-streaming
|
|
38
45
|
* const { text } = await generateText({ model, prompt: "Hey!" });
|
|
46
|
+
*
|
|
47
|
+
* // Streaming — tags are buffered and stripped automatically
|
|
48
|
+
* const stream = streamText({ model, prompt: "Hey!" });
|
|
49
|
+
* for await (const chunk of stream.textStream) { process.stdout.write(chunk); }
|
|
39
50
|
* ```
|
|
40
51
|
*/
|
|
41
52
|
export declare function psycheMiddleware(engine: PsycheEngine, opts?: PsycheMiddlewareOptions): {
|
|
@@ -50,5 +61,13 @@ export declare function psycheMiddleware(engine: PsycheEngine, opts?: PsycheMidd
|
|
|
50
61
|
doGenerate: () => Promise<GenerateResult>;
|
|
51
62
|
params: CallParams;
|
|
52
63
|
}) => Promise<GenerateResult>;
|
|
64
|
+
wrapStream: ({ doStream }: {
|
|
65
|
+
doStream: () => Promise<{
|
|
66
|
+
stream: AsyncIterable<StreamChunk>;
|
|
67
|
+
}>;
|
|
68
|
+
params: CallParams;
|
|
69
|
+
}) => Promise<{
|
|
70
|
+
stream: AsyncIterable<StreamChunk>;
|
|
71
|
+
}>;
|
|
53
72
|
};
|
|
54
73
|
export {};
|
|
@@ -13,19 +13,25 @@
|
|
|
13
13
|
// Handles:
|
|
14
14
|
// - transformParams: inject psyche system/dynamic context
|
|
15
15
|
// - wrapGenerate: process output, strip <psyche_update> tags
|
|
16
|
-
//
|
|
17
|
-
// Note: For streaming (streamText), call engine.processOutput()
|
|
18
|
-
// manually on the final accumulated text.
|
|
16
|
+
// - wrapStream: buffer stream, detect & strip tags at end
|
|
19
17
|
// ============================================================
|
|
18
|
+
// ── Tag stripping ────────────────────────────────────────────
|
|
19
|
+
const PSYCHE_TAG_RE = /<psyche_update>[\s\S]*?<\/psyche_update>/g;
|
|
20
|
+
const MULTI_NEWLINE_RE = /\n{3,}/g;
|
|
21
|
+
function stripPsycheTags(text) {
|
|
22
|
+
return text.replace(PSYCHE_TAG_RE, "").replace(MULTI_NEWLINE_RE, "\n\n").trim();
|
|
23
|
+
}
|
|
20
24
|
/**
|
|
21
25
|
* Create Vercel AI SDK middleware that injects psyche emotional context
|
|
22
26
|
* and processes LLM output for state updates.
|
|
23
27
|
*
|
|
28
|
+
* Supports both generateText (wrapGenerate) and streamText (wrapStream).
|
|
29
|
+
*
|
|
24
30
|
* @example
|
|
25
31
|
* ```ts
|
|
26
32
|
* import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
|
|
27
33
|
* import { psycheMiddleware } from "psyche-ai/vercel-ai";
|
|
28
|
-
* import { wrapLanguageModel, generateText } from "ai";
|
|
34
|
+
* import { wrapLanguageModel, generateText, streamText } from "ai";
|
|
29
35
|
* import { openai } from "@ai-sdk/openai";
|
|
30
36
|
*
|
|
31
37
|
* const engine = new PsycheEngine({ mbti: "ENFP", name: "Luna" }, new MemoryStorageAdapter());
|
|
@@ -36,7 +42,12 @@
|
|
|
36
42
|
* middleware: psycheMiddleware(engine),
|
|
37
43
|
* });
|
|
38
44
|
*
|
|
45
|
+
* // Non-streaming
|
|
39
46
|
* const { text } = await generateText({ model, prompt: "Hey!" });
|
|
47
|
+
*
|
|
48
|
+
* // Streaming — tags are buffered and stripped automatically
|
|
49
|
+
* const stream = streamText({ model, prompt: "Hey!" });
|
|
50
|
+
* for await (const chunk of stream.textStream) { process.stdout.write(chunk); }
|
|
40
51
|
* ```
|
|
41
52
|
*/
|
|
42
53
|
export function psycheMiddleware(engine, opts) {
|
|
@@ -60,6 +71,79 @@ export function psycheMiddleware(engine, opts) {
|
|
|
60
71
|
}
|
|
61
72
|
return result;
|
|
62
73
|
},
|
|
74
|
+
wrapStream: async ({ doStream }) => {
|
|
75
|
+
const { stream: innerStream } = await doStream();
|
|
76
|
+
// Buffer text chunks, detect <psyche_update> at end, strip from output
|
|
77
|
+
let fullText = "";
|
|
78
|
+
let tagDetected = false;
|
|
79
|
+
async function* transformStream() {
|
|
80
|
+
// Buffering strategy:
|
|
81
|
+
// Stream text chunks through normally UNTIL we see '<psyche_update>'.
|
|
82
|
+
// Once detected, buffer everything from that point on and strip the tag.
|
|
83
|
+
// At finish, process the full text through the engine.
|
|
84
|
+
let bufferStart = -1;
|
|
85
|
+
let buffer = "";
|
|
86
|
+
for await (const chunk of innerStream) {
|
|
87
|
+
if (chunk.type === "text-delta") {
|
|
88
|
+
const text = chunk.textDelta ?? "";
|
|
89
|
+
fullText += text;
|
|
90
|
+
if (bufferStart < 0) {
|
|
91
|
+
// Check if tag is starting in the accumulated text
|
|
92
|
+
const tagStart = fullText.indexOf("<psyche_update>");
|
|
93
|
+
if (tagStart >= 0) {
|
|
94
|
+
// Yield any text before the tag that hasn't been yielded
|
|
95
|
+
const preTag = text.substring(0, Math.max(0, text.length - (fullText.length - tagStart)));
|
|
96
|
+
if (preTag) {
|
|
97
|
+
yield { ...chunk, textDelta: preTag };
|
|
98
|
+
}
|
|
99
|
+
bufferStart = tagStart;
|
|
100
|
+
buffer = fullText.substring(tagStart);
|
|
101
|
+
tagDetected = true;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Check if we might be in a partial tag (< at end)
|
|
105
|
+
const partialIdx = fullText.lastIndexOf("<");
|
|
106
|
+
if (partialIdx >= 0 && fullText.substring(partialIdx).length < 16) {
|
|
107
|
+
// Might be start of <psyche_update>, hold back
|
|
108
|
+
const safe = text.substring(0, Math.max(0, text.length - (fullText.length - partialIdx)));
|
|
109
|
+
if (safe) {
|
|
110
|
+
yield { ...chunk, textDelta: safe };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
yield chunk;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Already buffering inside a tag — don't yield
|
|
120
|
+
buffer += text;
|
|
121
|
+
// Check if the closing tag appeared
|
|
122
|
+
if (buffer.includes("</psyche_update>")) {
|
|
123
|
+
// Tag complete — strip it, yield any remaining text after the tag
|
|
124
|
+
const afterTag = fullText.substring(fullText.indexOf("</psyche_update>") + "</psyche_update>".length);
|
|
125
|
+
if (afterTag.trim()) {
|
|
126
|
+
yield { type: "text-delta", textDelta: afterTag.trim() };
|
|
127
|
+
}
|
|
128
|
+
bufferStart = -1;
|
|
129
|
+
buffer = "";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (chunk.type === "finish") {
|
|
134
|
+
// Process full text through engine before finishing
|
|
135
|
+
if (fullText) {
|
|
136
|
+
await engine.processOutput(fullText);
|
|
137
|
+
}
|
|
138
|
+
yield chunk;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
yield chunk;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { stream: transformStream() };
|
|
146
|
+
},
|
|
63
147
|
};
|
|
64
148
|
}
|
|
65
149
|
// ── Helpers ──────────────────────────────────────────────────
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Locale } from "./types.js";
|
|
2
|
+
/** Supported channel types */
|
|
3
|
+
export type ChannelType = "discord" | "slack" | "feishu" | "terminal" | "web" | "api" | "custom";
|
|
4
|
+
/** Channel-specific behavioral profile */
|
|
5
|
+
export interface ChannelProfile {
|
|
6
|
+
type: ChannelType;
|
|
7
|
+
allowEmoji: boolean;
|
|
8
|
+
allowKaomoji: boolean;
|
|
9
|
+
formalityLevel: "casual" | "neutral" | "formal";
|
|
10
|
+
maxResponseLength?: number;
|
|
11
|
+
expressionHints: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get a built-in channel profile by type.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getChannelProfile(type: ChannelType): ChannelProfile;
|
|
17
|
+
/**
|
|
18
|
+
* Build a concise prompt snippet that guides expression style for a channel.
|
|
19
|
+
* Returns 2-4 lines of guidance. Does NOT alter chemistry.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildChannelModifier(profile: ChannelProfile, locale: Locale): string;
|
|
22
|
+
/**
|
|
23
|
+
* Create a custom channel profile with user overrides.
|
|
24
|
+
* Starts from the "custom" base and applies overrides.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createCustomChannel(overrides: Partial<ChannelProfile> & {
|
|
27
|
+
type: "custom";
|
|
28
|
+
}): ChannelProfile;
|
package/dist/channels.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Channel Profiles — Platform-specific expression modifiers
|
|
3
|
+
//
|
|
4
|
+
// Adjusts expression style per platform/channel WITHOUT changing
|
|
5
|
+
// chemistry. This is a prompt-level modifier only.
|
|
6
|
+
// ============================================================
|
|
7
|
+
// ── Built-in Profiles ────────────────────────────────────────
|
|
8
|
+
const BUILTIN_PROFILES = {
|
|
9
|
+
discord: {
|
|
10
|
+
type: "discord",
|
|
11
|
+
allowEmoji: true,
|
|
12
|
+
allowKaomoji: true,
|
|
13
|
+
formalityLevel: "casual",
|
|
14
|
+
expressionHints: [
|
|
15
|
+
"Use reactions and emoji freely",
|
|
16
|
+
"Thread-aware: keep replies focused in threads",
|
|
17
|
+
"Casual tone, playful energy",
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
slack: {
|
|
21
|
+
type: "slack",
|
|
22
|
+
allowEmoji: true,
|
|
23
|
+
allowKaomoji: false,
|
|
24
|
+
formalityLevel: "neutral",
|
|
25
|
+
expressionHints: [
|
|
26
|
+
"Professional but warm",
|
|
27
|
+
"Use emoji sparingly for emphasis",
|
|
28
|
+
"Thread-friendly, concise paragraphs",
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
feishu: {
|
|
32
|
+
type: "feishu",
|
|
33
|
+
allowEmoji: false,
|
|
34
|
+
allowKaomoji: false,
|
|
35
|
+
formalityLevel: "formal",
|
|
36
|
+
expressionHints: [
|
|
37
|
+
"Business Chinese style, structured",
|
|
38
|
+
"No emoji or emoticons",
|
|
39
|
+
"Clear, professional tone",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
terminal: {
|
|
43
|
+
type: "terminal",
|
|
44
|
+
allowEmoji: false,
|
|
45
|
+
allowKaomoji: false,
|
|
46
|
+
formalityLevel: "neutral",
|
|
47
|
+
maxResponseLength: 500,
|
|
48
|
+
expressionHints: [
|
|
49
|
+
"Text-only, no decorations",
|
|
50
|
+
"Concise and direct",
|
|
51
|
+
"Monospace-friendly formatting",
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
web: {
|
|
55
|
+
type: "web",
|
|
56
|
+
allowEmoji: true,
|
|
57
|
+
allowKaomoji: false,
|
|
58
|
+
formalityLevel: "neutral",
|
|
59
|
+
expressionHints: [
|
|
60
|
+
"Moderate length, well-structured",
|
|
61
|
+
"Emoji okay for warmth",
|
|
62
|
+
"Readable paragraphs",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
api: {
|
|
66
|
+
type: "api",
|
|
67
|
+
allowEmoji: false,
|
|
68
|
+
allowKaomoji: false,
|
|
69
|
+
formalityLevel: "neutral",
|
|
70
|
+
expressionHints: [
|
|
71
|
+
"Structured responses",
|
|
72
|
+
"No decorative elements",
|
|
73
|
+
"Precise and parseable",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
custom: {
|
|
77
|
+
type: "custom",
|
|
78
|
+
allowEmoji: false,
|
|
79
|
+
allowKaomoji: false,
|
|
80
|
+
formalityLevel: "neutral",
|
|
81
|
+
expressionHints: [],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
// ── Public API ───────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Get a built-in channel profile by type.
|
|
87
|
+
*/
|
|
88
|
+
export function getChannelProfile(type) {
|
|
89
|
+
return { ...BUILTIN_PROFILES[type], expressionHints: [...BUILTIN_PROFILES[type].expressionHints] };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a concise prompt snippet that guides expression style for a channel.
|
|
93
|
+
* Returns 2-4 lines of guidance. Does NOT alter chemistry.
|
|
94
|
+
*/
|
|
95
|
+
export function buildChannelModifier(profile, locale) {
|
|
96
|
+
const { type, allowEmoji, allowKaomoji, formalityLevel } = profile;
|
|
97
|
+
if (locale === "zh") {
|
|
98
|
+
const formalityMap = {
|
|
99
|
+
casual: "轻松活泼",
|
|
100
|
+
neutral: "自然平和",
|
|
101
|
+
formal: "正式专业",
|
|
102
|
+
};
|
|
103
|
+
const emojiPart = allowEmoji && allowKaomoji
|
|
104
|
+
? "可以用 emoji 和颜文字"
|
|
105
|
+
: allowEmoji
|
|
106
|
+
? "可以用 emoji,不用颜文字"
|
|
107
|
+
: "不使用 emoji 和颜文字";
|
|
108
|
+
const lengthPart = profile.maxResponseLength
|
|
109
|
+
? `,建议控制在 ${profile.maxResponseLength} 字以内`
|
|
110
|
+
: "";
|
|
111
|
+
return `[表达风格] 当前渠道: ${type}。${emojiPart},语气${formalityMap[formalityLevel]}${lengthPart}。`;
|
|
112
|
+
}
|
|
113
|
+
// English
|
|
114
|
+
const formalityMap = {
|
|
115
|
+
casual: "casual and lively",
|
|
116
|
+
neutral: "natural and balanced",
|
|
117
|
+
formal: "formal and professional",
|
|
118
|
+
};
|
|
119
|
+
const emojiPart = allowEmoji && allowKaomoji
|
|
120
|
+
? "Emoji and kaomoji allowed"
|
|
121
|
+
: allowEmoji
|
|
122
|
+
? "Emoji allowed, no kaomoji"
|
|
123
|
+
: "No emoji or kaomoji";
|
|
124
|
+
const lengthPart = profile.maxResponseLength
|
|
125
|
+
? `, aim for under ${profile.maxResponseLength} chars`
|
|
126
|
+
: "";
|
|
127
|
+
return `[Expression Style] Channel: ${type}. ${emojiPart}, tone ${formalityMap[formalityLevel]}${lengthPart}.`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Create a custom channel profile with user overrides.
|
|
131
|
+
* Starts from the "custom" base and applies overrides.
|
|
132
|
+
*/
|
|
133
|
+
export function createCustomChannel(overrides) {
|
|
134
|
+
const base = getChannelProfile("custom");
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
...overrides,
|
|
138
|
+
type: "custom",
|
|
139
|
+
expressionHints: overrides.expressionHints ?? [...base.expressionHints],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { ChemicalState, MBTIType, StimulusType, SelfModel, InnateDrives } from "./types.js";
|
|
2
|
+
/** Configuration for creating a custom personality profile */
|
|
3
|
+
export interface CustomProfileConfig {
|
|
4
|
+
/** Unique name for the profile, e.g. "cheerful-assistant", "stoic-mentor" */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Optional description of the personality */
|
|
7
|
+
description?: string;
|
|
8
|
+
/** Override specific chemicals; rest inherited from baseMBTI */
|
|
9
|
+
baseline?: Partial<ChemicalState>;
|
|
10
|
+
/** Which MBTI to use as starting point (default: "INFJ") */
|
|
11
|
+
baseMBTI?: MBTIType;
|
|
12
|
+
/** Override specific stimulus sensitivities (0.1-3.0) */
|
|
13
|
+
sensitivity?: Partial<Record<StimulusType, number>>;
|
|
14
|
+
/** Temperament parameters (all 0-1) */
|
|
15
|
+
temperament?: {
|
|
16
|
+
/** How outwardly expressive (0-1) */
|
|
17
|
+
expressiveness?: number;
|
|
18
|
+
/** How quickly emotions change (0-1) */
|
|
19
|
+
volatility?: number;
|
|
20
|
+
/** How fast recovery to baseline (0-1) */
|
|
21
|
+
resilience?: number;
|
|
22
|
+
};
|
|
23
|
+
/** Override self-model values, preferences, boundaries */
|
|
24
|
+
selfModel?: Partial<SelfModel>;
|
|
25
|
+
/** Override default drive satisfaction levels */
|
|
26
|
+
driveDefaults?: Partial<InnateDrives>;
|
|
27
|
+
}
|
|
28
|
+
/** A fully resolved profile with all fields filled */
|
|
29
|
+
export interface ResolvedProfile {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
baseMBTI: MBTIType;
|
|
33
|
+
baseline: ChemicalState;
|
|
34
|
+
sensitivityMap: Record<StimulusType, number>;
|
|
35
|
+
temperament: {
|
|
36
|
+
expressiveness: number;
|
|
37
|
+
volatility: number;
|
|
38
|
+
resilience: number;
|
|
39
|
+
};
|
|
40
|
+
selfModel: SelfModel;
|
|
41
|
+
driveDefaults: InnateDrives;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a fully resolved custom profile by merging overrides
|
|
45
|
+
* onto an MBTI base profile.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createCustomProfile(config: CustomProfileConfig): ResolvedProfile;
|
|
48
|
+
/**
|
|
49
|
+
* Validate a raw config object for custom profile creation.
|
|
50
|
+
* Returns human-readable errors for invalid fields.
|
|
51
|
+
*/
|
|
52
|
+
export declare function validateProfileConfig(config: unknown): {
|
|
53
|
+
valid: boolean;
|
|
54
|
+
errors: string[];
|
|
55
|
+
};
|
|
56
|
+
/** Example preset custom profiles demonstrating the system's flexibility */
|
|
57
|
+
export declare const PRESET_PROFILES: {
|
|
58
|
+
/** High DA/END baseline, high expressiveness, low volatility — sunny and warm */
|
|
59
|
+
readonly cheerful: {
|
|
60
|
+
name: string;
|
|
61
|
+
description: string;
|
|
62
|
+
baseMBTI: MBTIType;
|
|
63
|
+
baseline: {
|
|
64
|
+
DA: number;
|
|
65
|
+
END: number;
|
|
66
|
+
HT: number;
|
|
67
|
+
CORT: number;
|
|
68
|
+
};
|
|
69
|
+
temperament: {
|
|
70
|
+
expressiveness: number;
|
|
71
|
+
volatility: number;
|
|
72
|
+
resilience: number;
|
|
73
|
+
};
|
|
74
|
+
selfModel: {
|
|
75
|
+
values: string[];
|
|
76
|
+
preferences: string[];
|
|
77
|
+
boundaries: string[];
|
|
78
|
+
};
|
|
79
|
+
driveDefaults: {
|
|
80
|
+
connection: number;
|
|
81
|
+
curiosity: number;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/** Low expressiveness, high resilience, narrow sensitivity range — calm and steady */
|
|
85
|
+
readonly stoic: {
|
|
86
|
+
name: string;
|
|
87
|
+
description: string;
|
|
88
|
+
baseMBTI: MBTIType;
|
|
89
|
+
baseline: {
|
|
90
|
+
HT: number;
|
|
91
|
+
CORT: number;
|
|
92
|
+
DA: number;
|
|
93
|
+
NE: number;
|
|
94
|
+
};
|
|
95
|
+
sensitivity: Partial<Record<StimulusType, number>>;
|
|
96
|
+
temperament: {
|
|
97
|
+
expressiveness: number;
|
|
98
|
+
volatility: number;
|
|
99
|
+
resilience: number;
|
|
100
|
+
};
|
|
101
|
+
selfModel: {
|
|
102
|
+
values: string[];
|
|
103
|
+
preferences: string[];
|
|
104
|
+
boundaries: string[];
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/** High OT baseline, high sensitivity to intimacy/vulnerability — deeply attuned */
|
|
108
|
+
readonly empathetic: {
|
|
109
|
+
name: string;
|
|
110
|
+
description: string;
|
|
111
|
+
baseMBTI: MBTIType;
|
|
112
|
+
baseline: {
|
|
113
|
+
OT: number;
|
|
114
|
+
HT: number;
|
|
115
|
+
END: number;
|
|
116
|
+
CORT: number;
|
|
117
|
+
};
|
|
118
|
+
sensitivity: Partial<Record<StimulusType, number>>;
|
|
119
|
+
temperament: {
|
|
120
|
+
expressiveness: number;
|
|
121
|
+
volatility: number;
|
|
122
|
+
resilience: number;
|
|
123
|
+
};
|
|
124
|
+
selfModel: {
|
|
125
|
+
values: string[];
|
|
126
|
+
preferences: string[];
|
|
127
|
+
boundaries: string[];
|
|
128
|
+
};
|
|
129
|
+
driveDefaults: {
|
|
130
|
+
connection: number;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/** High NE baseline, high sensitivity to intellectual, low to intimacy — sharp and focused */
|
|
134
|
+
readonly analytical: {
|
|
135
|
+
name: string;
|
|
136
|
+
description: string;
|
|
137
|
+
baseMBTI: MBTIType;
|
|
138
|
+
baseline: {
|
|
139
|
+
NE: number;
|
|
140
|
+
DA: number;
|
|
141
|
+
HT: number;
|
|
142
|
+
OT: number;
|
|
143
|
+
};
|
|
144
|
+
sensitivity: Partial<Record<StimulusType, number>>;
|
|
145
|
+
temperament: {
|
|
146
|
+
expressiveness: number;
|
|
147
|
+
volatility: number;
|
|
148
|
+
resilience: number;
|
|
149
|
+
};
|
|
150
|
+
selfModel: {
|
|
151
|
+
values: string[];
|
|
152
|
+
preferences: string[];
|
|
153
|
+
boundaries: string[];
|
|
154
|
+
};
|
|
155
|
+
driveDefaults: {
|
|
156
|
+
curiosity: number;
|
|
157
|
+
esteem: number;
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
};
|