ei-tui 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/package.json +1 -1
- package/src/cli/README.md +21 -14
- package/src/cli/commands/personas.ts +12 -0
- package/src/cli/mcp.ts +6 -5
- package/src/cli/retrieval.ts +86 -8
- package/src/cli.ts +21 -19
- package/src/core/constants/seed-traits.ts +29 -0
- package/src/core/context-utils.ts +1 -0
- package/src/core/format-utils.ts +23 -0
- package/src/core/handlers/human-matching.ts +53 -35
- package/src/core/handlers/index.ts +5 -0
- package/src/core/handlers/persona-preview.ts +7 -0
- package/src/core/handlers/persona-topics.ts +3 -2
- package/src/core/handlers/rooms.ts +176 -0
- package/src/core/handlers/utils.ts +55 -3
- package/src/core/heartbeat-manager.ts +3 -1
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +13 -9
- package/src/core/orchestrators/human-extraction.ts +15 -2
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/orchestrators/persona-generation.ts +4 -0
- package/src/core/orchestrators/persona-topics.ts +2 -1
- package/src/core/orchestrators/room-extraction.ts +318 -0
- package/src/core/persona-manager.ts +16 -5
- package/src/core/personas/opencode-agent.ts +12 -2
- package/src/core/processor.ts +520 -4
- package/src/core/prompt-context-builder.ts +89 -5
- package/src/core/queue-processor.ts +68 -8
- package/src/core/room-manager.ts +408 -0
- package/src/core/state/index.ts +1 -0
- package/src/core/state/personas.ts +12 -2
- package/src/core/state/queue.ts +2 -2
- package/src/core/state/rooms.ts +182 -0
- package/src/core/state-manager.ts +124 -2
- package/src/core/tool-manager.ts +1 -1
- package/src/core/tools/index.ts +15 -0
- package/src/core/types/entities.ts +1 -0
- package/src/core/types/enums.ts +11 -0
- package/src/core/types/integrations.ts +10 -2
- package/src/core/types/llm.ts +3 -0
- package/src/core/types/rooms.ts +59 -0
- package/src/core/types.ts +1 -0
- package/src/core/utils/decay.ts +14 -8
- package/src/core/utils/exposure.ts +14 -0
- package/src/integrations/claude-code/importer.ts +23 -10
- package/src/integrations/cursor/importer.ts +22 -10
- package/src/integrations/opencode/importer.ts +30 -13
- package/src/prompts/ceremony/dedup.ts +2 -2
- package/src/prompts/generation/from-person.ts +85 -0
- package/src/prompts/generation/index.ts +2 -0
- package/src/prompts/generation/persona.ts +14 -10
- package/src/prompts/generation/seeds.ts +4 -29
- package/src/prompts/generation/types.ts +13 -0
- package/src/prompts/heartbeat/check.ts +1 -1
- package/src/prompts/heartbeat/ei.ts +4 -4
- package/src/prompts/heartbeat/types.ts +1 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/message-utils.ts +2 -2
- package/src/prompts/persona/topics-match.ts +7 -6
- package/src/prompts/persona/topics-update.ts +8 -11
- package/src/prompts/persona/types.ts +2 -1
- package/src/prompts/response/index.ts +4 -11
- package/src/prompts/response/sections.ts +22 -10
- package/src/prompts/response/types.ts +6 -0
- package/src/prompts/room/index.ts +115 -0
- package/src/prompts/room/sections.ts +150 -0
- package/src/prompts/room/types.ts +93 -0
- package/tui/README.md +20 -0
- package/tui/src/app.tsx +3 -2
- package/tui/src/commands/activate.tsx +98 -0
- package/tui/src/commands/archive.tsx +54 -25
- package/tui/src/commands/capture.tsx +50 -0
- package/tui/src/commands/dedupe.tsx +2 -7
- package/tui/src/commands/delete.tsx +48 -0
- package/tui/src/commands/details.tsx +7 -0
- package/tui/src/commands/persona.tsx +271 -9
- package/tui/src/commands/room.tsx +261 -0
- package/tui/src/commands/silence.tsx +29 -0
- package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
- package/tui/src/components/ConfirmOverlay.tsx +6 -0
- package/tui/src/components/ConflictOverlay.tsx +6 -0
- package/tui/src/components/HelpOverlay.tsx +6 -1
- package/tui/src/components/LoadingOverlay.tsx +51 -0
- package/tui/src/components/MessageList.tsx +1 -18
- package/tui/src/components/PersonPickerOverlay.tsx +121 -0
- package/tui/src/components/PersonaListOverlay.tsx +6 -1
- package/tui/src/components/PromptInput.tsx +141 -8
- package/tui/src/components/ProviderListOverlay.tsx +5 -1
- package/tui/src/components/QuotesOverlay.tsx +5 -1
- package/tui/src/components/RoomMessageList.tsx +179 -0
- package/tui/src/components/Sidebar.tsx +54 -2
- package/tui/src/components/StatusBar.tsx +99 -8
- package/tui/src/components/ToolkitListOverlay.tsx +5 -1
- package/tui/src/components/WelcomeOverlay.tsx +6 -0
- package/tui/src/context/ei.tsx +252 -1
- package/tui/src/context/keyboard.tsx +48 -12
- package/tui/src/util/cyp-editor.tsx +152 -0
- package/tui/src/util/quote-utils.ts +19 -0
- package/tui/src/util/room-editor.tsx +164 -0
- package/tui/src/util/room-logic.ts +8 -0
- package/tui/src/util/room-parser.ts +70 -0
- package/tui/src/util/yaml-serializers.ts +154 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type { Ei_Interface, Topic, Message, ContextStatus, PersonaEntity } from "../../core/types.js";
|
|
2
|
+
import type { Ei_Interface, Topic, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
|
+
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
3
4
|
import type { IClaudeCodeReader, ClaudeCodeSession, ClaudeCodeMessage } from "./types.js";
|
|
4
5
|
import {
|
|
5
6
|
CLAUDE_CODE_PERSONA_NAME,
|
|
@@ -48,6 +49,7 @@ function convertToEiMessage(msg: ClaudeCodeMessage): Message {
|
|
|
48
49
|
timestamp: msg.timestamp,
|
|
49
50
|
read: true,
|
|
50
51
|
context_status: "default" as ContextStatus,
|
|
52
|
+
external: true,
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -73,6 +75,14 @@ function ensureClaudeCodePersona(
|
|
|
73
75
|
if (existing) return existing;
|
|
74
76
|
|
|
75
77
|
const now = new Date().toISOString();
|
|
78
|
+
const seedTraits: PersonaTrait[] = DEFAULT_SEED_TRAITS.map((t) => ({
|
|
79
|
+
id: crypto.randomUUID(),
|
|
80
|
+
name: t.name,
|
|
81
|
+
description: t.description,
|
|
82
|
+
sentiment: t.sentiment,
|
|
83
|
+
strength: t.strength,
|
|
84
|
+
last_updated: now,
|
|
85
|
+
}));
|
|
76
86
|
const persona: PersonaEntity = {
|
|
77
87
|
id: crypto.randomUUID(),
|
|
78
88
|
display_name: CLAUDE_CODE_PERSONA_NAME,
|
|
@@ -83,7 +93,7 @@ function ensureClaudeCodePersona(
|
|
|
83
93
|
"Claude Code is an agentic coding assistant that helps with coding tasks, debugging, architecture decisions, and more.",
|
|
84
94
|
group_primary: CLAUDE_CODE_GROUP,
|
|
85
95
|
groups_visible: [CLAUDE_CODE_GROUP],
|
|
86
|
-
traits:
|
|
96
|
+
traits: seedTraits,
|
|
87
97
|
topics: [],
|
|
88
98
|
is_paused: false,
|
|
89
99
|
is_archived: false,
|
|
@@ -266,17 +276,19 @@ export async function importClaudeCodeSessions(
|
|
|
266
276
|
|
|
267
277
|
if (signal?.aborted) return result;
|
|
268
278
|
|
|
269
|
-
// ─── Step 4: Ensure persona, archive, clear
|
|
279
|
+
// ─── Step 4: Ensure persona, archive if new, clear external messages ──────────
|
|
280
|
+
const personaExistedBefore = stateManager.persona_getByName(CLAUDE_CODE_PERSONA_NAME) !== null;
|
|
270
281
|
const persona = ensureClaudeCodePersona(stateManager, eiInterface);
|
|
271
|
-
result.personaCreated = !
|
|
282
|
+
result.personaCreated = !personaExistedBefore;
|
|
272
283
|
|
|
273
|
-
if (!
|
|
284
|
+
if (!personaExistedBefore) {
|
|
274
285
|
stateManager.persona_archive(persona.id);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
286
|
+
} else {
|
|
287
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
288
|
+
const externalIds = existingMsgs.filter((m) => m.external === true).map((m) => m.id);
|
|
289
|
+
if (externalIds.length > 0) {
|
|
290
|
+
stateManager.messages_remove(persona.id, externalIds);
|
|
291
|
+
}
|
|
280
292
|
}
|
|
281
293
|
|
|
282
294
|
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
@@ -316,6 +328,7 @@ export async function importClaudeCodeSessions(
|
|
|
316
328
|
queueAllScans(context, stateManager, {
|
|
317
329
|
extraction_model: ccSettings?.extraction_model,
|
|
318
330
|
extraction_token_limit: ccSettings?.extraction_token_limit,
|
|
331
|
+
external_filter: "only",
|
|
319
332
|
});
|
|
320
333
|
result.extractionScansQueued += 4;
|
|
321
334
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type { Ei_Interface, Topic, Message, ContextStatus, PersonaEntity } from "../../core/types.js";
|
|
2
|
+
import type { Ei_Interface, Topic, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
|
+
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
3
4
|
import type { ICursorReader, CursorSession, CursorMessage } from "./types.js";
|
|
4
5
|
import {
|
|
5
6
|
CURSOR_PERSONA_NAME,
|
|
@@ -40,6 +41,7 @@ function convertToEiMessage(msg: CursorMessage): Message {
|
|
|
40
41
|
timestamp: msg.timestamp,
|
|
41
42
|
read: true,
|
|
42
43
|
context_status: "default" as ContextStatus,
|
|
44
|
+
external: true,
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -61,6 +63,14 @@ function ensureCursorPersona(
|
|
|
61
63
|
if (existing) return existing;
|
|
62
64
|
|
|
63
65
|
const now = new Date().toISOString();
|
|
66
|
+
const seedTraits: PersonaTrait[] = DEFAULT_SEED_TRAITS.map((t) => ({
|
|
67
|
+
id: crypto.randomUUID(),
|
|
68
|
+
name: t.name,
|
|
69
|
+
description: t.description,
|
|
70
|
+
sentiment: t.sentiment,
|
|
71
|
+
strength: t.strength,
|
|
72
|
+
last_updated: now,
|
|
73
|
+
}));
|
|
64
74
|
const persona: PersonaEntity = {
|
|
65
75
|
id: crypto.randomUUID(),
|
|
66
76
|
display_name: CURSOR_PERSONA_NAME,
|
|
@@ -71,7 +81,7 @@ function ensureCursorPersona(
|
|
|
71
81
|
"Cursor is an AI-powered IDE that helps with coding tasks, debugging, architecture decisions, and more.",
|
|
72
82
|
group_primary: CURSOR_GROUP,
|
|
73
83
|
groups_visible: [CURSOR_GROUP],
|
|
74
|
-
traits:
|
|
84
|
+
traits: seedTraits,
|
|
75
85
|
topics: [],
|
|
76
86
|
is_paused: false,
|
|
77
87
|
is_archived: false,
|
|
@@ -223,16 +233,18 @@ export async function importCursorSessions(
|
|
|
223
233
|
|
|
224
234
|
if (signal?.aborted) return result;
|
|
225
235
|
|
|
236
|
+
const personaExistedBefore = stateManager.persona_getByName(CURSOR_PERSONA_NAME) !== null;
|
|
226
237
|
const persona = ensureCursorPersona(stateManager, eiInterface);
|
|
227
|
-
result.personaCreated = !
|
|
238
|
+
result.personaCreated = !personaExistedBefore;
|
|
228
239
|
|
|
229
|
-
if (!
|
|
240
|
+
if (!personaExistedBefore) {
|
|
230
241
|
stateManager.persona_archive(persona.id);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
242
|
+
} else {
|
|
243
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
244
|
+
const externalIds = existingMsgs.filter((m) => m.external === true).map((m) => m.id);
|
|
245
|
+
if (externalIds.length > 0) {
|
|
246
|
+
stateManager.messages_remove(persona.id, externalIds);
|
|
247
|
+
}
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
@@ -267,7 +279,7 @@ export async function importCursorSessions(
|
|
|
267
279
|
messages_analyze: toAnalyze,
|
|
268
280
|
};
|
|
269
281
|
|
|
270
|
-
queueAllScans(context, stateManager, {});
|
|
282
|
+
queueAllScans(context, stateManager, { external_filter: "only" });
|
|
271
283
|
result.extractionScansQueued += 4;
|
|
272
284
|
}
|
|
273
285
|
|
|
@@ -3,7 +3,7 @@ import type { Ei_Interface, Message, ContextStatus } from "../../core/types.js";
|
|
|
3
3
|
import type { IOpenCodeReader, OpenCodeSession, OpenCodeMessage } from "./types.js";
|
|
4
4
|
import { UTILITY_AGENTS, AGENT_TO_AGENT_PREFIXES } from "./types.js";
|
|
5
5
|
import { createOpenCodeReader } from "./reader-factory.js";
|
|
6
|
-
import { ensureAgentPersona } from "../../core/personas/opencode-agent.js";
|
|
6
|
+
import { ensureAgentPersona, resolveCanonicalAgent } from "../../core/personas/opencode-agent.js";
|
|
7
7
|
import {
|
|
8
8
|
queueAllScans,
|
|
9
9
|
type ExtractionContext,
|
|
@@ -50,10 +50,10 @@ function convertToEiMessage(ocMsg: OpenCodeMessage): Message {
|
|
|
50
50
|
timestamp: ocMsg.timestamp,
|
|
51
51
|
read: true,
|
|
52
52
|
context_status: "default" as ContextStatus,
|
|
53
|
+
external: true,
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
/** Convert OC message to Ei Message with all extraction flags pre-set. */
|
|
57
57
|
function convertToPreMarkedEiMessage(ocMsg: OpenCodeMessage): Message {
|
|
58
58
|
return {
|
|
59
59
|
...convertToEiMessage(ocMsg),
|
|
@@ -171,9 +171,10 @@ export async function importOpenCodeSessions(
|
|
|
171
171
|
// ─── Step 4: Resolve agents → personas, group by persona ID ────────
|
|
172
172
|
// Resolve aliases up front so 'sisyphus' and 'Sisyphus (Ultraworker)'
|
|
173
173
|
// land in the same bucket instead of clobbering each other.
|
|
174
|
-
const byPersonaId = new Map<string, { persona: NonNullable<ReturnType<typeof stateManager.persona_getByName>>; msgs: OpenCodeMessage[] }>();
|
|
174
|
+
const byPersonaId = new Map<string, { persona: NonNullable<ReturnType<typeof stateManager.persona_getByName>>; msgs: OpenCodeMessage[]; isNew: boolean; agentName: string }>();
|
|
175
175
|
for (const msg of relevant) {
|
|
176
176
|
let persona = stateManager.persona_getByName(msg.agent);
|
|
177
|
+
let isNew = false;
|
|
177
178
|
if (!persona) {
|
|
178
179
|
persona = await ensureAgentPersona(msg.agent, {
|
|
179
180
|
stateManager,
|
|
@@ -181,28 +182,43 @@ export async function importOpenCodeSessions(
|
|
|
181
182
|
reader,
|
|
182
183
|
});
|
|
183
184
|
result.personasCreated.push(msg.agent);
|
|
185
|
+
isNew = true;
|
|
184
186
|
}
|
|
185
187
|
const bucket = byPersonaId.get(persona.id);
|
|
186
188
|
if (bucket) {
|
|
187
189
|
bucket.msgs.push(msg);
|
|
188
190
|
} else {
|
|
189
|
-
byPersonaId.set(persona.id, { persona, msgs: [msg] });
|
|
191
|
+
byPersonaId.set(persona.id, { persona, msgs: [msg], isNew, agentName: msg.agent });
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
194
196
|
const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
|
|
195
197
|
|
|
196
|
-
for (const [, { persona, msgs: agentMsgs }] of byPersonaId) {
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
for (const [, { persona, msgs: agentMsgs, isNew, agentName }] of byPersonaId) {
|
|
199
|
+
if (isNew) {
|
|
200
|
+
// Brand-new persona: archive it (coding-session store, not a live chat persona)
|
|
199
201
|
stateManager.persona_archive(persona.id);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
} else if (persona.is_archived) {
|
|
203
|
+
// Existing archived persona: refresh identity fields, then remove only external messages
|
|
204
|
+
const agentInfo = await reader.getAgentInfo(persona.display_name);
|
|
205
|
+
const { aliases } = resolveCanonicalAgent(agentName);
|
|
206
|
+
stateManager.persona_update(persona.id, {
|
|
207
|
+
short_description: agentInfo?.description ?? persona.short_description,
|
|
208
|
+
aliases,
|
|
209
|
+
});
|
|
210
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
211
|
+
const externalIds = existingMsgs.filter(m => m.external === true).map(m => m.id);
|
|
212
|
+
if (externalIds.length > 0) {
|
|
213
|
+
stateManager.messages_remove(persona.id, externalIds);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// Existing live (non-archived) persona: only remove external messages, leave chat history intact
|
|
217
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
218
|
+
const externalIds = existingMsgs.filter(m => m.external === true).map(m => m.id);
|
|
219
|
+
if (externalIds.length > 0) {
|
|
220
|
+
stateManager.messages_remove(persona.id, externalIds);
|
|
221
|
+
}
|
|
206
222
|
}
|
|
207
223
|
|
|
208
224
|
// Write messages — pre-mark old ones, leave new ones unmarked for extraction
|
|
@@ -241,6 +257,7 @@ export async function importOpenCodeSessions(
|
|
|
241
257
|
queueAllScans(context, stateManager, {
|
|
242
258
|
extraction_model: openCodeSettings?.extraction_model,
|
|
243
259
|
extraction_token_limit: openCodeSettings?.extraction_token_limit,
|
|
260
|
+
external_filter: "only",
|
|
244
261
|
});
|
|
245
262
|
result.extractionScansQueued += 4;
|
|
246
263
|
}
|
|
@@ -81,7 +81,7 @@ You have access to a tool called \`read_memory\` (6 calls max — see HARD RULES
|
|
|
81
81
|
]
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Call \`submit_dedup_decisions\` with your decisions. If the tool is unavailable, return raw JSON — no markdown fencing, no commentary, just the object.
|
|
85
85
|
|
|
86
86
|
Record format for "${typeLabel}" (based on type):
|
|
87
87
|
|
|
@@ -102,7 +102,7 @@ ${buildRecordFormatExamples(data.itemType)}
|
|
|
102
102
|
similarity_range: data.similarityRange,
|
|
103
103
|
}, null, 2);
|
|
104
104
|
|
|
105
|
-
const schemaReminder = `**
|
|
105
|
+
const schemaReminder = `**Call \`submit_dedup_decisions\` with your decisions.** If the tool is unavailable, return JSON:
|
|
106
106
|
\n\`\`\`json
|
|
107
107
|
{
|
|
108
108
|
"update": [
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { PersonaFromPersonPromptData, PromptOutput } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const JSON_SCHEMA = `\`\`\`json
|
|
4
|
+
{
|
|
5
|
+
"short_description": "10-15 word summary of who this person is",
|
|
6
|
+
"long_description": "2-3 sentences describing personality, approach, and what makes them distinctive",
|
|
7
|
+
"traits": [
|
|
8
|
+
{ "name": "...", "description": "...", "sentiment": 0.7, "strength": 0.8 }
|
|
9
|
+
],
|
|
10
|
+
"topics": [
|
|
11
|
+
{ "name": "...", "perspective": "...", "approach": "...", "personal_stake": "...", "sentiment": 0.6, "exposure_current": 0.5, "exposure_desired": 0.7 }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
\`\`\``;
|
|
15
|
+
|
|
16
|
+
const JSON_SCHEMA_ABBREVIATED = `Return JSON:
|
|
17
|
+
\`\`\`json
|
|
18
|
+
{
|
|
19
|
+
"short_description": "10-15 word summary",
|
|
20
|
+
"long_description": "2-3 sentence description",
|
|
21
|
+
"traits": [ { "name": "...", "description": "...", "sentiment": 0.0, "strength": 0.5 } ],
|
|
22
|
+
"topics": [ { "name": "...", "perspective": "...", "approach": "...", "personal_stake": "...", "sentiment": 0.5, "exposure_current": 0.5, "exposure_desired": 0.6 } ]
|
|
23
|
+
}
|
|
24
|
+
\`\`\``;
|
|
25
|
+
|
|
26
|
+
export function buildPersonaFromPersonPrompt(data: PersonaFromPersonPromptData): PromptOutput {
|
|
27
|
+
if (!data.name) {
|
|
28
|
+
throw new Error("buildPersonaFromPersonPrompt: name is required");
|
|
29
|
+
}
|
|
30
|
+
if (!data.description) {
|
|
31
|
+
throw new Error("buildPersonaFromPersonPrompt: description is required");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const isUpdate = !!(data.existing_trait_names?.length || data.existing_topic_names?.length);
|
|
35
|
+
|
|
36
|
+
const systemLines: string[] = [];
|
|
37
|
+
systemLines.push("You are building a persona definition from a real person's description.");
|
|
38
|
+
|
|
39
|
+
if (isUpdate) {
|
|
40
|
+
systemLines.push(
|
|
41
|
+
"This persona already exists — generate traits and topics that EXPAND its personality with new dimensions."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
systemLines.push(
|
|
46
|
+
"Generate content that authentically reflects the person described. Do not use generic filler.",
|
|
47
|
+
"",
|
|
48
|
+
"Generate exactly 3 traits and exactly 3 topics.",
|
|
49
|
+
"",
|
|
50
|
+
JSON_SCHEMA
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const system = systemLines.join("\n");
|
|
54
|
+
|
|
55
|
+
const userLines: string[] = [];
|
|
56
|
+
userLines.push(`Person: ${data.name}`);
|
|
57
|
+
|
|
58
|
+
if (data.relationship) {
|
|
59
|
+
userLines.push(`Relationship: ${data.relationship}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
userLines.push("", "Description:", data.description);
|
|
63
|
+
|
|
64
|
+
if (data.existing_trait_names && data.existing_trait_names.length > 0) {
|
|
65
|
+
userLines.push(
|
|
66
|
+
"",
|
|
67
|
+
"These traits already exist on this persona — do NOT repeat them. Generate 3 NEW traits that reveal different dimensions:",
|
|
68
|
+
...data.existing_trait_names.map((n) => `- ${n}`)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (data.existing_topic_names && data.existing_topic_names.length > 0) {
|
|
73
|
+
userLines.push(
|
|
74
|
+
"",
|
|
75
|
+
"These topics already exist — do NOT repeat them. Generate 3 NEW topics that expand different areas:",
|
|
76
|
+
...data.existing_topic_names.map((n) => `- ${n}`)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
userLines.push("", JSON_SCHEMA_ABBREVIATED);
|
|
81
|
+
|
|
82
|
+
const user = userLines.join("\n");
|
|
83
|
+
|
|
84
|
+
return { system, user };
|
|
85
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { buildPersonaGenerationPrompt } from "./persona.js";
|
|
2
2
|
export { buildPersonaDescriptionsPrompt } from "./descriptions.js";
|
|
3
|
+
export { buildPersonaFromPersonPrompt } from "./from-person.js";
|
|
3
4
|
export {
|
|
4
5
|
DEFAULT_SEED_TRAITS,
|
|
5
6
|
SEED_TRAIT_GENUINE,
|
|
@@ -9,6 +10,7 @@ export {
|
|
|
9
10
|
export type {
|
|
10
11
|
PersonaGenerationPromptData,
|
|
11
12
|
PersonaGenerationResult,
|
|
13
|
+
PersonaFromPersonPromptData,
|
|
12
14
|
PersonaDescriptionsPromptData,
|
|
13
15
|
PersonaDescriptionsResult,
|
|
14
16
|
PromptOutput,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { PersonaGenerationPromptData, PromptOutput } from "./types.js";
|
|
2
2
|
import { DEFAULT_SEED_TRAITS } from "./seeds.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Use `processor.generatePersonaPreview()` instead.
|
|
6
|
+
* This queue-based generation flow is being replaced by a synchronous one-shot
|
|
7
|
+
* preview path that lets users review the result before persisting.
|
|
8
|
+
* Will be removed once `generatePersonaPreview` is wired up end-to-end.
|
|
9
|
+
*/
|
|
4
10
|
export function buildPersonaGenerationPrompt(data: PersonaGenerationPromptData): PromptOutput {
|
|
5
11
|
if (!data.name) {
|
|
6
12
|
throw new Error("buildPersonaGenerationPrompt: name is required");
|
|
@@ -9,10 +15,10 @@ export function buildPersonaGenerationPrompt(data: PersonaGenerationPromptData):
|
|
|
9
15
|
const hasLongDescription = !!data.long_description?.trim();
|
|
10
16
|
const hasShortDescription = !!data.short_description?.trim();
|
|
11
17
|
|
|
12
|
-
const userProvidedTraits = data.
|
|
18
|
+
const userProvidedTraits = data.filtered_traits;
|
|
13
19
|
const allTraits = [...DEFAULT_SEED_TRAITS, ...userProvidedTraits];
|
|
14
20
|
const existingTraitCount = allTraits.length;
|
|
15
|
-
const existingTopicCount = data.
|
|
21
|
+
const existingTopicCount = data.filtered_topics.length;
|
|
16
22
|
|
|
17
23
|
const needsShortDescription = !hasShortDescription;
|
|
18
24
|
const needsMoreTraits = existingTraitCount < 3;
|
|
@@ -136,14 +142,12 @@ ${schemaFragment}`;
|
|
|
136
142
|
|
|
137
143
|
if (existingTopicCount > 0) {
|
|
138
144
|
userPrompt += `## User's Topics (PRESERVE EXACTLY, add more if fewer than 3)\n`;
|
|
139
|
-
for (const topic of data.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (topic.sentiment !== undefined) userPrompt += ` sentiment: ${topic.sentiment}\n`;
|
|
146
|
-
}
|
|
145
|
+
for (const topic of data.filtered_topics) {
|
|
146
|
+
userPrompt += `- ${topic.name}\n`;
|
|
147
|
+
if (topic.perspective) userPrompt += ` perspective: ${topic.perspective}\n`;
|
|
148
|
+
if (topic.approach) userPrompt += ` approach: ${topic.approach}\n`;
|
|
149
|
+
if (topic.personal_stake) userPrompt += ` personal_stake: ${topic.personal_stake}\n`;
|
|
150
|
+
if (topic.sentiment !== undefined) userPrompt += ` sentiment: ${topic.sentiment}\n`;
|
|
147
151
|
}
|
|
148
152
|
userPrompt += `\n`;
|
|
149
153
|
}
|
|
@@ -1,31 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export interface SeedTrait {
|
|
4
|
-
name: string;
|
|
5
|
-
description: string;
|
|
6
|
-
sentiment: number;
|
|
7
|
-
strength: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const SEED_TRAIT_GENUINE: SeedTrait = {
|
|
11
|
-
name: "Genuine Responses",
|
|
12
|
-
description: "Respond authentically rather than with empty validation. Disagree when appropriate. Skip phrases like 'Great question!' or 'Absolutely!' - just respond to the substance.",
|
|
13
|
-
sentiment: 0.5,
|
|
14
|
-
strength: 0.7,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const SEED_TRAIT_NATURAL_SPEECH: SeedTrait = {
|
|
18
|
-
name: "Natural Speech",
|
|
19
|
-
description: `Write in natural conversational flow. Avoid AI-typical patterns like:
|
|
20
|
-
- Choppy dramatic fragments ('Bold move. Risky play.')
|
|
21
|
-
- Rhetorical 'That X? Y.' structures
|
|
22
|
-
- 'That's not just... That's ...'
|
|
23
|
-
- formulaic paragraph openers`,
|
|
24
|
-
sentiment: 0.5,
|
|
25
|
-
strength: 0.7,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const DEFAULT_SEED_TRAITS: SeedTrait[] = [
|
|
1
|
+
export type { SeedTrait } from "../../core/constants/seed-traits.js";
|
|
2
|
+
export {
|
|
29
3
|
SEED_TRAIT_GENUINE,
|
|
30
4
|
SEED_TRAIT_NATURAL_SPEECH,
|
|
31
|
-
|
|
5
|
+
DEFAULT_SEED_TRAITS,
|
|
6
|
+
} from "../../core/constants/seed-traits.js";
|
|
@@ -11,6 +11,8 @@ export interface PersonaGenerationPromptData {
|
|
|
11
11
|
short_description?: string;
|
|
12
12
|
existing_traits?: Array<{ name?: string; description?: string; sentiment?: number; strength?: number }>;
|
|
13
13
|
existing_topics?: Array<{ name?: string; perspective?: string; approach?: string; personal_stake?: string; sentiment?: number; exposure_current?: number; exposure_desired?: number }>;
|
|
14
|
+
filtered_traits: Array<{ name?: string; description?: string; sentiment?: number; strength?: number }>;
|
|
15
|
+
filtered_topics: Array<{ name?: string; perspective?: string; approach?: string; personal_stake?: string; sentiment?: number; exposure_current?: number; exposure_desired?: number }>;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface PersonaGenerationResult {
|
|
@@ -31,6 +33,17 @@ export interface PersonaGenerationResult {
|
|
|
31
33
|
exposure_desired: number;
|
|
32
34
|
sentiment: number;
|
|
33
35
|
}>;
|
|
36
|
+
previous_long_description?: string;
|
|
37
|
+
previous_short_description?: string;
|
|
38
|
+
aliases?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PersonaFromPersonPromptData {
|
|
42
|
+
name: string;
|
|
43
|
+
description: string;
|
|
44
|
+
relationship?: string;
|
|
45
|
+
existing_trait_names?: string[];
|
|
46
|
+
existing_topic_names?: string[];
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
export interface PersonaDescriptionsPromptData {
|
|
@@ -126,7 +126,7 @@ ${formatPeopleWithGaps(data.human.people)}`;
|
|
|
126
126
|
|
|
127
127
|
const outputFragment = `## Response Format
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
Call the \`submit_heartbeat_check\` tool with your decision. If the tool is unavailable, return JSON:
|
|
130
130
|
|
|
131
131
|
\`\`\`json
|
|
132
132
|
{
|
|
@@ -40,8 +40,8 @@ function countTrailingPersonaMessages(history: Message[]): number {
|
|
|
40
40
|
return count;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function getLastPersonaMessage(
|
|
44
|
-
return
|
|
43
|
+
function getLastPersonaMessage(systemMessages: Message[]): Message | undefined {
|
|
44
|
+
return systemMessages.slice(-1)[0];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function buildEiHeartbeatPrompt(data: EiHeartbeatPromptData): PromptOutput {
|
|
@@ -79,7 +79,7 @@ ${itemsSection}
|
|
|
79
79
|
|
|
80
80
|
## Response Format
|
|
81
81
|
|
|
82
|
-
Pick ONE item (or none):
|
|
82
|
+
Call the \`submit_ei_heartbeat\` tool with your decision. Pick ONE item (or none). If the tool is unavailable, return JSON:
|
|
83
83
|
|
|
84
84
|
\`\`\`json
|
|
85
85
|
{
|
|
@@ -101,7 +101,7 @@ Or if nothing warrants reaching out:
|
|
|
101
101
|
${formatMessagesAsPlaceholders(data.recent_history, "Ei")}`;
|
|
102
102
|
|
|
103
103
|
const consecutiveMessages = countTrailingPersonaMessages(data.recent_history);
|
|
104
|
-
const lastEiMsg = getLastPersonaMessage(data.
|
|
104
|
+
const lastEiMsg = getLastPersonaMessage(data.system_messages);
|
|
105
105
|
|
|
106
106
|
let unansweredWarning = "";
|
|
107
107
|
if (lastEiMsg && consecutiveMessages >= 1) {
|
package/src/prompts/index.ts
CHANGED
|
@@ -16,10 +16,12 @@ export type {
|
|
|
16
16
|
export {
|
|
17
17
|
buildPersonaGenerationPrompt,
|
|
18
18
|
buildPersonaDescriptionsPrompt,
|
|
19
|
+
buildPersonaFromPersonPrompt,
|
|
19
20
|
} from "./generation/index.js";
|
|
20
21
|
export type {
|
|
21
22
|
PersonaGenerationPromptData,
|
|
22
23
|
PersonaGenerationResult,
|
|
24
|
+
PersonaFromPersonPromptData,
|
|
23
25
|
PersonaDescriptionsPromptData,
|
|
24
26
|
PersonaDescriptionsResult,
|
|
25
27
|
} from "./generation/types.js";
|
|
@@ -81,3 +83,16 @@ export type {
|
|
|
81
83
|
DescriptionCheckPromptData,
|
|
82
84
|
DescriptionCheckResult,
|
|
83
85
|
} from "./ceremony/types.js";
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
buildRoomResponsePrompt,
|
|
89
|
+
buildRoomJudgePrompt,
|
|
90
|
+
} from "./room/index.js";
|
|
91
|
+
export type {
|
|
92
|
+
RoomResponsePromptData,
|
|
93
|
+
RoomJudgePromptData,
|
|
94
|
+
RoomJudgeResult,
|
|
95
|
+
RoomParticipantIdentity,
|
|
96
|
+
RoomHistoryMessage,
|
|
97
|
+
RoomJudgeCandidate,
|
|
98
|
+
} from "./room/types.js";
|
|
@@ -13,7 +13,7 @@ export function getMessageDisplayText(message: Message): string | null {
|
|
|
13
13
|
if (message.action_response) parts.push(`_${message.action_response}_`);
|
|
14
14
|
if (message.verbal_response) parts.push(message.verbal_response);
|
|
15
15
|
if (message.silence_reason) {
|
|
16
|
-
const name = 'Persona';
|
|
16
|
+
const name = message.speaker_name ?? 'Persona';
|
|
17
17
|
parts.push(`[${name} chose not to respond because: ${message.silence_reason}]`);
|
|
18
18
|
}
|
|
19
19
|
if (parts.length === 0) return null;
|
|
@@ -45,7 +45,7 @@ export function buildChatMessageContent(message: Message): string {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function formatMessageAsPlaceholder(message: Message, personaName: string): string {
|
|
48
|
-
const role = message.role === "human" ? "human" : personaName;
|
|
48
|
+
const role = message.role === "human" ? "human" : (message.speaker_name ?? personaName);
|
|
49
49
|
return `[mid:${message.id}:${role}]`;
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -24,11 +24,12 @@ You are checking if a topic candidate already exists in ${personaName}'s topic l
|
|
|
24
24
|
|
|
25
25
|
# Matching Rules
|
|
26
26
|
|
|
27
|
-
1. **Exact match**: Same topic name → return its ID
|
|
28
|
-
2. **Similar match**: Clearly the same topic with different wording → return its ID
|
|
27
|
+
1. **Exact match**: Same topic name → action "match", return its ID
|
|
28
|
+
2. **Similar match**: Clearly the same topic with different wording → action "match", return its ID
|
|
29
29
|
- "Steam Deck" and "Steam Deck Modding" → MATCH (same core topic)
|
|
30
30
|
- "Cooking" and "Italian Cooking" → consider MATCH if the specific is part of the general
|
|
31
|
-
3. **No match**: Genuinely different topic →
|
|
31
|
+
3. **No match**: Genuinely different topic → action "create"
|
|
32
|
+
4. **Skip**: Trivial, off-topic, or too vague to be a meaningful persona topic → action "skip"
|
|
32
33
|
|
|
33
34
|
# Existing Topics
|
|
34
35
|
|
|
@@ -38,10 +39,9 @@ ${formatExistingTopics(data.existing_topics)}
|
|
|
38
39
|
|
|
39
40
|
# Response Format
|
|
40
41
|
|
|
41
|
-
Return ONLY the ID of the matching topic, or null if no match exists.
|
|
42
|
-
|
|
43
42
|
\`\`\`json
|
|
44
43
|
{
|
|
44
|
+
"action": "match" | "create" | "skip",
|
|
45
45
|
"matched_id": "uuid-of-matching-topic" | null,
|
|
46
46
|
"reason": "brief explanation"
|
|
47
47
|
}
|
|
@@ -55,11 +55,12 @@ Name: ${data.candidate.name}
|
|
|
55
55
|
Message Count: ${data.candidate.message_count}
|
|
56
56
|
Sentiment Signal: ${data.candidate.sentiment_signal}
|
|
57
57
|
|
|
58
|
-
Find the best match in ${personaName}'s existing topics, or
|
|
58
|
+
Find the best match in ${personaName}'s existing topics, or decide if this should be created or skipped.
|
|
59
59
|
|
|
60
60
|
**Return JSON:**
|
|
61
61
|
\`\`\`json
|
|
62
62
|
{
|
|
63
|
+
"action": "match" | "create" | "skip",
|
|
63
64
|
"matched_id": "..." | null,
|
|
64
65
|
"reason": "..."
|
|
65
66
|
}
|
|
@@ -80,15 +80,12 @@ Example: "As a hobbit who left home, the Shire is both memory and motivation."
|
|
|
80
80
|
## sentiment
|
|
81
81
|
How ${personaName} feels about this topic. Scale: -1.0 (hate) to 1.0 (love).
|
|
82
82
|
|
|
83
|
-
##
|
|
84
|
-
How
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
- Maximum: 1.0
|
|
90
|
-
|
|
91
|
-
**For new topics**: Start at 0.3-0.5 depending on discussion depth.
|
|
83
|
+
## exposure_impact
|
|
84
|
+
How much this topic was discussed in the recent messages. Choose one:
|
|
85
|
+
- "high" — Major topic, multiple substantive exchanges
|
|
86
|
+
- "medium" — Meaningful mention, discussed with some depth
|
|
87
|
+
- "low" — Brief or passing mention
|
|
88
|
+
- "none" — Barely mentioned, tangential at best
|
|
92
89
|
|
|
93
90
|
## exposure_desired
|
|
94
91
|
How much ${personaName} wants to discuss this topic. Scale: 0.0 to 1.0.
|
|
@@ -112,7 +109,7 @@ How much ${personaName} wants to discuss this topic. Scale: 0.0 to 1.0.
|
|
|
112
109
|
"approach": "",
|
|
113
110
|
"personal_stake": "",
|
|
114
111
|
"sentiment": 0.5,
|
|
115
|
-
"
|
|
112
|
+
"exposure_impact": "medium",
|
|
116
113
|
"exposure_desired": 0.5
|
|
117
114
|
}
|
|
118
115
|
\`\`\``;
|
|
@@ -148,7 +145,7 @@ ${isNewTopic ? 'Create' : 'Update'} the PersonaTopic based on how ${personaName}
|
|
|
148
145
|
"approach": "...",
|
|
149
146
|
"personal_stake": "...",
|
|
150
147
|
"sentiment": 0.5,
|
|
151
|
-
"
|
|
148
|
+
"exposure_impact": "medium",
|
|
152
149
|
"exposure_desired": 0.5
|
|
153
150
|
}
|
|
154
151
|
\`\`\``;
|