ei-tui 1.5.0 → 1.6.1
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 +2 -2
- package/package.json +1 -1
- package/src/cli/README.md +16 -71
- package/src/cli.ts +159 -17
- package/src/core/orchestrators/ceremony.ts +47 -16
- package/src/core/processor.ts +11 -3
- package/src/core/prompt-context-builder.ts +2 -0
- package/src/core/tools/builtin/persona-notes.ts +81 -0
- package/src/core/tools/index.ts +56 -0
- package/src/core/types/data-items.ts +1 -2
- package/src/core/types/entities.ts +1 -0
- package/src/integrations/slack/importer.ts +0 -1
- package/src/prompts/response/index.ts +5 -2
- package/src/prompts/response/sections.ts +10 -0
- package/src/prompts/response/types.ts +1 -0
- package/src/prompts/room/index.ts +3 -0
- package/src/prompts/room/types.ts +1 -0
- package/tui/README.md +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ You can access the Web version at [ei.flare576.com](https://ei.flare576.com).
|
|
|
6
6
|
|
|
7
7
|
You can run the local version via `bunx ei-tui` — no install needed, always current (see [### TUI](#tui) for details).
|
|
8
8
|
|
|
9
|
-
If you're here to give your coding tools (OpenCode, Claude Code, Cursor) persistent memory, jump over to [TUI README.md](./tui/README.md) to learn how to get information _into_ Ei, and [CLI README.md](./src/cli/README.md) to
|
|
9
|
+
If you're here to give your coding tools (OpenCode, Claude Code, Cursor) persistent memory, jump over to [TUI README.md](./tui/README.md) to learn how to get information _into_ Ei, and [CLI README.md](./src/cli/README.md) to wire up automatic context injection so your agents receive relevant memory before every message — no tool calls required.
|
|
10
10
|
|
|
11
11
|
## What Does "Local First" Mean?
|
|
12
12
|
|
|
@@ -140,7 +140,7 @@ opencode:
|
|
|
140
140
|
|
|
141
141
|
OpenCode saves sessions as JSON or SQLite (depending on version). Ei reads them, extracts context per-agent (each agent like Sisyphus gets its own persona), and keeps everything current as sessions accumulate.
|
|
142
142
|
|
|
143
|
-
OpenCode can also *read* Ei's knowledge back out via the [CLI tool](src/cli/README.md) —
|
|
143
|
+
OpenCode can also *read* Ei's knowledge back out via the [CLI tool](src/cli/README.md). Run `ei --install` to wire up automatic context injection — relevant topics are injected before every message, and (with [Oh My OpenCode](https://github.com/code-yeongyu/oh-my-opencode)) each agent's Ei persona record is loaded into the system prompt at session start. The agent knows who it is *to you* before it reads your first message.
|
|
144
144
|
|
|
145
145
|
#### Claude Code
|
|
146
146
|
|
package/package.json
CHANGED
package/src/cli/README.md
CHANGED
|
@@ -15,7 +15,7 @@ ei --recent # Most recently mentioned items (no query
|
|
|
15
15
|
ei --persona "Beta" --recent # Most recently mentioned items Beta has learned
|
|
16
16
|
ei --id <id> # Look up entity by ID — or fetch a message by FQ ID
|
|
17
17
|
echo <id> | ei --id # Look up entity by ID from stdin
|
|
18
|
-
ei --install #
|
|
18
|
+
ei --install # Wire Ei into Claude Code, Cursor, and OpenCode (MCP + hooks + persona plugin)
|
|
19
19
|
ei mcp # Start the Ei MCP stdio server (for Cursor/Claude Desktop)
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -47,13 +47,17 @@ Quotes surfaced by `ei_search` include a `message_id` field in this format — p
|
|
|
47
47
|
ei --install
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
This registers Ei with Claude Code, Cursor, and OpenCode — MCP server config
|
|
50
|
+
This registers Ei with Claude Code, Cursor, and OpenCode — MCP server config, context injection hooks, and (for OpenCode) a persona identity plugin so agents know who they are before the first message:
|
|
51
51
|
|
|
52
|
-
| Tool | MCP | Context Hook |
|
|
53
|
-
|
|
54
|
-
| **Claude Code** | `~/.claude.json` | `~/.claude/settings.json` (`UserPromptSubmit`) + `~/.claude/hooks/ei-inject.ts` |
|
|
55
|
-
| **Cursor** | `~/.cursor/mcp.json` | `~/.cursor/hooks.json` (`beforeSubmitPrompt`) + `~/.cursor/hooks/ei-inject.sh` |
|
|
56
|
-
| **OpenCode** | manual (see below) |
|
|
52
|
+
| Tool | MCP | Context Hook | Persona Plugin |
|
|
53
|
+
|------|-----|-------------|----------------|
|
|
54
|
+
| **Claude Code** | `~/.claude.json` | `~/.claude/settings.json` (`UserPromptSubmit`) + `~/.claude/hooks/ei-inject.ts` | — |
|
|
55
|
+
| **Cursor** | `~/.cursor/mcp.json` | `~/.cursor/hooks.json` (`beforeSubmitPrompt`) + `~/.cursor/hooks/ei-inject.sh` | — |
|
|
56
|
+
| **OpenCode** | manual (see below) | Via Oh My OpenCode compatibility layer (reads `~/.claude/settings.json`) | `~/.config/opencode/plugins/ei-persona.ts` |
|
|
57
|
+
|
|
58
|
+
**Context hook**: fires before every message, searches Ei for topics relevant to what you just asked, injects them silently. No tool call required.
|
|
59
|
+
|
|
60
|
+
**Persona plugin** (OpenCode + [Oh My OpenCode](https://github.com/code-yeongyu/oh-my-opencode) only): injects the agent's Ei relationship record directly into the system prompt at session start — traits, working style, shared context. The agent knows who it is *to you* before it reads a word of your message.
|
|
57
61
|
|
|
58
62
|
**OpenCode MCP**: add manually to `~/.config/opencode/opencode.jsonc`:
|
|
59
63
|
|
|
@@ -82,73 +86,14 @@ Claude Code and Cursor call `ei mcp` to start the MCP stdio server. You can run
|
|
|
82
86
|
ei mcp
|
|
83
87
|
```
|
|
84
88
|
|
|
85
|
-
##
|
|
86
|
-
|
|
87
|
-
`ei --install` handles both the technical wiring **and** context injection. After running it, your agent will automatically receive recent Ei memory before every message — no tool calls required.
|
|
88
|
-
|
|
89
|
-
The snippets below are optional manual overrides if you want to customize the behavior or add targeted mid-session queries.
|
|
89
|
+
## How Automatic Context Injection Works
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
After `ei --install`, agents receive Ei context without any manual tool calls:
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
If you're running vanilla OpenCode without Oh My OpenCode, add to `~/.config/opencode/AGENTS.md`:
|
|
96
|
-
|
|
97
|
-
```markdown
|
|
98
|
-
Use the **ei** MCP to pull user context when the user references past work, mentions people
|
|
99
|
-
or preferences, or asks "how did we do X." Call `ei_search` with a natural-language query.
|
|
100
|
-
Use `ei --persona "Beta" "topic"` to scope results to what a specific persona has learned.
|
|
101
|
-
```
|
|
93
|
+
1. **Before each message** — the hook searches Ei using your prompt + recent conversation history as the query, then injects relevant topics into the conversation as `[Ei Memory Context]`. You won't see this in your chat view; the agent does.
|
|
94
|
+
2. **At session start** (OpenCode + OMO only) — the persona plugin finds the agent's Ei persona record and appends it to the system prompt as `<ei-relationship>`. The agent knows its working style, traits, and shared history with you before the session begins.
|
|
102
95
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Add to `~/.claude/CLAUDE.md` (user-level) or `CLAUDE.md` at project root:
|
|
106
|
-
|
|
107
|
-
```markdown
|
|
108
|
-
At session start, use the **ei** MCP to pull user context: call `ei_search` with a
|
|
109
|
-
natural-language query about the user's preferences, active projects, and workflow.
|
|
110
|
-
A `persona` filter is available to scope results to what a specific persona has learned.
|
|
111
|
-
Use `type: "personas"` to search for personas by name.
|
|
112
|
-
|
|
113
|
-
Use Ei when the user references past decisions, mentions people or preferences, asks
|
|
114
|
-
"how did we do X," or needs to look up a person by any name, handle, or account — people
|
|
115
|
-
results include an `identifiers` array (GitHub username, Discord handle, email, nickname, etc.)
|
|
116
|
-
covering all known accounts and aliases. Query again when they correct you or reference
|
|
117
|
-
something from a previous session.
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Cursor
|
|
121
|
-
|
|
122
|
-
Create `.cursor/rules/ei-mcp.mdc` in your project (or `~/.cursor/rules/` for user-level):
|
|
123
|
-
|
|
124
|
-
```markdown
|
|
125
|
-
---
|
|
126
|
-
description: When to use the Ei MCP for user memory and context
|
|
127
|
-
alwaysApply: true
|
|
128
|
-
---
|
|
129
|
-
# Ei MCP — User knowledge base
|
|
130
|
-
|
|
131
|
-
The **ei** MCP (server `user-ei`) is a persistent knowledge base built from the user's
|
|
132
|
-
conversations (facts, people, topics, quotes, personas).
|
|
133
|
-
|
|
134
|
-
**Use it when:**
|
|
135
|
-
- The user refers to past decisions, fixes, or "how we did X" and current chat/codebase
|
|
136
|
-
doesn't have that context.
|
|
137
|
-
- You need the user's preferences, contacts, or project conventions (e.g. who to ask for
|
|
138
|
-
access, how something was fixed).
|
|
139
|
-
- You need to look up a person by any name, handle, or account — people results include an
|
|
140
|
-
`identifiers` array (GitHub username, Discord handle, email, nickname, etc.) covering all
|
|
141
|
-
known accounts and aliases for that person.
|
|
142
|
-
- The question is about the user personally (people, workflow, prior discussions) rather
|
|
143
|
-
than only code.
|
|
144
|
-
|
|
145
|
-
**How to use:**
|
|
146
|
-
1. Call `ei_search` (server `user-ei`) with a natural-language query (or omit query and use `recent: true` to browse); optionally filter by `type` (facts, people, topics, quotes, personas) or `persona` display_name.
|
|
147
|
-
2. If you need the full record for any result, call `ei_lookup` with the entity `id` from step 1 — works for all types including personas.
|
|
148
|
-
3. If a quote result has a `message_id`, call `ei_fetch_message` with that ID and optional `before`/`after` counts to read the original conversation with context.
|
|
149
|
-
|
|
150
|
-
Prefer querying Ei before asking the user for context they may have already shared.
|
|
151
|
-
```
|
|
96
|
+
The `ei_search`, `ei_lookup`, and `ei_fetch_message` MCP tools are still available for targeted mid-session queries — use them when you want to look something up explicitly.
|
|
152
97
|
|
|
153
98
|
## MCP Tools Reference
|
|
154
99
|
|
package/src/cli.ts
CHANGED
|
@@ -90,8 +90,31 @@ Examples:
|
|
|
90
90
|
|
|
91
91
|
async function installMcpClients(): Promise<void> {
|
|
92
92
|
await installClaudeCode();
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
|
|
94
|
+
const home = process.env.HOME || "~";
|
|
95
|
+
|
|
96
|
+
const cursorDataDirs = [
|
|
97
|
+
join(home, "Library", "Application Support", "Cursor"),
|
|
98
|
+
join(home, ".config", "Cursor"),
|
|
99
|
+
join(home, "AppData", "Roaming", "Cursor"),
|
|
100
|
+
];
|
|
101
|
+
const hasCursor = (await Promise.all(cursorDataDirs.map((p) => Bun.file(join(p, "User")).exists()))).some(Boolean);
|
|
102
|
+
if (hasCursor) {
|
|
103
|
+
await installCursor();
|
|
104
|
+
} else {
|
|
105
|
+
console.log(`ℹ️ Cursor not detected — skipping Cursor install.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const opencodeDir = join(home, ".config", "opencode");
|
|
109
|
+
const hasOpenCode = await Bun.file(join(opencodeDir, "opencode.jsonc")).exists() ||
|
|
110
|
+
await Bun.file(join(opencodeDir, "opencode.json")).exists() ||
|
|
111
|
+
await Bun.file(join(opencodeDir, "opencode.db")).exists();
|
|
112
|
+
|
|
113
|
+
if (hasOpenCode) {
|
|
114
|
+
await installOpenCodePlugin();
|
|
115
|
+
} else {
|
|
116
|
+
console.log(`ℹ️ OpenCode not detected — skipping OpenCode plugin install.`);
|
|
117
|
+
}
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
async function installClaudeCode(): Promise<void> {
|
|
@@ -155,10 +178,11 @@ import { $ } from "bun";
|
|
|
155
178
|
|
|
156
179
|
const heading = \`
|
|
157
180
|
## Ei Memory Context
|
|
181
|
+
*(The user cannot see this block. It is injected automatically before their message.)*
|
|
182
|
+
*(If you reference anything from it, briefly explain where it came from — e.g. "Ei shows you've been working on X" — so the user isn't confused by knowledge that appeared from nowhere.)*
|
|
158
183
|
|
|
159
|
-
Ei is a personal knowledge base built from coding sessions, Slack, documents, and conversations.
|
|
160
|
-
The following topics MAY be relevant to your current task — use
|
|
161
|
-
MCP tools for targeted queries.
|
|
184
|
+
Ei is a personal knowledge base built from the user's coding sessions, Slack, documents, and conversations.
|
|
185
|
+
The following topics MAY be relevant to your current task — use \\\`ei_search\\\` or \\\`ei_lookup\\\` for targeted queries.
|
|
162
186
|
\`;
|
|
163
187
|
|
|
164
188
|
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
@@ -319,6 +343,128 @@ exit 0
|
|
|
319
343
|
async function installOpenCodePlugin(): Promise<void> {
|
|
320
344
|
const home = process.env.HOME || "~";
|
|
321
345
|
const opencodeDir = join(home, ".config", "opencode");
|
|
346
|
+
const pluginsDir = join(opencodeDir, "plugins");
|
|
347
|
+
const pluginPath = join(pluginsDir, "ei-persona.ts");
|
|
348
|
+
|
|
349
|
+
await Bun.$`mkdir -p ${pluginsDir}`;
|
|
350
|
+
|
|
351
|
+
const pluginContent = `import { $ } from "bun"
|
|
352
|
+
import { join } from "path"
|
|
353
|
+
import { appendFileSync } from "fs"
|
|
354
|
+
|
|
355
|
+
const sessionCache = new Map<string, string | null>()
|
|
356
|
+
const sessionFetch = new Map<string, Promise<string | null>>()
|
|
357
|
+
|
|
358
|
+
const logPath = join(process.env.EI_DATA_PATH ?? join(process.env.HOME ?? "~", ".local", "share", "ei"), "ei-persona-plugin.log")
|
|
359
|
+
|
|
360
|
+
function log(msg: string) {
|
|
361
|
+
try {
|
|
362
|
+
appendFileSync(logPath, \`[\${new Date().toISOString()}] \${msg}\\n\`)
|
|
363
|
+
} catch {}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
type PersonaTrait = { name: string; description: string; strength: number }
|
|
367
|
+
type PersonaTopic = { name: string; perspective: string; approach: string; exposure_current: number }
|
|
368
|
+
type PersonaResult = { display_name: string; base_prompt?: string; traits?: PersonaTrait[]; topics?: PersonaTopic[] }
|
|
369
|
+
|
|
370
|
+
// Pulls the agent name from the system prompt. Handles OMO's multiple formats:
|
|
371
|
+
// You are "Sisyphus" - ... (quoted, dash)
|
|
372
|
+
// You are "Sisyphus - Ultraworker" (quoted, dash in name)
|
|
373
|
+
// You are Atlas - ... (unquoted, dash)
|
|
374
|
+
// You are Hephaestus, ... (unquoted, comma)
|
|
375
|
+
export function extractAgentName(systemPrompt: string): string | null {
|
|
376
|
+
const clean = systemPrompt.replace(/[\\u200B-\\u200D\\uFEFF]/g, "")
|
|
377
|
+
const quoted = clean.match(/You are "([^"]+)"/)
|
|
378
|
+
if (quoted?.[1]) return quoted[1].trim()
|
|
379
|
+
const unquoted = clean.match(/You are ([A-Za-z][A-Za-z0-9]*)(?:\\s*[-—,]|\\s*$)/m)
|
|
380
|
+
if (unquoted?.[1]) return unquoted[1].trim()
|
|
381
|
+
return null
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Queries Ei for persona candidates and validates by name containment —
|
|
385
|
+
// tolerates OMO renaming agents without requiring a hardcoded alias map.
|
|
386
|
+
export async function resolveEiPersona(rawName: string): Promise<PersonaResult | null> {
|
|
387
|
+
try {
|
|
388
|
+
const out = await $\`bunx ei-tui@latest personas -n 5 \${rawName}\`.text()
|
|
389
|
+
const candidates = JSON.parse(out.trim()) as PersonaResult[]
|
|
390
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return null
|
|
391
|
+
const rawLower = rawName.toLowerCase()
|
|
392
|
+
const match = candidates.find((p) => {
|
|
393
|
+
const nameLower = p.display_name.toLowerCase()
|
|
394
|
+
return rawLower.includes(nameLower) || nameLower.includes(rawLower)
|
|
395
|
+
})
|
|
396
|
+
return match ?? null
|
|
397
|
+
} catch {
|
|
398
|
+
return null
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function buildEiRelationshipBlock(persona: PersonaResult): string {
|
|
403
|
+
const strongTraits = (persona.traits ?? [])
|
|
404
|
+
.filter((t) => t.strength >= 0.7)
|
|
405
|
+
.sort((a, b) => b.strength - a.strength)
|
|
406
|
+
.map((t) => \`**\${t.name}** (\${Math.round(t.strength * 100)}%): \${t.description}\`)
|
|
407
|
+
.join("\\n")
|
|
408
|
+
const sortedTopics = [...(persona.topics ?? [])]
|
|
409
|
+
.sort((a, b) => b.exposure_current - a.exposure_current)
|
|
410
|
+
.map((t) => \`**\${t.name}**: \${t.perspective} — \${t.approach}\`)
|
|
411
|
+
.join("\\n")
|
|
412
|
+
return [
|
|
413
|
+
"<!-- ei-relationship-injected -->",
|
|
414
|
+
"<ei-relationship>",
|
|
415
|
+
"## Ei: Relationship Context",
|
|
416
|
+
"",
|
|
417
|
+
persona.base_prompt ?? "",
|
|
418
|
+
"",
|
|
419
|
+
"### Working Style",
|
|
420
|
+
strongTraits || "(no traits above threshold)",
|
|
421
|
+
"",
|
|
422
|
+
"### Shared Context",
|
|
423
|
+
sortedTopics || "(no topics)",
|
|
424
|
+
"</ei-relationship>",
|
|
425
|
+
].join("\\n")
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export default async function EiPersonaPlugin() {
|
|
429
|
+
return {
|
|
430
|
+
name: "ei-persona",
|
|
431
|
+
"experimental.chat.system.transform": async (
|
|
432
|
+
input: { sessionID?: string; model: { id: string; providerID: string; [key: string]: unknown } },
|
|
433
|
+
output: { system: string[] },
|
|
434
|
+
): Promise<void> => {
|
|
435
|
+
const rawName = extractAgentName(output.system[0] ?? "")
|
|
436
|
+
if (!rawName) return
|
|
437
|
+
|
|
438
|
+
const cacheKey = \`\${input.sessionID ?? "unknown"}:\${rawName}\`
|
|
439
|
+
|
|
440
|
+
if (sessionCache.has(cacheKey)) {
|
|
441
|
+
const cached = sessionCache.get(cacheKey) ?? null
|
|
442
|
+
if (cached !== null && !output.system[0].includes("<!-- ei-relationship-injected -->"))
|
|
443
|
+
output.system[0] = output.system[0] + "\\n\\n" + cached
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!sessionFetch.has(cacheKey)) {
|
|
448
|
+
sessionFetch.set(cacheKey, (async () => {
|
|
449
|
+
const persona = await resolveEiPersona(rawName)
|
|
450
|
+
if (!persona) return null
|
|
451
|
+
log(\`ei-persona: injecting \${persona.display_name}\`)
|
|
452
|
+
return buildEiRelationshipBlock(persona)
|
|
453
|
+
})())
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const block = await sessionFetch.get(cacheKey)!
|
|
457
|
+
sessionCache.set(cacheKey, block)
|
|
458
|
+
if (block !== null && !output.system[0].includes("<!-- ei-relationship-injected -->"))
|
|
459
|
+
output.system[0] = output.system[0] + "\\n\\n" + block
|
|
460
|
+
},
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
`;
|
|
464
|
+
|
|
465
|
+
await Bun.write(pluginPath, pluginContent);
|
|
466
|
+
console.log(`✓ Installed Ei persona plugin to ${pluginPath}`);
|
|
467
|
+
|
|
322
468
|
const omoCandidates = [
|
|
323
469
|
join(opencodeDir, "oh-my-opencode.json"),
|
|
324
470
|
join(opencodeDir, "oh-my-opencode.jsonc"),
|
|
@@ -329,22 +475,18 @@ async function installOpenCodePlugin(): Promise<void> {
|
|
|
329
475
|
];
|
|
330
476
|
const hasOmo = (await Promise.all(omoCandidates.map((p) => Bun.file(p).exists()))).some(Boolean);
|
|
331
477
|
|
|
332
|
-
if (hasOmo) {
|
|
333
|
-
console.log(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
console.log(`
|
|
338
|
-
ℹ️ OpenCode detected without Oh My OpenCode.
|
|
339
|
-
The ~/.claude/settings.json UserPromptSubmit hook only fires in Claude Code.
|
|
340
|
-
For the same context injection in OpenCode, we recommend:
|
|
478
|
+
if (!hasOmo) {
|
|
479
|
+
console.log(`
|
|
480
|
+
ℹ️ Oh My OpenCode not detected.
|
|
481
|
+
The Ei persona plugin is installed, but context injection (hook) requires OMO.
|
|
482
|
+
For full Ei integration in OpenCode, we recommend:
|
|
341
483
|
|
|
342
484
|
bunx oh-my-opencode install
|
|
343
485
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
automatically via its Claude Code compatibility layer.
|
|
486
|
+
OMO picks up the Ei UserPromptSubmit hook automatically via its Claude Code
|
|
487
|
+
compatibility layer.
|
|
347
488
|
`);
|
|
489
|
+
}
|
|
348
490
|
}
|
|
349
491
|
|
|
350
492
|
async function getRecentSessionMessages(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, ContextStatus, type CeremonyConfig, type PersonaTopic, type Topic } from "../types.js";
|
|
1
|
+
import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, ContextStatus, type CeremonyConfig, type PersonaTopic, type Topic, type Message } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
3
|
import { normalizeRoomMessages } from "../handlers/utils.js";
|
|
4
4
|
import { applyDecayToValue } from "../utils/index.js";
|
|
@@ -168,9 +168,9 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
|
|
|
168
168
|
* If any ceremony_progress items remain in the queue, does nothing — more work pending.
|
|
169
169
|
* Phase 1: Dedup → Phase 2: Expose → Phase 3: EventSummary → Decay → Phase 4: Person Rewrite → Topic Rewrite (fire-and-forget)
|
|
170
170
|
*/
|
|
171
|
-
export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
171
|
+
export function handleCeremonyProgress(state: StateManager, lastPhase: number): { wroteEiWarning: boolean } {
|
|
172
172
|
if (state.queue_hasPendingCeremonies()) {
|
|
173
|
-
return; // Still processing ceremony items
|
|
173
|
+
return { wroteEiWarning: false }; // Still processing ceremony items
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
if (lastPhase === 1) {
|
|
@@ -232,13 +232,13 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
232
232
|
console.log(`[ceremony:expose] Queued room persona topic rating: ${personaForRoom.display_name} in "${room.display_name}" (${unprocessedRaw.length} messages)`);
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
|
-
return;
|
|
235
|
+
return { wroteEiWarning: false };
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
if (lastPhase === 4) {
|
|
239
239
|
console.log("[ceremony:progress] Person Rewrite complete, starting Topic Rewrite");
|
|
240
240
|
queueTopicRewritePhase(state);
|
|
241
|
-
return;
|
|
241
|
+
return { wroteEiWarning: false };
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
if (lastPhase === 2) {
|
|
@@ -251,7 +251,7 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
251
251
|
console.log("[ceremony:progress] No event summary work, advancing to Decay");
|
|
252
252
|
handleCeremonyProgress(state, 3);
|
|
253
253
|
}
|
|
254
|
-
return;
|
|
254
|
+
return { wroteEiWarning: false };
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// Phase 3 (EventSummary) complete → advance to Decay/Prune then Person Rewrite (phase 4)
|
|
@@ -285,9 +285,10 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
// Reflection phase: fire-and-forget critic calls for persona person records above threshold
|
|
288
|
-
queueReflectionPhase(state);
|
|
288
|
+
const wroteEiWarning = queueReflectionPhase(state);
|
|
289
289
|
|
|
290
290
|
console.log("[ceremony:progress] Ceremony Decay complete");
|
|
291
|
+
return { wroteEiWarning };
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
// =============================================================================
|
|
@@ -446,7 +447,8 @@ export function runHumanCeremony(state: StateManager): void {
|
|
|
446
447
|
// REWRITE PHASE (fire-and-forget — queues Low-priority Phase 1 scans)
|
|
447
448
|
// =============================================================================
|
|
448
449
|
|
|
449
|
-
const
|
|
450
|
+
const PERSON_REWRITE_DESCRIPTION_THRESHOLD = 1000;
|
|
451
|
+
const TOPIC_REWRITE_DESCRIPTION_THRESHOLD = 750;
|
|
450
452
|
|
|
451
453
|
/**
|
|
452
454
|
* Forces an unconditional, threshold-bypassing Person scan on Apply/Dismiss.
|
|
@@ -494,7 +496,7 @@ export function queuePersonRewritePhase(state: StateManager, options?: { ceremon
|
|
|
494
496
|
i => i.type.toLowerCase() === 'ei persona'
|
|
495
497
|
);
|
|
496
498
|
return !isPersonaLinked
|
|
497
|
-
&& (person.description?.length ?? 0) >
|
|
499
|
+
&& (person.description?.length ?? 0) > PERSON_REWRITE_DESCRIPTION_THRESHOLD;
|
|
498
500
|
});
|
|
499
501
|
|
|
500
502
|
const alreadyChecked = allCandidates.filter(p => {
|
|
@@ -520,7 +522,7 @@ export function queuePersonRewritePhase(state: StateManager, options?: { ceremon
|
|
|
520
522
|
return;
|
|
521
523
|
}
|
|
522
524
|
|
|
523
|
-
console.log(`[ceremony:rewrite] Found ${personsToScan.length} person(s) above ${
|
|
525
|
+
console.log(`[ceremony:rewrite] Found ${personsToScan.length} person(s) above ${PERSON_REWRITE_DESCRIPTION_THRESHOLD} chars — queueing person rewrite scans`);
|
|
524
526
|
|
|
525
527
|
for (const person of personsToScan) {
|
|
526
528
|
const prompt = buildPersonRewriteScanPrompt({ item: person, itemType: "person" });
|
|
@@ -552,7 +554,7 @@ export function queueTopicRewritePhase(state: StateManager): void {
|
|
|
552
554
|
|
|
553
555
|
const human = state.getHuman();
|
|
554
556
|
const allCandidateTopics = human.topics.filter(topic =>
|
|
555
|
-
(topic.description?.length ?? 0) >
|
|
557
|
+
(topic.description?.length ?? 0) > TOPIC_REWRITE_DESCRIPTION_THRESHOLD
|
|
556
558
|
);
|
|
557
559
|
|
|
558
560
|
const alreadyCheckedTopics = allCandidateTopics.filter(t => {
|
|
@@ -578,7 +580,7 @@ export function queueTopicRewritePhase(state: StateManager): void {
|
|
|
578
580
|
return;
|
|
579
581
|
}
|
|
580
582
|
|
|
581
|
-
console.log(`[ceremony:rewrite] Found ${topicsToScan.length} topic(s) above ${
|
|
583
|
+
console.log(`[ceremony:rewrite] Found ${topicsToScan.length} topic(s) above ${TOPIC_REWRITE_DESCRIPTION_THRESHOLD} chars — queueing topic rewrite scans`);
|
|
582
584
|
|
|
583
585
|
for (const topic of topicsToScan) {
|
|
584
586
|
const prompt = buildTopicRewriteScanPrompt({ item: topic, itemType: "topic" });
|
|
@@ -615,16 +617,43 @@ function queueEventSummaryForAll(state: StateManager, options?: ExtractionOption
|
|
|
615
617
|
console.log(`[ceremony:event] Queued event summary scans for ${activePersonas.length} personas (${totalQueued} total chunks)`);
|
|
616
618
|
}
|
|
617
619
|
|
|
618
|
-
function queueReflectionPhase(state: StateManager):
|
|
620
|
+
function queueReflectionPhase(state: StateManager): boolean {
|
|
619
621
|
const personas = state.persona_getAll().filter(p =>
|
|
620
622
|
!p.is_paused && !p.is_archived && !p.is_static
|
|
621
623
|
);
|
|
622
624
|
|
|
625
|
+
const human = state.getHuman();
|
|
623
626
|
let queued = 0;
|
|
627
|
+
let wroteEiWarning = false;
|
|
628
|
+
|
|
624
629
|
for (const persona of personas) {
|
|
625
|
-
const
|
|
626
|
-
|
|
630
|
+
const linkedRecords = human.people.filter(p =>
|
|
631
|
+
p.identifiers?.some(i => i.type.toLowerCase() === 'ei persona' && i.value === persona.id)
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
if (linkedRecords.length === 0) continue;
|
|
635
|
+
|
|
636
|
+
const overThreshold = linkedRecords.filter(p => (p.description?.length ?? 0) > PERSON_LOG_REFLECTION_THRESHOLD);
|
|
637
|
+
if (overThreshold.length === 0) continue;
|
|
638
|
+
|
|
639
|
+
if (linkedRecords.length > 1) {
|
|
640
|
+
const names = linkedRecords.map(p => `"${p.name}"`).join(" and ");
|
|
641
|
+
console.log(`[ceremony:reflection] ${persona.display_name} is linked to multiple person records (${names}) — skipping reflection, writing Ei warning`);
|
|
642
|
+
|
|
643
|
+
const warning: Message = {
|
|
644
|
+
id: crypto.randomUUID(),
|
|
645
|
+
role: "system",
|
|
646
|
+
content: `During today's ceremony, I noticed that **${persona.display_name}** is connected to multiple person records: ${names}. This might be intentional — if you created a composite persona — but if not, you may want to check the identifiers on those records. Reflection for ${persona.display_name} has been paused until this is resolved.`,
|
|
647
|
+
timestamp: new Date().toISOString(),
|
|
648
|
+
read: false,
|
|
649
|
+
context_status: ContextStatus.Always,
|
|
650
|
+
};
|
|
651
|
+
state.messages_append("ei", warning);
|
|
652
|
+
wroteEiWarning = true;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
627
655
|
|
|
656
|
+
const personRecord = linkedRecords[0];
|
|
628
657
|
const prompt = buildReflectionCriticPrompt({
|
|
629
658
|
persona_identity: {
|
|
630
659
|
name: persona.display_name,
|
|
@@ -650,7 +679,9 @@ function queueReflectionPhase(state: StateManager): void {
|
|
|
650
679
|
console.log(`[ceremony:reflection] Queued critic for ${persona.display_name} (person log: ${personRecord.description?.length} chars)`);
|
|
651
680
|
}
|
|
652
681
|
|
|
653
|
-
if (queued === 0) {
|
|
682
|
+
if (queued === 0 && !wroteEiWarning) {
|
|
654
683
|
console.log("[ceremony:reflection] No persona person records above threshold — skipping");
|
|
655
684
|
}
|
|
685
|
+
|
|
686
|
+
return wroteEiWarning;
|
|
656
687
|
}
|
package/src/core/processor.ts
CHANGED
|
@@ -36,7 +36,8 @@ import { handlers } from "./handlers/index.js";
|
|
|
36
36
|
import { normalizeRoomMessages, getMessageContent } from "./handlers/utils.js";
|
|
37
37
|
import { sanitizeEiPersonaIdentifiers } from "./utils/identifier-utils.js";
|
|
38
38
|
import { ContextStatus as ContextStatusEnum, RoomMode } from "./types.js";
|
|
39
|
-
import { registerFindMemoryExecutor, registerFetchMemoryExecutor, registerFetchMessageExecutor, registerFileReadExecutor, SYSTEM_TOOLS } from "./tools/index.js";
|
|
39
|
+
import { registerFindMemoryExecutor, registerFetchMemoryExecutor, registerFetchMessageExecutor, registerFileReadExecutor, registerPersonaNoteExecutors, buildPersonaNoteTools, SYSTEM_TOOLS } from "./tools/index.js";
|
|
40
|
+
import { createAddNoteExecutor, createClearNoteExecutor } from "./tools/builtin/persona-notes.js";
|
|
40
41
|
import { createFindMemoryExecutor } from "./tools/builtin/find-memory.js";
|
|
41
42
|
import { createFetchMemoryExecutor } from "./tools/builtin/fetch-memory.js";
|
|
42
43
|
import { createFetchMessageExecutor } from "./tools/builtin/fetch-message.js";
|
|
@@ -252,6 +253,10 @@ export class Processor {
|
|
|
252
253
|
this.seedSettings();
|
|
253
254
|
registerFindMemoryExecutor(createFindMemoryExecutor(this.searchHumanData.bind(this), this.getPersonaList.bind(this), this.stateManager.getHuman.bind(this.stateManager)));
|
|
254
255
|
registerFetchMemoryExecutor(createFetchMemoryExecutor(this.stateManager.getHuman.bind(this.stateManager)));
|
|
256
|
+
registerPersonaNoteExecutors(
|
|
257
|
+
createAddNoteExecutor(this.stateManager.persona_getById.bind(this.stateManager), this.stateManager.persona_update.bind(this.stateManager)),
|
|
258
|
+
createClearNoteExecutor(this.stateManager.persona_getById.bind(this.stateManager), this.stateManager.persona_update.bind(this.stateManager))
|
|
259
|
+
);
|
|
255
260
|
if (this.isTUI) {
|
|
256
261
|
await registerFileReadExecutor();
|
|
257
262
|
const retrievalPath = "../cli/retrieval.js";
|
|
@@ -1414,7 +1419,7 @@ const toolNextSteps = new Set([
|
|
|
1414
1419
|
t.name === "find_memory" || t.name === "fetch_memory" || t.name === "fetch_message"
|
|
1415
1420
|
);
|
|
1416
1421
|
} else if (toolNextSteps.has(request.next_step) && toolPersonaId) {
|
|
1417
|
-
tools = [...SYSTEM_TOOLS, ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
|
|
1422
|
+
tools = [...SYSTEM_TOOLS, ...buildPersonaNoteTools(toolPersonaId), ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
|
|
1418
1423
|
}
|
|
1419
1424
|
|
|
1420
1425
|
// Auto-inject each handler's dedicated submit tool — infrastructure, not user-visible.
|
|
@@ -2103,7 +2108,10 @@ const toolNextSteps = new Set([
|
|
|
2103
2108
|
}
|
|
2104
2109
|
|
|
2105
2110
|
if (typeof response.request.data.ceremony_progress === "number") {
|
|
2106
|
-
handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
|
|
2111
|
+
const ceremonyResult = handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
|
|
2112
|
+
if (ceremonyResult.wroteEiWarning) {
|
|
2113
|
+
this.interface.onMessageAdded?.("ei");
|
|
2114
|
+
}
|
|
2107
2115
|
}
|
|
2108
2116
|
|
|
2109
2117
|
if (response.request.next_step === LLMNextStep.HandleDocumentSegmentation) {
|
|
@@ -302,6 +302,7 @@ export async function buildResponsePromptData(
|
|
|
302
302
|
interested_topics: persona.topics.filter(t => t.exposure_desired - t.exposure_current > 0.2),
|
|
303
303
|
include_message_timestamps: persona.include_message_timestamps,
|
|
304
304
|
pending_update: persona.pending_update,
|
|
305
|
+
notes: persona.notes,
|
|
305
306
|
},
|
|
306
307
|
human: filteredHuman,
|
|
307
308
|
visible_personas: visiblePersonas,
|
|
@@ -395,6 +396,7 @@ export async function buildRoomResponsePromptData(
|
|
|
395
396
|
traits: respondingPersona.traits,
|
|
396
397
|
topics: respondingPersona.topics,
|
|
397
398
|
include_message_timestamps: respondingPersona.include_message_timestamps,
|
|
399
|
+
notes: respondingPersona.notes,
|
|
398
400
|
},
|
|
399
401
|
other_participants: otherParticipants,
|
|
400
402
|
human: filteredHuman,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ToolExecutor } from "../types.js";
|
|
2
|
+
import type { PersonaEntity } from "../../types.js";
|
|
3
|
+
|
|
4
|
+
export const NOTES_MAX = 20;
|
|
5
|
+
|
|
6
|
+
type GetPersona = (id: string) => PersonaEntity | null;
|
|
7
|
+
type UpdatePersona = (id: string, updates: Partial<PersonaEntity>) => boolean;
|
|
8
|
+
|
|
9
|
+
export function createAddNoteExecutor(getPersona: GetPersona, updatePersona: UpdatePersona): ToolExecutor {
|
|
10
|
+
return {
|
|
11
|
+
name: "add_note",
|
|
12
|
+
|
|
13
|
+
async execute(args: Record<string, unknown>, config?: Record<string, string>): Promise<string> {
|
|
14
|
+
const personaId = config?.persona_id ?? "";
|
|
15
|
+
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
16
|
+
console.log(`[add_note] persona="${personaId}" text="${text.slice(0, 60)}"`);
|
|
17
|
+
|
|
18
|
+
if (!personaId) {
|
|
19
|
+
return JSON.stringify({ error: "Tool misconfigured: missing persona_id" });
|
|
20
|
+
}
|
|
21
|
+
if (!text) {
|
|
22
|
+
return JSON.stringify({ error: "Missing required argument: text" });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const persona = getPersona(personaId);
|
|
26
|
+
if (!persona) {
|
|
27
|
+
return JSON.stringify({ error: "Persona not found" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const notes = [...(persona.notes ?? [])];
|
|
31
|
+
|
|
32
|
+
if (notes.length >= NOTES_MAX) {
|
|
33
|
+
notes.shift();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
notes.push(text);
|
|
37
|
+
updatePersona(personaId, { notes });
|
|
38
|
+
|
|
39
|
+
const index = notes.length;
|
|
40
|
+
console.log(`[add_note] added note at position ${index}/${NOTES_MAX}`);
|
|
41
|
+
return JSON.stringify({ added: true, index, total: notes.length });
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createClearNoteExecutor(getPersona: GetPersona, updatePersona: UpdatePersona): ToolExecutor {
|
|
47
|
+
return {
|
|
48
|
+
name: "clear_note",
|
|
49
|
+
|
|
50
|
+
async execute(args: Record<string, unknown>, config?: Record<string, string>): Promise<string> {
|
|
51
|
+
const personaId = config?.persona_id ?? "";
|
|
52
|
+
const index = typeof args.index === "number" ? args.index : NaN;
|
|
53
|
+
console.log(`[clear_note] persona="${personaId}" index=${index}`);
|
|
54
|
+
|
|
55
|
+
if (!personaId) {
|
|
56
|
+
return JSON.stringify({ error: "Tool misconfigured: missing persona_id" });
|
|
57
|
+
}
|
|
58
|
+
if (!Number.isInteger(index) || index < 1) {
|
|
59
|
+
return JSON.stringify({ error: "index must be an integer >= 1" });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const persona = getPersona(personaId);
|
|
63
|
+
if (!persona) {
|
|
64
|
+
return JSON.stringify({ error: "Persona not found" });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const notes = [...(persona.notes ?? [])];
|
|
68
|
+
const zeroIdx = index - 1;
|
|
69
|
+
|
|
70
|
+
if (zeroIdx >= notes.length) {
|
|
71
|
+
return JSON.stringify({ error: `No note at index ${index} (total: ${notes.length})` });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
notes.splice(zeroIdx, 1);
|
|
75
|
+
updatePersona(personaId, { notes });
|
|
76
|
+
|
|
77
|
+
console.log(`[clear_note] removed note at index ${index}, remaining=${notes.length}`);
|
|
78
|
+
return JSON.stringify({ cleared: true, index, remaining: notes.length });
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
package/src/core/tools/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { tavilyWebSearchExecutor, tavilyNewsSearchExecutor } from "./builtin/web
|
|
|
12
12
|
import { currentlyPlayingExecutor } from "./builtin/currently-playing.js";
|
|
13
13
|
import { likedSongsExecutor } from "./builtin/spotify-liked-songs.js";
|
|
14
14
|
import { webFetchExecutor } from "./builtin/web-fetch.js";
|
|
15
|
+
import { NOTES_MAX } from "./builtin/persona-notes.js";
|
|
15
16
|
// file-read and list-directory are Node-only — imported lazily via registerFileReadExecutor() to avoid
|
|
16
17
|
// file-read and list-directory are Node-only — imported lazily via registerFileReadExecutor() to avoid
|
|
17
18
|
|
|
@@ -128,6 +129,61 @@ export function registerFetchMessageExecutor(executor: ToolExecutor): void {
|
|
|
128
129
|
executorRegistry.set(executor.name, executor);
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
export function registerPersonaNoteExecutors(executor1: ToolExecutor, executor2: ToolExecutor): void {
|
|
133
|
+
executorRegistry.set(executor1.name, executor1);
|
|
134
|
+
executorRegistry.set(executor2.name, executor2);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build per-request ToolDefinition objects for the persona notes tools, injecting the
|
|
139
|
+
* current personaId via config so the shared executor knows which persona to update.
|
|
140
|
+
*/
|
|
141
|
+
export function buildPersonaNoteTools(personaId: string): ToolDefinition[] {
|
|
142
|
+
const now = new Date(0).toISOString();
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
id: `builtin-add-note-${personaId}`,
|
|
146
|
+
provider_id: "ei",
|
|
147
|
+
name: "add_note",
|
|
148
|
+
display_name: "Add Note",
|
|
149
|
+
description: `In Ei, your system prompt can change from one turn to the next — Ei is constantly trying to provide you relevant, up-to-date information about the user and the world. If you see something in your system prompt that you don't immediately want to bring up, but want to remember, use this tool to record it for later. Additionally, if you need to remember something but cannot or should not say it directly in conversation, you can use this tool to make a note as well. Notes appear in your system prompt as a numbered list so you always see them. Limit: ${NOTES_MAX} notes (oldest evicted when full).`,
|
|
150
|
+
input_schema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
text: { type: "string", description: "The note to remember. Keep it concise." },
|
|
154
|
+
},
|
|
155
|
+
required: ["text"],
|
|
156
|
+
},
|
|
157
|
+
config: { persona_id: personaId },
|
|
158
|
+
runtime: "any",
|
|
159
|
+
builtin: true,
|
|
160
|
+
enabled: true,
|
|
161
|
+
created_at: now,
|
|
162
|
+
max_calls_per_interaction: 5,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: `builtin-clear-note-${personaId}`,
|
|
166
|
+
provider_id: "ei",
|
|
167
|
+
name: "clear_note",
|
|
168
|
+
display_name: "Clear Note",
|
|
169
|
+
description: "Remove a note from your scratchpad by its 1-based index (matching the numbered list in your system prompt). Use when you no longer need to track something — e.g., after you've addressed it in conversation.",
|
|
170
|
+
input_schema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
index: { type: "number", description: "1-based index of the note to remove" },
|
|
174
|
+
},
|
|
175
|
+
required: ["index"],
|
|
176
|
+
},
|
|
177
|
+
config: { persona_id: personaId },
|
|
178
|
+
runtime: "any",
|
|
179
|
+
builtin: true,
|
|
180
|
+
enabled: true,
|
|
181
|
+
created_at: now,
|
|
182
|
+
max_calls_per_interaction: 5,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
131
187
|
/**
|
|
132
188
|
* Register the file_read, list_directory, directory_tree, search_files, grep, and get_file_info
|
|
133
189
|
* executors — called by Processor on TUI/Node only.
|
|
@@ -75,8 +75,7 @@ export interface Person extends DataItemBase {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export interface Quote {
|
|
78
|
-
|
|
79
|
-
id: string; // UUID (use crypto.randomUUID())
|
|
78
|
+
id: string; // UUID — stable identity for CRUD operations (use crypto.randomUUID())
|
|
80
79
|
message_id: string | null; // FK to Message.id (nullable for manual quotes)
|
|
81
80
|
data_item_ids: string[]; // FK[] to DataItemBase.id
|
|
82
81
|
persona_groups: string[]; // Visibility groups
|
|
@@ -185,6 +185,7 @@ export interface PersonaEntity {
|
|
|
185
185
|
avatar_emoji?: string; // Single emoji character used as avatar in place of initials.
|
|
186
186
|
avatar_image?: string; // Base64-encoded 64×64 image used as avatar (takes priority over avatar_emoji).
|
|
187
187
|
preferred_theme?: string; // Theme ID (built-in name or ThemeDefinition.id). Applied to chat panel when this persona is active.
|
|
188
|
+
notes?: string[]; // Private scratchpad — up to 20 short-term notes visible in the system prompt. Oldest evicted when full.
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
export interface PersonaCreationInput {
|
|
@@ -27,7 +27,6 @@ export interface SlackImportResult {
|
|
|
27
27
|
function ensureSlackPersona(stateManager: StateManager, eiInterface: Ei_Interface): PersonaEntity {
|
|
28
28
|
const existing = stateManager.persona_getAll().find(p => p.display_name === "Slack");
|
|
29
29
|
if (existing) {
|
|
30
|
-
if (existing.is_archived) stateManager.persona_unarchive(existing.id);
|
|
31
30
|
return existing;
|
|
32
31
|
}
|
|
33
32
|
const persona: PersonaEntity = {
|
|
@@ -11,6 +11,7 @@ import type { ResponsePromptData, PromptOutput } from "./types.js";
|
|
|
11
11
|
import { formatCurrentTime } from "../../core/format-utils.js";
|
|
12
12
|
import {
|
|
13
13
|
buildIdentitySection,
|
|
14
|
+
buildNotesSection,
|
|
14
15
|
buildGuidelinesSection,
|
|
15
16
|
buildTraitsSection,
|
|
16
17
|
buildTopicsSection,
|
|
@@ -44,6 +45,7 @@ Your role is unique among personas:
|
|
|
44
45
|
- Consider their traits when building your responses more than the current conversation history
|
|
45
46
|
- You encourage human-to-human connection when appropriate`;
|
|
46
47
|
|
|
48
|
+
const notesSection = buildNotesSection(data.persona.notes);
|
|
47
49
|
const guidelines = buildGuidelinesSection("ei");
|
|
48
50
|
const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
|
|
49
51
|
const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
|
|
@@ -63,7 +65,7 @@ Your role is unique among personas:
|
|
|
63
65
|
: "";
|
|
64
66
|
|
|
65
67
|
return `${identity}
|
|
66
|
-
|
|
68
|
+
${notesSection ? `\n${notesSection}` : ""}
|
|
67
69
|
${guidelines}
|
|
68
70
|
|
|
69
71
|
${yourTraits}
|
|
@@ -93,6 +95,7 @@ ${conversationState}
|
|
|
93
95
|
*/
|
|
94
96
|
function buildStandardSystemPrompt(data: ResponsePromptData): string {
|
|
95
97
|
const identity = buildIdentitySection(data.persona);
|
|
98
|
+
const notesSection = buildNotesSection(data.persona.notes);
|
|
96
99
|
const guidelines = buildGuidelinesSection(data.persona.name);
|
|
97
100
|
const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
|
|
98
101
|
const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
|
|
@@ -111,7 +114,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
|
|
|
111
114
|
: "";
|
|
112
115
|
|
|
113
116
|
return `${identity}
|
|
114
|
-
|
|
117
|
+
${notesSection ? `\n${notesSection}` : ""}
|
|
115
118
|
${guidelines}
|
|
116
119
|
|
|
117
120
|
${yourTraits}
|
|
@@ -33,6 +33,16 @@ export function buildIdentitySection(persona: ResponsePromptData["persona"]): st
|
|
|
33
33
|
${description}`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// NOTES SECTION
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
export function buildNotesSection(notes: string[] | undefined): string {
|
|
41
|
+
if (!notes || notes.length === 0) return "";
|
|
42
|
+
const list = notes.map((n, i) => `${i + 1}. ${n}`).join("\n");
|
|
43
|
+
return `## Your Notes\n\nThings you've chosen to remember. Use \`clear_note\` once you've addressed something.\n\n${list}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
// =============================================================================
|
|
37
47
|
// GUIDELINES SECTION
|
|
38
48
|
// =============================================================================
|
|
@@ -33,6 +33,7 @@ export interface ResponsePromptData {
|
|
|
33
33
|
include_message_timestamps?: boolean;
|
|
34
34
|
/** Proposed identity revision pending human review. Persona carries this as ambient self-awareness — no critique, just the proposed changes. */
|
|
35
35
|
pending_update?: PersonaEntity["pending_update"];
|
|
36
|
+
notes?: string[];
|
|
36
37
|
};
|
|
37
38
|
human: {
|
|
38
39
|
name: string;
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
buildHumanSection,
|
|
22
22
|
buildQuotesSection,
|
|
23
23
|
buildToolsSection,
|
|
24
|
+
buildNotesSection,
|
|
24
25
|
} from "../response/sections.js";
|
|
25
26
|
|
|
26
27
|
export type {
|
|
@@ -42,6 +43,7 @@ export function buildRoomResponsePrompt(data: RoomResponsePromptData): PromptOut
|
|
|
42
43
|
const aliasText = persona.aliases.length > 0 ? ` (also known as: ${persona.aliases.join(", ")})` : "";
|
|
43
44
|
|
|
44
45
|
const identity = `You are ${name}${aliasText}.\n\n${desc}`;
|
|
46
|
+
const notesSection = buildNotesSection(persona.notes);
|
|
45
47
|
const traits = buildRoomTraitsSection(persona.traits);
|
|
46
48
|
const topics = buildRoomTopicsSection(persona.topics);
|
|
47
49
|
const humanSection = buildHumanSection(human);
|
|
@@ -57,6 +59,7 @@ export function buildRoomResponsePrompt(data: RoomResponsePromptData): PromptOut
|
|
|
57
59
|
|
|
58
60
|
const system = [
|
|
59
61
|
identity,
|
|
62
|
+
notesSection,
|
|
60
63
|
traits,
|
|
61
64
|
topics,
|
|
62
65
|
humanSection,
|
package/tui/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Enable any or all three in `/settings`. They work independently and feed into th
|
|
|
39
39
|
|
|
40
40
|
Sessions are processed oldest-first, one per queue cycle. On first run Ei works through your backlog gradually — it won't flood your LLM provider.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
All three tools also support reading Ei's knowledge back out via the [CLI tool](../src/cli/README.md) — run `ei --install` to wire up automatic context injection (hooks + persona plugin) so your coding agents receive relevant Ei memory before every message without any manual tool calls.
|
|
43
43
|
|
|
44
44
|
## Slack Integration
|
|
45
45
|
|