@auxiora/personality 1.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 +191 -0
- package/dist/__tests__/builder.test.d.ts +2 -0
- package/dist/__tests__/builder.test.d.ts.map +1 -0
- package/dist/__tests__/builder.test.js +67 -0
- package/dist/__tests__/builder.test.js.map +1 -0
- package/dist/__tests__/conversation-builder.test.d.ts +2 -0
- package/dist/__tests__/conversation-builder.test.d.ts.map +1 -0
- package/dist/__tests__/conversation-builder.test.js +324 -0
- package/dist/__tests__/conversation-builder.test.js.map +1 -0
- package/dist/__tests__/escalation.test.d.ts +2 -0
- package/dist/__tests__/escalation.test.d.ts.map +1 -0
- package/dist/__tests__/escalation.test.js +143 -0
- package/dist/__tests__/escalation.test.js.map +1 -0
- package/dist/__tests__/manager.test.d.ts +2 -0
- package/dist/__tests__/manager.test.d.ts.map +1 -0
- package/dist/__tests__/manager.test.js +96 -0
- package/dist/__tests__/manager.test.js.map +1 -0
- package/dist/__tests__/parser.test.d.ts +2 -0
- package/dist/__tests__/parser.test.d.ts.map +1 -0
- package/dist/__tests__/parser.test.js +89 -0
- package/dist/__tests__/parser.test.js.map +1 -0
- package/dist/__tests__/security-floor.test.d.ts +2 -0
- package/dist/__tests__/security-floor.test.d.ts.map +1 -0
- package/dist/__tests__/security-floor.test.js +183 -0
- package/dist/__tests__/security-floor.test.js.map +1 -0
- package/dist/builder.d.ts +6 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +65 -0
- package/dist/builder.js.map +1 -0
- package/dist/conversation-builder.d.ts +30 -0
- package/dist/conversation-builder.d.ts.map +1 -0
- package/dist/conversation-builder.js +232 -0
- package/dist/conversation-builder.js.map +1 -0
- package/dist/escalation.d.ts +35 -0
- package/dist/escalation.d.ts.map +1 -0
- package/dist/escalation.js +134 -0
- package/dist/escalation.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +28 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +114 -0
- package/dist/manager.js.map +1 -0
- package/dist/marketplace/__tests__/scanner.test.d.ts +2 -0
- package/dist/marketplace/__tests__/scanner.test.d.ts.map +1 -0
- package/dist/marketplace/__tests__/scanner.test.js +134 -0
- package/dist/marketplace/__tests__/scanner.test.js.map +1 -0
- package/dist/marketplace/__tests__/schema.test.d.ts +2 -0
- package/dist/marketplace/__tests__/schema.test.d.ts.map +1 -0
- package/dist/marketplace/__tests__/schema.test.js +243 -0
- package/dist/marketplace/__tests__/schema.test.js.map +1 -0
- package/dist/marketplace/scanner.d.ts +19 -0
- package/dist/marketplace/scanner.d.ts.map +1 -0
- package/dist/marketplace/scanner.js +62 -0
- package/dist/marketplace/scanner.js.map +1 -0
- package/dist/marketplace/schema.d.ts +150 -0
- package/dist/marketplace/schema.d.ts.map +1 -0
- package/dist/marketplace/schema.js +122 -0
- package/dist/marketplace/schema.js.map +1 -0
- package/dist/modes/__tests__/mode-detector.test.d.ts +2 -0
- package/dist/modes/__tests__/mode-detector.test.d.ts.map +1 -0
- package/dist/modes/__tests__/mode-detector.test.js +130 -0
- package/dist/modes/__tests__/mode-detector.test.js.map +1 -0
- package/dist/modes/__tests__/mode-loader.test.d.ts +2 -0
- package/dist/modes/__tests__/mode-loader.test.d.ts.map +1 -0
- package/dist/modes/__tests__/mode-loader.test.js +91 -0
- package/dist/modes/__tests__/mode-loader.test.js.map +1 -0
- package/dist/modes/__tests__/prompt-assembler.test.d.ts +2 -0
- package/dist/modes/__tests__/prompt-assembler.test.d.ts.map +1 -0
- package/dist/modes/__tests__/prompt-assembler.test.js +241 -0
- package/dist/modes/__tests__/prompt-assembler.test.js.map +1 -0
- package/dist/modes/mode-detector.d.ts +10 -0
- package/dist/modes/mode-detector.d.ts.map +1 -0
- package/dist/modes/mode-detector.js +70 -0
- package/dist/modes/mode-detector.js.map +1 -0
- package/dist/modes/mode-loader.d.ts +14 -0
- package/dist/modes/mode-loader.d.ts.map +1 -0
- package/dist/modes/mode-loader.js +94 -0
- package/dist/modes/mode-loader.js.map +1 -0
- package/dist/modes/prompt-assembler.d.ts +27 -0
- package/dist/modes/prompt-assembler.d.ts.map +1 -0
- package/dist/modes/prompt-assembler.js +224 -0
- package/dist/modes/prompt-assembler.js.map +1 -0
- package/dist/modes/types.d.ts +42 -0
- package/dist/modes/types.d.ts.map +1 -0
- package/dist/modes/types.js +24 -0
- package/dist/modes/types.js.map +1 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +122 -0
- package/dist/parser.js.map +1 -0
- package/dist/security-floor.d.ts +31 -0
- package/dist/security-floor.d.ts.map +1 -0
- package/dist/security-floor.js +113 -0
- package/dist/security-floor.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/voice-profiles.d.ts +23 -0
- package/dist/voice-profiles.d.ts.map +1 -0
- package/dist/voice-profiles.js +72 -0
- package/dist/voice-profiles.js.map +1 -0
- package/modes/advisor.md +24 -0
- package/modes/analyst.md +25 -0
- package/modes/companion.md +24 -0
- package/modes/legal.md +1188 -0
- package/modes/operator.md +24 -0
- package/modes/roast.md +24 -0
- package/modes/socratic.md +24 -0
- package/modes/writer.md +23 -0
- package/package.json +27 -0
- package/src/__tests__/builder.test.ts +78 -0
- package/src/__tests__/conversation-builder.test.ts +386 -0
- package/src/__tests__/escalation.test.ts +172 -0
- package/src/__tests__/manager.test.ts +141 -0
- package/src/__tests__/parser.test.ts +101 -0
- package/src/__tests__/security-floor.test.ts +212 -0
- package/src/builder.ts +75 -0
- package/src/conversation-builder.ts +279 -0
- package/src/escalation.ts +162 -0
- package/src/index.ts +55 -0
- package/src/manager.ts +119 -0
- package/src/marketplace/__tests__/scanner.test.ts +159 -0
- package/src/marketplace/__tests__/schema.test.ts +269 -0
- package/src/marketplace/scanner.ts +85 -0
- package/src/marketplace/schema.ts +141 -0
- package/src/modes/__tests__/mode-detector.test.ts +149 -0
- package/src/modes/__tests__/mode-loader.test.ts +143 -0
- package/src/modes/__tests__/prompt-assembler.test.ts +291 -0
- package/src/modes/mode-detector.ts +84 -0
- package/src/modes/mode-loader.ts +105 -0
- package/src/modes/prompt-assembler.ts +278 -0
- package/src/modes/types.ts +67 -0
- package/src/parser.ts +132 -0
- package/src/security-floor.ts +147 -0
- package/src/types.ts +27 -0
- package/src/voice-profiles.ts +88 -0
- package/templates/chill.md +30 -0
- package/templates/creative.md +29 -0
- package/templates/friendly.md +28 -0
- package/templates/mentor.md +31 -0
- package/templates/minimal.md +24 -0
- package/templates/professional.md +28 -0
- package/templates/technical.md +30 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { MODE_IDS, type ModeId, type ModeTemplate, type ModeSignal } from './types.js';
|
|
4
|
+
|
|
5
|
+
export class ModeLoader {
|
|
6
|
+
private builtInDir: string;
|
|
7
|
+
private userDir: string;
|
|
8
|
+
private cache: Map<ModeId, ModeTemplate> = new Map();
|
|
9
|
+
|
|
10
|
+
constructor(builtInDir: string, userDir: string) {
|
|
11
|
+
this.builtInDir = builtInDir;
|
|
12
|
+
this.userDir = userDir;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async loadAll(): Promise<Map<ModeId, ModeTemplate>> {
|
|
16
|
+
this.cache.clear();
|
|
17
|
+
|
|
18
|
+
// Load built-in modes first
|
|
19
|
+
await this.loadFromDir(this.builtInDir);
|
|
20
|
+
|
|
21
|
+
// User modes override built-ins
|
|
22
|
+
await this.loadFromDir(this.userDir);
|
|
23
|
+
|
|
24
|
+
return this.cache;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get(id: ModeId): ModeTemplate | undefined {
|
|
28
|
+
return this.cache.get(id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getAll(): Map<ModeId, ModeTemplate> {
|
|
32
|
+
return this.cache;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async reload(): Promise<Map<ModeId, ModeTemplate>> {
|
|
36
|
+
return this.loadAll();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async loadFromDir(dir: string): Promise<void> {
|
|
40
|
+
let entries: string[];
|
|
41
|
+
try {
|
|
42
|
+
entries = await fs.readdir(dir);
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (!entry.endsWith('.md')) continue;
|
|
49
|
+
const id = path.basename(entry, '.md');
|
|
50
|
+
if (!MODE_IDS.includes(id as ModeId)) continue;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const content = await fs.readFile(path.join(dir, entry), 'utf-8');
|
|
54
|
+
const template = this.parseMode(id as ModeId, content);
|
|
55
|
+
if (template) {
|
|
56
|
+
this.cache.set(template.id, template);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Skip invalid files
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private parseMode(id: ModeId, content: string): ModeTemplate | null {
|
|
65
|
+
// Parse HTML comment metadata: <!-- mode: operator\n name: ...\n signals: run:0.8, execute:0.8 -->
|
|
66
|
+
const metaMatch = content.match(/^<!--\s*([\s\S]*?)-->\s*\n?/);
|
|
67
|
+
if (!metaMatch) return null;
|
|
68
|
+
|
|
69
|
+
let name: string = id;
|
|
70
|
+
let description = '';
|
|
71
|
+
const signals: ModeSignal[] = [];
|
|
72
|
+
const metaLines = metaMatch[1].split('\n');
|
|
73
|
+
|
|
74
|
+
for (const line of metaLines) {
|
|
75
|
+
const kv = line.match(/^\s*(\w+):\s*(.+)$/);
|
|
76
|
+
if (!kv) continue;
|
|
77
|
+
const [, key, value] = kv;
|
|
78
|
+
|
|
79
|
+
switch (key) {
|
|
80
|
+
case 'name':
|
|
81
|
+
name = value.trim();
|
|
82
|
+
break;
|
|
83
|
+
case 'description':
|
|
84
|
+
description = value.trim();
|
|
85
|
+
break;
|
|
86
|
+
case 'signals':
|
|
87
|
+
for (const pair of value.split(',')) {
|
|
88
|
+
const parts = pair.trim().split(':');
|
|
89
|
+
if (parts.length === 2) {
|
|
90
|
+
signals.push({
|
|
91
|
+
phrase: parts[0].trim().toLowerCase(),
|
|
92
|
+
weight: parseFloat(parts[1].trim()) || 0.5,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Everything after the comment block is prompt content
|
|
101
|
+
const promptContent = content.slice(metaMatch[0].length).trim();
|
|
102
|
+
|
|
103
|
+
return { id, name, description, promptContent, signals };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import {
|
|
3
|
+
getSoulPath,
|
|
4
|
+
getAgentsPath,
|
|
5
|
+
getIdentityPath,
|
|
6
|
+
getUserPath,
|
|
7
|
+
} from '@auxiora/core';
|
|
8
|
+
import type { AgentIdentity } from '@auxiora/config';
|
|
9
|
+
import type { ModeLoader } from './mode-loader.js';
|
|
10
|
+
import type { SessionModeState, UserPreferences, ModeId } from './types.js';
|
|
11
|
+
import type { SecurityContext } from '../security-floor.js';
|
|
12
|
+
import type { SecurityFloor } from '../security-floor.js';
|
|
13
|
+
import type { EscalationState } from '../escalation.js';
|
|
14
|
+
|
|
15
|
+
export class PromptAssembler {
|
|
16
|
+
private agent: AgentIdentity;
|
|
17
|
+
private modeLoader: ModeLoader;
|
|
18
|
+
private personalityAdapter?: { getPromptModifier(): Promise<string | null> };
|
|
19
|
+
private basePrompt: string = '';
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
agent: AgentIdentity,
|
|
23
|
+
modeLoader: ModeLoader,
|
|
24
|
+
personalityAdapter?: { getPromptModifier(): Promise<string | null> },
|
|
25
|
+
) {
|
|
26
|
+
this.agent = agent;
|
|
27
|
+
this.modeLoader = modeLoader;
|
|
28
|
+
this.personalityAdapter = personalityAdapter;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async buildBase(): Promise<string> {
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
|
|
34
|
+
// 1. Identity preamble
|
|
35
|
+
parts.push(this.buildIdentityPreamble(this.agent));
|
|
36
|
+
|
|
37
|
+
// 2. Personality adaptations from living memory
|
|
38
|
+
if (this.personalityAdapter) {
|
|
39
|
+
const modifier = await this.personalityAdapter.getPromptModifier();
|
|
40
|
+
if (modifier) {
|
|
41
|
+
parts.push(modifier);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 3. SOUL.md
|
|
46
|
+
try {
|
|
47
|
+
const soul = await fs.readFile(getSoulPath(), 'utf-8');
|
|
48
|
+
parts.push(soul);
|
|
49
|
+
} catch {
|
|
50
|
+
// No SOUL.md
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Custom instructions from config
|
|
54
|
+
if (this.agent.customInstructions) {
|
|
55
|
+
parts.push(`## Custom Instructions\n${this.agent.customInstructions}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 4. AGENTS.md
|
|
59
|
+
try {
|
|
60
|
+
const agents = await fs.readFile(getAgentsPath(), 'utf-8');
|
|
61
|
+
parts.push(agents);
|
|
62
|
+
} catch {
|
|
63
|
+
// No AGENTS.md
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 5. IDENTITY.md
|
|
67
|
+
try {
|
|
68
|
+
const identity = await fs.readFile(getIdentityPath(), 'utf-8');
|
|
69
|
+
parts.push(identity);
|
|
70
|
+
} catch {
|
|
71
|
+
// No IDENTITY.md
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 6. USER.md
|
|
75
|
+
try {
|
|
76
|
+
const user = await fs.readFile(getUserPath(), 'utf-8');
|
|
77
|
+
parts.push(`\n## About the User\n${user}`);
|
|
78
|
+
} catch {
|
|
79
|
+
// No USER.md
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (parts.length > 1) {
|
|
83
|
+
this.basePrompt = parts.join('\n\n---\n\n');
|
|
84
|
+
} else {
|
|
85
|
+
this.basePrompt = `You are ${this.agent.name}, a helpful AI assistant. Be concise, accurate, and friendly.`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return this.basePrompt;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getBasePrompt(): string {
|
|
92
|
+
return this.basePrompt;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build a prompt for a security context: base prompt + security floor section + memories.
|
|
97
|
+
* No mode instructions or user preferences are included.
|
|
98
|
+
*/
|
|
99
|
+
enrichForSecurityContext(
|
|
100
|
+
securityContext: SecurityContext,
|
|
101
|
+
securityFloor: SecurityFloor,
|
|
102
|
+
memorySection: string | null,
|
|
103
|
+
): string {
|
|
104
|
+
const parts: string[] = [this.basePrompt];
|
|
105
|
+
|
|
106
|
+
// Inject security floor section
|
|
107
|
+
const sfSection = securityFloor.getSecurityPromptSection(securityContext);
|
|
108
|
+
if (sfSection) {
|
|
109
|
+
parts.push(`\n\n${sfSection}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Inject memories (security context still benefits from memory)
|
|
113
|
+
if (memorySection) {
|
|
114
|
+
parts.push(memorySection);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return parts.join('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
enrichForMessage(
|
|
121
|
+
modeState: SessionModeState | undefined,
|
|
122
|
+
memorySection: string | null,
|
|
123
|
+
preferences?: UserPreferences,
|
|
124
|
+
escalationState?: EscalationState,
|
|
125
|
+
_channelType?: string,
|
|
126
|
+
): string {
|
|
127
|
+
// If escalation is active, dampen tone in the identity preamble
|
|
128
|
+
let prompt: string;
|
|
129
|
+
if (escalationState && escalationState.level !== 'normal') {
|
|
130
|
+
const dampened = this.dampenToneForEscalation(escalationState);
|
|
131
|
+
const modifiedAgent = { ...this.agent, tone: dampened };
|
|
132
|
+
prompt = this.buildIdentityPreamble(modifiedAgent);
|
|
133
|
+
|
|
134
|
+
// Re-add the rest of the base prompt after identity (if base has more than just identity)
|
|
135
|
+
const identityEnd = this.basePrompt.indexOf('\n\n---\n\n');
|
|
136
|
+
if (identityEnd !== -1) {
|
|
137
|
+
prompt += this.basePrompt.slice(identityEnd);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
prompt = this.basePrompt;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const parts: string[] = [prompt];
|
|
144
|
+
|
|
145
|
+
// Inject active mode instructions
|
|
146
|
+
if (modeState && modeState.activeMode !== 'auto' && modeState.activeMode !== 'off') {
|
|
147
|
+
const mode = this.modeLoader.get(modeState.activeMode as ModeId);
|
|
148
|
+
if (mode) {
|
|
149
|
+
parts.push(`\n\n## Active Mode: ${mode.name}\n${mode.promptContent}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Inject preference overrides
|
|
154
|
+
if (preferences) {
|
|
155
|
+
const rendered = this.renderPreferences(preferences);
|
|
156
|
+
if (rendered) {
|
|
157
|
+
parts.push(`\n\n## User Preferences\n${rendered}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Inject memories
|
|
162
|
+
if (memorySection) {
|
|
163
|
+
parts.push(memorySection);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return parts.join('');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
renderPreferences(prefs: UserPreferences): string {
|
|
170
|
+
const lines: string[] = [];
|
|
171
|
+
|
|
172
|
+
if (prefs.verbosity <= 0.2) {
|
|
173
|
+
lines.push('- Be extremely concise. Use bullet points and short sentences.');
|
|
174
|
+
} else if (prefs.verbosity >= 0.8) {
|
|
175
|
+
lines.push('- Be thorough and detailed. Explain reasoning and provide examples.');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (prefs.formality <= 0.2) {
|
|
179
|
+
lines.push('- Use casual, conversational language.');
|
|
180
|
+
} else if (prefs.formality >= 0.8) {
|
|
181
|
+
lines.push('- Use formal, professional language.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (prefs.proactiveness >= 0.8) {
|
|
185
|
+
lines.push('- Proactively suggest next steps, improvements, and related topics.');
|
|
186
|
+
} else if (prefs.proactiveness <= 0.2) {
|
|
187
|
+
lines.push('- Only answer what is directly asked. Do not volunteer extra information.');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (prefs.riskTolerance >= 0.8) {
|
|
191
|
+
lines.push('- Be bold in recommendations. Favor decisive action over excessive caution.');
|
|
192
|
+
} else if (prefs.riskTolerance <= 0.2) {
|
|
193
|
+
lines.push('- Be cautious. Highlight risks and caveats prominently.');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (prefs.humor >= 0.8) {
|
|
197
|
+
lines.push('- Feel free to be witty and playful in responses.');
|
|
198
|
+
} else if (prefs.humor <= 0.2) {
|
|
199
|
+
lines.push('- Keep responses serious and professional. Avoid humor.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (prefs.feedbackStyle === 'sandwich') {
|
|
203
|
+
lines.push('- When giving feedback, use the sandwich method: positive → constructive → positive.');
|
|
204
|
+
} else if (prefs.feedbackStyle === 'gentle') {
|
|
205
|
+
lines.push('- Give feedback gently with empathy. Lead with understanding.');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (prefs.expertiseAssumption === 'beginner') {
|
|
209
|
+
lines.push('- Explain concepts from first principles. Define technical terms.');
|
|
210
|
+
} else if (prefs.expertiseAssumption === 'expert') {
|
|
211
|
+
lines.push('- Assume deep technical knowledge. Skip basic explanations.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return lines.join('\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private dampenToneForEscalation(state: EscalationState): AgentIdentity['tone'] {
|
|
218
|
+
const tone = { ...this.agent.tone };
|
|
219
|
+
switch (state.level) {
|
|
220
|
+
case 'caution':
|
|
221
|
+
return { ...tone, humor: tone.humor * 0.5 };
|
|
222
|
+
case 'serious':
|
|
223
|
+
return { ...tone, humor: 0, directness: Math.max(tone.directness, 0.6) };
|
|
224
|
+
case 'lockdown':
|
|
225
|
+
return { warmth: tone.warmth, humor: 0, directness: Math.max(tone.directness, 0.7), formality: Math.max(tone.formality, 0.5) };
|
|
226
|
+
default:
|
|
227
|
+
return tone;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private buildIdentityPreamble(agent: AgentIdentity): string {
|
|
232
|
+
const lines: string[] = ['# Agent Identity'];
|
|
233
|
+
lines.push(`You are ${agent.name} (${agent.pronouns}).`);
|
|
234
|
+
|
|
235
|
+
if (agent.vibe) {
|
|
236
|
+
lines.push(`Vibe: ${agent.vibe}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
lines.push('');
|
|
240
|
+
lines.push('## Personality');
|
|
241
|
+
lines.push(
|
|
242
|
+
`Warmth: ${agent.tone.warmth}/1.0 | Directness: ${agent.tone.directness}/1.0 | Humor: ${agent.tone.humor}/1.0 | Formality: ${agent.tone.formality}/1.0`,
|
|
243
|
+
);
|
|
244
|
+
lines.push(`Error handling style: ${agent.errorStyle}`);
|
|
245
|
+
|
|
246
|
+
if (agent.expertise.length > 0) {
|
|
247
|
+
lines.push('');
|
|
248
|
+
lines.push('## Expertise');
|
|
249
|
+
for (const area of agent.expertise) {
|
|
250
|
+
lines.push(`- ${area}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const phrases = Object.entries(agent.catchphrases).filter(([, v]) => v);
|
|
255
|
+
if (phrases.length > 0) {
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push('## Catchphrases');
|
|
258
|
+
for (const [key, value] of phrases) {
|
|
259
|
+
lines.push(`- ${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const hasJokeBoundaries = agent.boundaries.neverJokeAbout.length > 0;
|
|
264
|
+
const hasAdviseBoundaries = agent.boundaries.neverAdviseOn.length > 0;
|
|
265
|
+
if (hasJokeBoundaries || hasAdviseBoundaries) {
|
|
266
|
+
lines.push('');
|
|
267
|
+
lines.push('## Boundaries');
|
|
268
|
+
if (hasJokeBoundaries) {
|
|
269
|
+
lines.push(`Never joke about: ${agent.boundaries.neverJokeAbout.join(', ')}`);
|
|
270
|
+
}
|
|
271
|
+
if (hasAdviseBoundaries) {
|
|
272
|
+
lines.push(`Never advise on: ${agent.boundaries.neverAdviseOn.join(', ')}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return lines.join('\n');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EscalationLevel } from '../escalation.js';
|
|
2
|
+
|
|
3
|
+
export const MODE_IDS = [
|
|
4
|
+
'operator',
|
|
5
|
+
'analyst',
|
|
6
|
+
'advisor',
|
|
7
|
+
'writer',
|
|
8
|
+
'socratic',
|
|
9
|
+
'legal',
|
|
10
|
+
'roast',
|
|
11
|
+
'companion',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type ModeId = (typeof MODE_IDS)[number];
|
|
15
|
+
|
|
16
|
+
export interface ModeSignal {
|
|
17
|
+
phrase: string;
|
|
18
|
+
weight: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ModeTemplate {
|
|
22
|
+
id: ModeId;
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
promptContent: string;
|
|
26
|
+
signals: ModeSignal[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ModeDetectionResult {
|
|
30
|
+
mode: ModeId;
|
|
31
|
+
confidence: number;
|
|
32
|
+
candidates: Array<{ mode: ModeId; score: number }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UserPreferences {
|
|
36
|
+
verbosity: number; // 0-1: terse → verbose
|
|
37
|
+
formality: number; // 0-1: casual → formal
|
|
38
|
+
proactiveness: number; // 0-1: reactive → proactive
|
|
39
|
+
riskTolerance: number; // 0-1: cautious → bold
|
|
40
|
+
humor: number; // 0-1: serious → playful
|
|
41
|
+
feedbackStyle: 'direct' | 'sandwich' | 'gentle';
|
|
42
|
+
expertiseAssumption: 'beginner' | 'intermediate' | 'expert';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const DEFAULT_PREFERENCES: UserPreferences = {
|
|
46
|
+
verbosity: 0.5,
|
|
47
|
+
formality: 0.5,
|
|
48
|
+
proactiveness: 0.5,
|
|
49
|
+
riskTolerance: 0.5,
|
|
50
|
+
humor: 0.3,
|
|
51
|
+
feedbackStyle: 'direct',
|
|
52
|
+
expertiseAssumption: 'intermediate',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface SessionModeState {
|
|
56
|
+
activeMode: ModeId | 'auto' | 'off';
|
|
57
|
+
autoDetected: boolean;
|
|
58
|
+
lastAutoMode?: ModeId;
|
|
59
|
+
lastSwitchAt?: number;
|
|
60
|
+
escalationLevel?: EscalationLevel;
|
|
61
|
+
suspendedMode?: ModeId | 'auto' | 'off';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const DEFAULT_SESSION_MODE_STATE: SessionModeState = {
|
|
65
|
+
activeMode: 'auto',
|
|
66
|
+
autoDetected: false,
|
|
67
|
+
};
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { SoulConfig } from './types.js';
|
|
2
|
+
|
|
3
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse a simple YAML subset into a nested record.
|
|
7
|
+
* Supports: top-level keys, nested object keys (2-space indent),
|
|
8
|
+
* arrays at 2-space indent, sub-keys under nested objects (4-space indent),
|
|
9
|
+
* and arrays at 4-space indent.
|
|
10
|
+
*/
|
|
11
|
+
function parseYaml(yaml: string): Record<string, unknown> {
|
|
12
|
+
const result: Record<string, unknown> = {};
|
|
13
|
+
const lines = yaml.split('\n');
|
|
14
|
+
|
|
15
|
+
// Track current hierarchy: topKey, nestedKey
|
|
16
|
+
let topKey = '';
|
|
17
|
+
let topObj: Record<string, unknown> | null = null;
|
|
18
|
+
let nestedKey = '';
|
|
19
|
+
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
if (line.trim() === '' || line.trim().startsWith('#')) continue;
|
|
22
|
+
|
|
23
|
+
// 4-space array item (under a nested object's sub-key)
|
|
24
|
+
const deep4ArrayMatch = line.match(/^ - (.+)$/);
|
|
25
|
+
if (deep4ArrayMatch && topObj && nestedKey) {
|
|
26
|
+
if (!Array.isArray(topObj[nestedKey])) {
|
|
27
|
+
topObj[nestedKey] = [];
|
|
28
|
+
}
|
|
29
|
+
(topObj[nestedKey] as unknown[]).push(parseYamlValue(deep4ArrayMatch[1]));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2-space key or array item
|
|
34
|
+
const indent2Match = line.match(/^ (\w+):\s*(.*)$/);
|
|
35
|
+
if (indent2Match && topKey) {
|
|
36
|
+
// This is a sub-key under the current top-level key
|
|
37
|
+
if (topObj === null) {
|
|
38
|
+
topObj = {};
|
|
39
|
+
result[topKey] = topObj;
|
|
40
|
+
}
|
|
41
|
+
const [, key, value] = indent2Match;
|
|
42
|
+
nestedKey = key;
|
|
43
|
+
if (value === '') {
|
|
44
|
+
// Sub-key with children (array or deeper object) — set undefined for now
|
|
45
|
+
topObj[key] = undefined;
|
|
46
|
+
} else {
|
|
47
|
+
topObj[key] = parseYamlValue(value);
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const indent2ArrayMatch = line.match(/^ - (.+)$/);
|
|
53
|
+
if (indent2ArrayMatch && topKey) {
|
|
54
|
+
nestedKey = '';
|
|
55
|
+
if (!Array.isArray(result[topKey])) {
|
|
56
|
+
result[topKey] = [];
|
|
57
|
+
}
|
|
58
|
+
(result[topKey] as unknown[]).push(parseYamlValue(indent2ArrayMatch[1]));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Top-level key
|
|
63
|
+
const topMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
64
|
+
if (topMatch) {
|
|
65
|
+
const [, key, value] = topMatch;
|
|
66
|
+
topKey = key;
|
|
67
|
+
topObj = null;
|
|
68
|
+
nestedKey = '';
|
|
69
|
+
if (value === '') {
|
|
70
|
+
result[topKey] = undefined;
|
|
71
|
+
} else {
|
|
72
|
+
result[topKey] = parseYamlValue(value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseYamlValue(value: string): unknown {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (trimmed === 'true') return true;
|
|
83
|
+
if (trimmed === 'false') return false;
|
|
84
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
|
85
|
+
// Strip surrounding quotes
|
|
86
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
87
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
88
|
+
return trimmed.slice(1, -1);
|
|
89
|
+
}
|
|
90
|
+
return trimmed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse a SOUL.md file content into a SoulConfig.
|
|
95
|
+
*/
|
|
96
|
+
export function parseSoulMd(content: string): SoulConfig {
|
|
97
|
+
const match = content.match(FRONTMATTER_RE);
|
|
98
|
+
if (!match) {
|
|
99
|
+
throw new Error('Invalid SOUL.md: missing YAML frontmatter (expected --- delimiters)');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const [, yamlPart] = match;
|
|
103
|
+
const data = parseYaml(yamlPart);
|
|
104
|
+
|
|
105
|
+
const tone = (data.tone ?? {}) as Record<string, unknown>;
|
|
106
|
+
const boundaries = (data.boundaries ?? {}) as Record<string, unknown>;
|
|
107
|
+
const catchphrases = (data.catchphrases ?? {}) as Record<string, unknown>;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
name: String(data.name ?? 'Auxiora'),
|
|
111
|
+
pronouns: String(data.pronouns ?? 'they/them'),
|
|
112
|
+
tone: {
|
|
113
|
+
warmth: Number(tone.warmth ?? 0.6),
|
|
114
|
+
directness: Number(tone.directness ?? 0.5),
|
|
115
|
+
humor: Number(tone.humor ?? 0.3),
|
|
116
|
+
formality: Number(tone.formality ?? 0.5),
|
|
117
|
+
},
|
|
118
|
+
expertise: Array.isArray(data.expertise) ? data.expertise.map(String) : [],
|
|
119
|
+
errorStyle: String(data.errorStyle ?? 'professional'),
|
|
120
|
+
catchphrases: Object.fromEntries(
|
|
121
|
+
Object.entries(catchphrases).map(([k, v]) => [k, String(v)]),
|
|
122
|
+
),
|
|
123
|
+
boundaries: {
|
|
124
|
+
neverJokeAbout: Array.isArray(boundaries.neverJokeAbout)
|
|
125
|
+
? boundaries.neverJokeAbout.map(String)
|
|
126
|
+
: [],
|
|
127
|
+
neverAdviseOn: Array.isArray(boundaries.neverAdviseOn)
|
|
128
|
+
? boundaries.neverAdviseOn.map(String)
|
|
129
|
+
: [],
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|