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.
Files changed (103) hide show
  1. package/README.md +14 -0
  2. package/package.json +1 -1
  3. package/src/cli/README.md +21 -14
  4. package/src/cli/commands/personas.ts +12 -0
  5. package/src/cli/mcp.ts +6 -5
  6. package/src/cli/retrieval.ts +86 -8
  7. package/src/cli.ts +21 -19
  8. package/src/core/constants/seed-traits.ts +29 -0
  9. package/src/core/context-utils.ts +1 -0
  10. package/src/core/format-utils.ts +23 -0
  11. package/src/core/handlers/human-matching.ts +53 -35
  12. package/src/core/handlers/index.ts +5 -0
  13. package/src/core/handlers/persona-preview.ts +7 -0
  14. package/src/core/handlers/persona-topics.ts +3 -2
  15. package/src/core/handlers/rooms.ts +176 -0
  16. package/src/core/handlers/utils.ts +55 -3
  17. package/src/core/heartbeat-manager.ts +3 -1
  18. package/src/core/llm-client.ts +1 -1
  19. package/src/core/message-manager.ts +13 -9
  20. package/src/core/orchestrators/human-extraction.ts +15 -2
  21. package/src/core/orchestrators/index.ts +1 -0
  22. package/src/core/orchestrators/persona-generation.ts +4 -0
  23. package/src/core/orchestrators/persona-topics.ts +2 -1
  24. package/src/core/orchestrators/room-extraction.ts +318 -0
  25. package/src/core/persona-manager.ts +16 -5
  26. package/src/core/personas/opencode-agent.ts +12 -2
  27. package/src/core/processor.ts +520 -4
  28. package/src/core/prompt-context-builder.ts +89 -5
  29. package/src/core/queue-processor.ts +68 -8
  30. package/src/core/room-manager.ts +408 -0
  31. package/src/core/state/index.ts +1 -0
  32. package/src/core/state/personas.ts +12 -2
  33. package/src/core/state/queue.ts +2 -2
  34. package/src/core/state/rooms.ts +182 -0
  35. package/src/core/state-manager.ts +124 -2
  36. package/src/core/tool-manager.ts +1 -1
  37. package/src/core/tools/index.ts +15 -0
  38. package/src/core/types/entities.ts +1 -0
  39. package/src/core/types/enums.ts +11 -0
  40. package/src/core/types/integrations.ts +10 -2
  41. package/src/core/types/llm.ts +3 -0
  42. package/src/core/types/rooms.ts +59 -0
  43. package/src/core/types.ts +1 -0
  44. package/src/core/utils/decay.ts +14 -8
  45. package/src/core/utils/exposure.ts +14 -0
  46. package/src/integrations/claude-code/importer.ts +23 -10
  47. package/src/integrations/cursor/importer.ts +22 -10
  48. package/src/integrations/opencode/importer.ts +30 -13
  49. package/src/prompts/ceremony/dedup.ts +2 -2
  50. package/src/prompts/generation/from-person.ts +85 -0
  51. package/src/prompts/generation/index.ts +2 -0
  52. package/src/prompts/generation/persona.ts +14 -10
  53. package/src/prompts/generation/seeds.ts +4 -29
  54. package/src/prompts/generation/types.ts +13 -0
  55. package/src/prompts/heartbeat/check.ts +1 -1
  56. package/src/prompts/heartbeat/ei.ts +4 -4
  57. package/src/prompts/heartbeat/types.ts +1 -0
  58. package/src/prompts/index.ts +15 -0
  59. package/src/prompts/message-utils.ts +2 -2
  60. package/src/prompts/persona/topics-match.ts +7 -6
  61. package/src/prompts/persona/topics-update.ts +8 -11
  62. package/src/prompts/persona/types.ts +2 -1
  63. package/src/prompts/response/index.ts +4 -11
  64. package/src/prompts/response/sections.ts +22 -10
  65. package/src/prompts/response/types.ts +6 -0
  66. package/src/prompts/room/index.ts +115 -0
  67. package/src/prompts/room/sections.ts +150 -0
  68. package/src/prompts/room/types.ts +93 -0
  69. package/tui/README.md +20 -0
  70. package/tui/src/app.tsx +3 -2
  71. package/tui/src/commands/activate.tsx +98 -0
  72. package/tui/src/commands/archive.tsx +54 -25
  73. package/tui/src/commands/capture.tsx +50 -0
  74. package/tui/src/commands/dedupe.tsx +2 -7
  75. package/tui/src/commands/delete.tsx +48 -0
  76. package/tui/src/commands/details.tsx +7 -0
  77. package/tui/src/commands/persona.tsx +271 -9
  78. package/tui/src/commands/room.tsx +261 -0
  79. package/tui/src/commands/silence.tsx +29 -0
  80. package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
  81. package/tui/src/components/ConfirmOverlay.tsx +6 -0
  82. package/tui/src/components/ConflictOverlay.tsx +6 -0
  83. package/tui/src/components/HelpOverlay.tsx +6 -1
  84. package/tui/src/components/LoadingOverlay.tsx +51 -0
  85. package/tui/src/components/MessageList.tsx +1 -18
  86. package/tui/src/components/PersonPickerOverlay.tsx +121 -0
  87. package/tui/src/components/PersonaListOverlay.tsx +6 -1
  88. package/tui/src/components/PromptInput.tsx +141 -8
  89. package/tui/src/components/ProviderListOverlay.tsx +5 -1
  90. package/tui/src/components/QuotesOverlay.tsx +5 -1
  91. package/tui/src/components/RoomMessageList.tsx +179 -0
  92. package/tui/src/components/Sidebar.tsx +54 -2
  93. package/tui/src/components/StatusBar.tsx +99 -8
  94. package/tui/src/components/ToolkitListOverlay.tsx +5 -1
  95. package/tui/src/components/WelcomeOverlay.tsx +6 -0
  96. package/tui/src/context/ei.tsx +252 -1
  97. package/tui/src/context/keyboard.tsx +48 -12
  98. package/tui/src/util/cyp-editor.tsx +152 -0
  99. package/tui/src/util/quote-utils.ts +19 -0
  100. package/tui/src/util/room-editor.tsx +164 -0
  101. package/tui/src/util/room-logic.ts +8 -0
  102. package/tui/src/util/room-parser.ts +70 -0
  103. 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, write messages ──────────
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 = !stateManager.persona_getByName(CLAUDE_CODE_PERSONA_NAME);
282
+ result.personaCreated = !personaExistedBefore;
272
283
 
273
- if (!persona.is_archived) {
284
+ if (!personaExistedBefore) {
274
285
  stateManager.persona_archive(persona.id);
275
- }
276
-
277
- const existingMsgs = stateManager.messages_get(persona.id);
278
- if (existingMsgs.length > 0) {
279
- stateManager.messages_remove(persona.id, existingMsgs.map((m) => m.id));
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 = !stateManager.persona_getByName(CURSOR_PERSONA_NAME);
238
+ result.personaCreated = !personaExistedBefore;
228
239
 
229
- if (!persona.is_archived) {
240
+ if (!personaExistedBefore) {
230
241
  stateManager.persona_archive(persona.id);
231
- }
232
-
233
- const existingMsgs = stateManager.messages_get(persona.id);
234
- if (existingMsgs.length > 0) {
235
- stateManager.messages_remove(persona.id, existingMsgs.map((m) => m.id));
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
- // Archive persona (message store only, not a conversational persona)
198
- if (!persona.is_archived) {
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
- // Clear existing messages — this persona holds exactly one session at a time
203
- const existingMsgs = stateManager.messages_get(persona.id);
204
- if (existingMsgs.length > 0) {
205
- stateManager.messages_remove(persona.id, existingMsgs.map(m => m.id));
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
- Return raw JSON. No markdown fencing, no commentary, no explanation. Just the JSON object.
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 = `**Return JSON:**
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.existing_traits?.filter(t => t.name?.trim()) ?? [];
18
+ const userProvidedTraits = data.filtered_traits;
13
19
  const allTraits = [...DEFAULT_SEED_TRAITS, ...userProvidedTraits];
14
20
  const existingTraitCount = allTraits.length;
15
- const existingTopicCount = data.existing_topics?.filter(t => t.name?.trim())?.length ?? 0;
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.existing_topics ?? []) {
140
- if (topic.name?.trim()) {
141
- userPrompt += `- ${topic.name}\n`;
142
- if (topic.perspective) userPrompt += ` perspective: ${topic.perspective}\n`;
143
- if (topic.approach) userPrompt += ` approach: ${topic.approach}\n`;
144
- if (topic.personal_stake) userPrompt += ` personal_stake: ${topic.personal_stake}\n`;
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
- // Seed traits for new personas. Users can set strength=0.0 to disable or delete entirely.
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
- Return JSON in this exact format:
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(history: Message[]): Message | undefined {
44
- return history.filter(m => m.role === "system").slice(-1)[0];
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.recent_history);
104
+ const lastEiMsg = getLastPersonaMessage(data.system_messages);
105
105
 
106
106
  let unansweredWarning = "";
107
107
  if (lastEiMsg && consecutiveMessages >= 1) {
@@ -87,6 +87,7 @@ export type EiHeartbeatItem =
87
87
  export interface EiHeartbeatPromptData {
88
88
  items: EiHeartbeatItem[];
89
89
  recent_history: Message[];
90
+ system_messages: Message[]; // Pre-filtered system messages from recent_history
90
91
  }
91
92
 
92
93
  /**
@@ -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'; // Caller doesn't pass persona name; frontends can override
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 → return null
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 return null if this is genuinely new.
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
- ## exposure_current
84
- How recently/frequently this topic has been discussed. Scale: 0.0 to 1.0.
85
-
86
- **For existing topics**: ONLY INCREASE this value based on conversation activity.
87
- - Active discussion increase by 0.2-0.3
88
- - Brief mention increase by 0.1
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
- "exposure_current": 0.5,
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
- "exposure_current": 0.5,
148
+ "exposure_impact": "medium",
152
149
  "exposure_desired": 0.5
153
150
  }
154
151
  \`\`\``;