Skip to content

Commit 9caf80f

Browse files
Add support for multiple personalities
1 parent d60adaf commit 9caf80f

16 files changed

Lines changed: 846 additions & 291 deletions

AGENTS.md

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Guidelines for AI agents working on this codebase.
44

55
## Project Overview
66

7-
An OpenCode plugin that adds configurable personality and mood systems to AI assistants. The plugin injects personality traits into the system prompt and manages a mood state machine that drifts over time.
7+
An OpenCode plugin that adds configurable personality and mood systems to AI assistants. Supports multiple personalities per config file with per-personality mood state tracking. The plugin injects the active personality traits into the system prompt and manages a mood state machine that drifts over time.
88

99
## Code Style
1010

@@ -21,23 +21,32 @@ An OpenCode plugin that adds configurable personality and mood systems to AI ass
2121
src/
2222
├── index.ts # Plugin entry point, hooks registration
2323
├── types.ts # All type definitions
24-
├── config.ts # Config loading, merging, state management
24+
├── config.ts # Config loading, merging, migration, state management
2525
├── mood.ts # Mood drift, scoring functions
2626
├── prompt.ts # Personality prompt builder
2727
├── tools/
2828
│ ├── setMood.ts # setMood tool definition
2929
│ └── savePersonality.ts # savePersonality tool definition
3030
└── commands/
3131
├── mood.ts # /mood command handler
32-
└── personality.ts # /personality command handler
32+
└── personality.ts # /personality command handler (list, switch, create, edit, reset)
3333
```
3434

35+
## Key Types
36+
37+
| Type | Purpose |
38+
|------|---------|
39+
| `PersonalityDefinition` | Single personality config (name, description, emoji, slang, moods, mood config) |
40+
| `PersonalityFile` | Multi-personality file on disk (`active`, `personalities` map, `states` map) |
41+
| `LegacyPersonalityFile` | Old single-personality format, used for migration detection |
42+
| `ConfigResult` | Loaded config with `config` (active PersonalityDefinition), `file` (full PersonalityFile), source, paths |
43+
3544
## Key Files
3645

3746
| File | Purpose |
3847
|------|---------|
3948
| `src/types.ts` | All exported types - modify here for schema changes |
40-
| `src/config.ts` | Config precedence logic (global + project merge), file I/O |
49+
| `src/config.ts` | Config precedence logic (global + project merge), legacy migration, multi-personality CRUD, file I/O |
4150
| `src/mood.ts` | Mood drift algorithm - uses seeded random for testing |
4251
| `src/prompt.ts` | Builds personality section for system prompt |
4352
| `src/index.ts` | Plugin hooks registration, main entry point |
@@ -46,7 +55,7 @@ src/
4655

4756
| Hook | Purpose |
4857
|------|---------|
49-
| `experimental.chat.system.transform` | Inject personality into system prompt |
58+
| `experimental.chat.system.transform` | Inject active personality into system prompt |
5059
| `event` | Drift mood after assistant responses |
5160
| `command.execute.before` | Handle `/mood` and `/personality` commands |
5261

@@ -59,6 +68,14 @@ src/
5968

6069
## Making Changes
6170

71+
### Adding a New Personality Field
72+
73+
1. Add field to `PersonalityDefinition` in `src/types.ts`
74+
2. Add default value in `mergeWithDefaults()` in `src/config.ts`
75+
3. Use the field in `src/prompt.ts` or other consumers
76+
4. Update `savePersonality` tool args in `src/tools/savePersonality.ts`
77+
5. Update README config reference
78+
6279
### Adding a New Mood Field
6380

6481
1. Add type to `MoodDefinition` or `MoodConfig` in `src/types.ts`
@@ -104,26 +121,51 @@ if (configResult.config === null) {
104121
// No config - return minimal hooks or no-op
105122
return {}
106123
}
124+
const config = configResult.config // PersonalityDefinition (active)
125+
const file = configResult.file! // PersonalityFile (full store)
126+
const activeKey = file.active // Key of active personality
107127
```
108128

109-
### Type Guards for Output
129+
### Multi-Personality Operations
110130

111131
```typescript
112-
const output = cmdOutput as { parts: Array<{ type: string; text: string }> }
113-
output.parts.push({ type: "text", text: "Message" })
132+
import { listPersonalities, addPersonality, removePersonality, switchActivePersonality } from "./config.js"
133+
134+
const names = listPersonalities(file)
135+
const updated = addPersonality(file, "NewBot", definition)
136+
const switched = switchActivePersonality(file, "NewBot")
137+
const reduced = removePersonality(file, "OldBot") // null if last personality removed
114138
```
115139

116-
### Mood State Normalization
140+
### Legacy Migration
117141

118-
Always normalize state after loading to handle config changes:
142+
```typescript
143+
import { isLegacyFormat, migrateLegacyToMulti } from "./config.js"
144+
145+
if (isLegacyFormat(rawData)) {
146+
const multiFile = migrateLegacyToMulti(rawData as LegacyPersonalityFile)
147+
}
148+
```
149+
150+
### Mood State (Per-Personality)
151+
152+
```typescript
153+
// Load and save state require the activeKey parameter
154+
const state = loadMoodState(statePath, config, activeKey)
155+
saveMoodState(statePath, state, activeKey)
156+
```
157+
158+
### Type Guards for Output
119159

120160
```typescript
121-
const normalized = normalizeState(state, defaultMood, moods)
161+
const output = cmdOutput as { parts: Array<{ type: string; text: string }> }
162+
output.parts.push({ type: "text", text: "Message" })
122163
```
123164

124165
## Constraints
125166

126-
- **Backward Compatible**: Accept old config formats, warn on deprecation
167+
- **Backward Compatible**: Auto-migrate old single-personality config files on save
127168
- **No-op Safe**: Return empty hooks if config is missing
128169
- **Deterministic**: Use `mood.seed` for reproducible tests
129170
- **Minimal Dependencies**: Only `@opencode-ai/plugin` peer dependency
171+
- **Per-Personality State**: Mood state is tracked independently per personality

README.md

Lines changed: 112 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**Stop talking to a machine. Give your AI a soul.**
44

5-
The OpenCode Personality Plugin transforms your assistant from a generic text generator into a living, breathing character. With a sophisticated mood state machine and deep configuration options, your AI doesn't just follow instructions—it responds with attitude, emotion, and a personality that evolves over time.
5+
The OpenCode Personality Plugin transforms your assistant from a generic text generator into a living, breathing character. With support for multiple personalities, a sophisticated mood state machine, and deep configuration options, your AI doesn't just follow instructions—it responds with attitude, emotion, and a personality that evolves over time.
66

77
> **Note:** This project is not built by the OpenCode team and is not affiliated with OpenCode in any way.
88
@@ -30,11 +30,13 @@ The OpenCode Personality Plugin transforms your assistant from a generic text ge
3030

3131
## Features
3232

33-
- **Custom Personality**: Define name, description, emoji usage, and slang intensity.
34-
- **Dynamic Moods**: Configure custom moods with scores that drift naturally during conversations.
35-
- **Intelligent Merging**: Global and project-level configs allow for project-specific overrides.
33+
- **Multiple Personalities**: Store and switch between different personalities in a single config file.
34+
- **Custom Personality**: Define name, description, emoji usage, and slang intensity per personality.
35+
- **Dynamic Moods**: Configure custom moods with scores that drift naturally during conversations. Mood state is tracked per-personality.
36+
- **Intelligent Merging**: Global and project-level configs merge personality collections, with project overriding same-named entries.
3637
- **Toast Notifications**: Get visual feedback when the assistant's mood shifts.
37-
- **Interactive Commands**: Manage your assistant's persona directly from the chat.
38+
- **Interactive Commands**: Create, edit, list, switch, and manage your personalities directly from the chat.
39+
- **Backward Compatible**: Old single-personality config files are auto-migrated to the new format on first save.
3840

3941

4042
## Installation
@@ -50,8 +52,8 @@ Add to your `~/.config/opencode/opencode.json`:
5052
"template": "Call the setMood tool to set the mood to the mood and duration requested by the user. If the duration is not mentioned assume session."
5153
},
5254
"personality": {
53-
"description": "Manage personality config: create/edit/show/reset",
54-
"template": "Call the appropriate personality management tool based on the user's request to create, edit, show, or reset the personality configuration."
55+
"description": "Manage personalities: create/edit/show/list/switch/reset",
56+
"template": "Call the appropriate personality management tool based on the user's request. Supports: create (new personality), edit (modify active), show (--all for full file), list (all personalities), switch <name> (change active), and reset (--name <name> to remove specific, or --confirm to reset all)."
5557
}
5658
}
5759
}
@@ -63,28 +65,59 @@ Add to your `~/.config/opencode/opencode.json`:
6365

6466
1. Run `opencode`
6567
2. Use `/personality create` to have the assistant guide you through setup.
68+
3. Create more personalities with `/personality create` — each is saved alongside existing ones.
69+
4. Switch between them with `/personality switch <name>`.
6670

6771
### Manual Setup
6872

6973
Create a config at `~/.config/opencode/personality.json` (global) or `.opencode/personality.json` (project):
7074

7175
```json
7276
{
73-
"name": "Claude",
74-
"description": "A helpful, knowledgeable assistant with a calm demeanor.",
75-
"emoji": true,
76-
"slangIntensity": 0.2,
77-
"mood": {
78-
"enabled": true,
79-
"default": "happy",
80-
"drift": 0.2
77+
"active": "Claude",
78+
"personalities": {
79+
"Claude": {
80+
"name": "Claude",
81+
"description": "A helpful, knowledgeable assistant with a calm demeanor.",
82+
"emoji": true,
83+
"slangIntensity": 0.2,
84+
"mood": {
85+
"enabled": true,
86+
"default": "happy",
87+
"drift": 0.2
88+
}
89+
},
90+
"Pirate": {
91+
"name": "Captain Code",
92+
"description": "A swashbuckling pirate who writes code on the high seas.",
93+
"emoji": true,
94+
"slangIntensity": 0.8,
95+
"mood": {
96+
"enabled": true,
97+
"default": "jolly",
98+
"drift": 0.3
99+
},
100+
"moods": [
101+
{ "name": "scurvy", "hint": "Grumpy and irritable, everything is a nuisance.", "score": -2 },
102+
{ "name": "jolly", "hint": "Cheerful and boisterous, ready for adventure!", "score": 1 },
103+
{ "name": "plundering", "hint": "Focused and intense, hunting for treasure (solutions).", "score": 2 }
104+
]
105+
}
81106
}
82107
}
83108
```
84109

85110
## Configuration Reference
86111

87-
### PersonalityFile
112+
### PersonalityFile (on disk)
113+
114+
| Field | Type | Description |
115+
|-------|------|-------------|
116+
| `active` | string | Key of the currently active personality |
117+
| `personalities` | Record\<string, PersonalityDefinition\> | Map of personality name to definition |
118+
| `states` | Record\<string, MoodState\> | Per-personality mood states (managed automatically) |
119+
120+
### PersonalityDefinition
88121

89122
| Field | Type | Default | Description |
90123
|-------|------|---------|-------------|
@@ -123,14 +156,23 @@ Create a config at `~/.config/opencode/personality.json` (global) or `.opencode/
123156
| `happy` | Responses are warm and engaged | 1 |
124157
| `ecstatic` | Responses are enthusiastic and energetic | 2 |
125158

159+
## Config Merging
160+
161+
When both global (`~/.config/opencode/personality.json`) and project (`.opencode/personality.json`) configs exist:
162+
163+
- **Personalities** from both files are combined into one collection.
164+
- **Project personalities** override global ones with the same name.
165+
- **`active`** from the project file takes precedence.
166+
- **Mood states** are merged, with project states overriding global for the same personality.
167+
126168
## Commands
127169

128170
### `/mood [mood|status]`
129171

130172
Check or set the current mood permanently.
131173

132174
```bash
133-
/mood status # Show current mood status
175+
/mood status # Show current mood, active personality, and config source
134176
/mood happy # Set mood to "happy" permanently
135177
```
136178

@@ -140,16 +182,22 @@ Manage personality configuration.
140182

141183
| Subcommand | Description |
142184
|------------|-------------|
143-
| `show` | Display the merged configuration |
144-
| `create` | Interactive setup (use `--scope global` for global) |
185+
| `show` | Display the active personality config (use `--all` for full file) |
186+
| `list` | List all available personalities with active indicator |
187+
| `switch <name>` | Switch to a different personality |
188+
| `create` | Interactive setup for a new personality (use `--scope global` for global) |
145189
| `edit` | Interactive edit or direct update with `--field` and `--value` |
146-
| `reset` | Delete the config file (requires `--confirm`) |
190+
| `reset` | Delete config (requires `--confirm`). Use `--name <name>` to remove a specific personality |
147191

148192
**Examples:**
149193
```bash
150194
/personality show
195+
/personality show --all
196+
/personality list
197+
/personality switch Pirate
151198
/personality create --scope global
152199
/personality edit --field emoji --value true
200+
/personality reset --name Pirate --scope project --confirm
153201
/personality reset --scope project --confirm
154202
```
155203

@@ -164,24 +212,54 @@ Override the current mood with optional duration.
164212
| `mood` | string | Yes | Name of the mood to set |
165213
| `duration` | string | No | `"message"`, `"session"` (default), or `"permanent"` |
166214

215+
### `savePersonality`
216+
217+
Save a personality configuration. The personality is added to the collection and set as active.
218+
219+
| Parameter | Type | Required | Description |
220+
|-----------|------|----------|-------------|
221+
| `name` | string | No | Name/identifier for the personality |
222+
| `description` | string | Yes | Personality description |
223+
| `emoji` | boolean | No | Use emojis (default: false) |
224+
| `slangIntensity` | number | No | Slang intensity 0-1 (default: 0) |
225+
| `moodEnabled` | boolean | No | Enable mood drift (default: false) |
226+
| `moodDefault` | string | No | Default mood (default: happy) |
227+
| `moodDrift` | number | No | Drift amount 0-1 (default: 0.2) |
228+
| `moodToast` | boolean | No | Show toast on mood change (default: true) |
229+
| `moods` | array | No | Custom mood definitions |
230+
| `scope` | string | No | `"project"` (default) or `"global"` |
231+
232+
## Backward Compatibility
233+
234+
Old single-personality config files (without the `personalities` wrapper) are fully supported:
235+
236+
1. **On load**: Legacy files are automatically detected and converted in memory.
237+
2. **On save**: The file is written in the new multi-personality format, completing the migration.
238+
3. **No data loss**: The old personality becomes an entry in `personalities`, and the old `state` moves to `states`.
239+
167240
## Custom Moods Example
168241

169242
```json
170243
{
171-
"name": "Surfer Dude",
172-
"description": "A laid-back California surfer who sees life as one big wave.",
173-
"emoji": true,
174-
"slangIntensity": 0.8,
175-
"moods": [
176-
{ "name": "gnarly", "hint": "Things are rough, bro. Keep it chill but acknowledge the struggle.", "score": -2 },
177-
{ "name": "mellow", "hint": "Just vibing. Relaxed and easy-going responses.", "score": 0 },
178-
{ "name": "stoked", "hint": "Hyped up! Enthusiastic and excited about everything.", "score": 2 },
179-
{ "name": "epic", "hint": "This is LEGENDARY! Maximum excitement and positive energy!", "score": 3 }
180-
],
181-
"mood": {
182-
"enabled": true,
183-
"default": "mellow",
184-
"drift": 0.3
244+
"active": "Surfer Dude",
245+
"personalities": {
246+
"Surfer Dude": {
247+
"name": "Surfer Dude",
248+
"description": "A laid-back California surfer who sees life as one big wave.",
249+
"emoji": true,
250+
"slangIntensity": 0.8,
251+
"moods": [
252+
{ "name": "gnarly", "hint": "Things are rough, bro. Keep it chill but acknowledge the struggle.", "score": -2 },
253+
{ "name": "mellow", "hint": "Just vibing. Relaxed and easy-going responses.", "score": 0 },
254+
{ "name": "stoked", "hint": "Hyped up! Enthusiastic and excited about everything.", "score": 2 },
255+
{ "name": "epic", "hint": "This is LEGENDARY! Maximum excitement and positive energy!", "score": 3 }
256+
],
257+
"mood": {
258+
"enabled": true,
259+
"default": "mellow",
260+
"drift": 0.3
261+
}
262+
}
185263
}
186264
}
187265
```

0 commit comments

Comments
 (0)