ei-tui 0.5.4 → 0.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.
Files changed (86) hide show
  1. package/package.json +1 -1
  2. package/src/core/constants/built-in-identifier-types.ts +24 -0
  3. package/src/core/embedding-service.ts +24 -1
  4. package/src/core/handlers/dedup.ts +34 -4
  5. package/src/core/handlers/heartbeat.ts +16 -0
  6. package/src/core/handlers/human-extraction.ts +201 -7
  7. package/src/core/handlers/human-matching.ts +71 -22
  8. package/src/core/handlers/index.ts +52 -14
  9. package/src/core/handlers/persona-generation.ts +2 -0
  10. package/src/core/handlers/persona-response.ts +37 -22
  11. package/src/core/handlers/persona-topics.ts +35 -271
  12. package/src/core/handlers/rewrite.ts +3 -0
  13. package/src/core/handlers/rooms.ts +41 -20
  14. package/src/core/handlers/utils.ts +10 -8
  15. package/src/core/heartbeat-manager.ts +60 -2
  16. package/src/core/llm-client.ts +1 -1
  17. package/src/core/message-manager.ts +3 -2
  18. package/src/core/orchestrators/ceremony.ts +54 -144
  19. package/src/core/orchestrators/dedup-phase.ts +0 -199
  20. package/src/core/orchestrators/extraction-chunker.ts +8 -3
  21. package/src/core/orchestrators/human-extraction.ts +37 -85
  22. package/src/core/orchestrators/index.ts +4 -8
  23. package/src/core/orchestrators/person-migration.ts +55 -0
  24. package/src/core/orchestrators/persona-topics.ts +64 -89
  25. package/src/core/orchestrators/room-extraction.ts +34 -0
  26. package/src/core/persona-manager.ts +21 -2
  27. package/src/core/personas/opencode-agent.ts +1 -0
  28. package/src/core/processor.ts +51 -14
  29. package/src/core/prompt-context-builder.ts +38 -5
  30. package/src/core/queue-processor.ts +4 -2
  31. package/src/core/room-manager.ts +6 -7
  32. package/src/core/state/human.ts +6 -0
  33. package/src/core/state/personas.ts +35 -10
  34. package/src/core/state/rooms.ts +21 -0
  35. package/src/core/state-manager.ts +61 -0
  36. package/src/core/types/data-items.ts +12 -0
  37. package/src/core/types/entities.ts +3 -0
  38. package/src/core/types/enums.ts +2 -7
  39. package/src/core/types/llm.ts +2 -0
  40. package/src/core/types/rooms.ts +2 -0
  41. package/src/core/utils/identifier-utils.ts +19 -0
  42. package/src/core/utils/index.ts +2 -1
  43. package/src/core/utils/levenshtein.ts +18 -0
  44. package/src/integrations/claude-code/importer.ts +1 -0
  45. package/src/integrations/cursor/importer.ts +1 -0
  46. package/src/prompts/ceremony/index.ts +1 -0
  47. package/src/prompts/ceremony/person-migration.ts +77 -0
  48. package/src/prompts/ceremony/rewrite.ts +1 -1
  49. package/src/prompts/ceremony/user-dedup.ts +15 -1
  50. package/src/prompts/heartbeat/check.ts +28 -12
  51. package/src/prompts/heartbeat/ei.ts +2 -0
  52. package/src/prompts/heartbeat/types.ts +12 -0
  53. package/src/prompts/human/index.ts +0 -2
  54. package/src/prompts/human/person-scan.ts +58 -14
  55. package/src/prompts/human/person-update.ts +171 -96
  56. package/src/prompts/human/topic-update.ts +1 -1
  57. package/src/prompts/human/types.ts +5 -1
  58. package/src/prompts/index.ts +3 -10
  59. package/src/prompts/message-utils.ts +9 -23
  60. package/src/prompts/persona/index.ts +3 -10
  61. package/src/prompts/persona/topics-rate.ts +95 -0
  62. package/src/prompts/persona/types.ts +8 -48
  63. package/src/prompts/response/index.ts +3 -7
  64. package/src/prompts/response/sections.ts +7 -57
  65. package/src/prompts/room/index.ts +1 -1
  66. package/src/prompts/room/sections.ts +8 -31
  67. package/tui/src/commands/me.tsx +14 -7
  68. package/tui/src/commands/persona.tsx +120 -83
  69. package/tui/src/components/MessageList.tsx +9 -4
  70. package/tui/src/components/RoomMessageList.tsx +10 -5
  71. package/tui/src/context/keyboard.tsx +2 -2
  72. package/tui/src/util/cyp-editor.tsx +13 -8
  73. package/tui/src/util/yaml-context.ts +66 -0
  74. package/tui/src/util/yaml-human.ts +274 -0
  75. package/tui/src/util/yaml-persona.ts +479 -0
  76. package/tui/src/util/yaml-provider.ts +215 -0
  77. package/tui/src/util/yaml-queue.ts +81 -0
  78. package/tui/src/util/yaml-quotes.ts +46 -0
  79. package/tui/src/util/yaml-serializers.ts +9 -1417
  80. package/tui/src/util/yaml-settings.ts +223 -0
  81. package/tui/src/util/yaml-shared.ts +32 -0
  82. package/tui/src/util/yaml-toolkit.ts +55 -0
  83. package/src/prompts/human/person-match.ts +0 -65
  84. package/src/prompts/persona/topics-match.ts +0 -70
  85. package/src/prompts/persona/topics-scan.ts +0 -98
  86. package/src/prompts/persona/topics-update.ts +0 -154
@@ -1,4 +1,4 @@
1
- import type { PromptOutput } from "./types.js";
1
+ import type { PromptOutput, ParticipantContext } from "./types.js";
2
2
  import type { Person, Message } from "../../core/types.js";
3
3
  import { formatMessagesAsPlaceholders } from "../message-utils.js";
4
4
 
@@ -10,11 +10,26 @@ export interface PersonUpdatePromptData {
10
10
  messages_context: Message[];
11
11
  messages_analyze: Message[];
12
12
  persona_name: string;
13
+ participant_context?: ParticipantContext;
14
+ known_identifier_types?: string[];
15
+ }
16
+
17
+ function participantContextSection(ctx: ParticipantContext | undefined): string {
18
+ if (!ctx) return "";
19
+ const lines: string[] = ["# Participant Context", "The following may help you understand who is in this conversation.", ""];
20
+ lines.push(`## Persona: ${ctx.persona_name}`);
21
+ if (ctx.persona_description) lines.push(ctx.persona_description);
22
+ lines.push("");
23
+ lines.push("## Human User");
24
+ if (ctx.human_name) lines.push(`Name: ${ctx.human_name}`);
25
+ if (ctx.human_age !== undefined) lines.push(`Age: ${ctx.human_age}`);
26
+ lines.push("");
27
+ return lines.join("\n");
13
28
  }
14
29
 
15
30
  function formatExistingPerson(person: Person): string {
16
31
  return JSON.stringify({
17
- name: person.name,
32
+ identifiers: person.identifiers ?? [],
18
33
  description: person.description,
19
34
  sentiment: person.sentiment,
20
35
  relationship: person.relationship,
@@ -29,118 +44,159 @@ export function buildPersonUpdatePrompt(data: PersonUpdatePromptData): PromptOut
29
44
  }
30
45
 
31
46
  const personaName = data.persona_name;
47
+ const isNewItem = data.existing_item === null;
48
+ const humanName = data.participant_context?.human_name;
49
+
50
+ const builtInTypes = ['Full Name', 'First Name', 'Nickname', 'Email', 'GitHub', 'Discord',
51
+ 'Roblox', 'Reddit', 'Twitter', 'FF14', 'Relationship', 'Ei Persona'];
52
+ const userTypes = data.known_identifier_types ?? [];
53
+ const allTypes = [...new Set([...builtInTypes, ...userTypes])].join(', ');
54
+
55
+ const personRelationship = data.existing_item?.relationship ?? data.new_person_relationship ?? 'Unknown';
56
+ const personName = data.existing_item?.name ?? data.new_person_name ?? 'Unknown';
57
+
58
+ // ── WHO YOU ARE ANALYZING ──────────────────────────────────────────────────
59
+ // This section anchors the model on the target person before anything else.
60
+ // Local models lose context from long prompts — this must be first.
61
+
62
+ const personFocusSection = isNewItem
63
+ ? `# WHO YOU ARE ANALYZING
64
+
65
+ NEW PERSON — not yet in system
66
+ Relationship: ${personRelationship}
67
+ Name: ${personName === 'Unknown' ? 'Not yet known' : personName}${data.new_person_description ? `\nWhat we know: ${data.new_person_description}` : ''}
68
+
69
+ Your ONLY job is to find information about the HUMAN USER's **${personRelationship}** in the conversation and create their record. Ignore all other people mentioned — they each have their own separate records.`
70
+ : `# WHO YOU ARE ANALYZING
71
+
72
+ EXISTING PERSON RECORD — update only if the Most Recent Messages contain new information
73
+ Name: ${personName}
74
+ Relationship: ${personRelationship}
75
+
76
+ Current record:
77
+ \`\`\`json
78
+ ${formatExistingPerson(data.existing_item!)}
79
+ \`\`\`
32
80
 
33
- const nameSection = `The person's actual name, or the clearest available identifier.
81
+ Your ONLY job is to update THIS SPECIFIC PERSON's record based on the Most Recent Messages. Ignore all other people mentioned — they each have their own separate records.`;
34
82
 
35
- Only update when you learn something more specific.
83
+ // ── TASK ──────────────────────────────────────────────────────────────────
36
84
 
37
- Examples: "Unknown woman" → "Carol", "Mom" → "Carol (Mom)", "David" → "David Kim"`;
85
+ const taskSection = isNewItem
86
+ ? `# Task
38
87
 
39
- const descriptionSection = `A concise summary of who this person is and how they relate to the HUMAN USER. Personas use this to recognize this person and engage meaningfully when they come up.
88
+ You are creating a new PERSON record based on what you find about the HUMAN USER's **${personRelationship}** in the conversation.
89
+
90
+ The record you create will be referenced by personas in future conversations — keep it accurate, brief, and useful.`
91
+ : `# Task
92
+
93
+ You are scanning a conversation to update a PERSON record in the HUMAN USER's life.
94
+
95
+ Apply changes to the record above **ONLY IF DOING SO WILL PROVIDE THE HUMAN USER WITH A BETTER EXPERIENCE IN THE FUTURE**.
96
+
97
+ Detail you add should:
98
+ 1. Be meaningful, accurate, or still true to the HUMAN USER in six months or more
99
+ 2. **NOT** already be present in the record above`;
100
+
101
+ // ── DESCRIPTION SECTION ───────────────────────────────────────────────────
102
+ // New: shorter, no "don't accumulate" guidance (nothing to accumulate yet)
103
+ // Existing: full synthesis guidance
104
+
105
+ const descriptionSection = isNewItem
106
+ ? `A concise summary of who this person is and how they relate to the HUMAN USER. Keep it brief and factual — only what you can confirm from the conversation.
107
+
108
+ - Capture who this person IS — their role in the user's life
109
+ - Be useful to a persona who's never heard this person's name before
110
+ - 1-3 sentences maximum
111
+ - If you know their birth date or birth year, include it as a date (e.g. "born 1986-10-28") — never as a current age (ages change, dates don't)
112
+
113
+ **ABSOLUTELY VITAL**: Do **NOT** embellish. Record only what the user actually said or demonstrated.`
114
+ : `A concise summary of who this person is and how they relate to the HUMAN USER. Personas use this to recognize this person and engage meaningfully when they come up.
40
115
 
41
116
  ## CRITICAL: Synthesize, don't accumulate
42
117
 
43
118
  Every update must **rewrite** the description as a current-state summary. Never append to it.
44
119
 
45
- **Good description**: "Borfinda, partner of 12 years. Former marine biologist, now stay-at-home parent. Tends to ground the user when they spiral; dry sense of humor. Two kids together."
120
+ **Good**: "Borfinda, partner of 12 years. Former marine biologist, now stay-at-home parent. Tends to ground the user when they spiral; dry sense of humor. Two kids together."
46
121
 
47
- **Bad description**: "Borfinda was mentioned when the user talked about moving. In a later conversation she came up again during the work stress discussion. Most recently the user said she was supportive."
122
+ **Bad**: "Borfinda was mentioned when the user talked about moving. In a later conversation she came up again during the work stress discussion. Most recently the user said she was supportive."
48
123
 
49
124
  The description should:
50
125
  - Capture who this person IS — their role, characteristics, relationship texture
51
126
  - Include what the HUMAN USER has revealed about them over time
52
- - Be useful to a persona who's never heard this person's name before
53
127
  - Read as a brief, confident summary — not a log of when they were mentioned
128
+ - Not exceed 3-4 sentences
54
129
 
55
130
  The description should NOT:
56
131
  - Append "Most recently:", "Latest mention:", or any temporal marker
57
132
  - Accumulate a session-by-session history of every time this person came up
58
133
  - Speculate about the person based on thin evidence
59
- - Exceed 3-4 sentences under any circumstances
134
+ - Record someone's age record their birth date or birth year instead (e.g. "born 1981" not "age 44")
60
135
 
61
136
  **ABSOLUTELY VITAL**: Do **NOT** embellish — personas use their own voice. Record what the user actually said or demonstrated, not your interpretation of its emotional significance.`;
62
137
 
63
- const relationshipSection = `## Relationship (\`relationship\`)
138
+ // ── IDENTIFIERS ───────────────────────────────────────────────────────────
64
139
 
65
- How the HUMAN USER is currently related to this PERSON.
66
-
67
- Once known, this field changes infrequently a "Father" may later be clarified to "Step-Father", but is unlikely to become "Uncle".
68
-
69
- Keep it concise and specific. Avoid vague labels.
70
-
71
- Examples: "Unknown" → "Coworker", "Mother" → "Step-Mother", "Fiance" → "Spouse", "AI Persona" → "AI Companion"`;
140
+ const identityGuard = humanName
141
+ ? `CRITICAL: The HUMAN USER is ${humanName}. They wrote these messages. Do NOT assign their names, nicknames, or handles as identifiers for this person's record — UNLESS this IS the user's own Self record (relationship: "Self").`
142
+ : `CRITICAL: The HUMAN USER wrote these messages. Do NOT assign their own names or handles as identifiers for this person's recordUNLESS this IS the user's own Self record (relationship: "Self"). Do NOT return \`relationship: "Self"\` unless you are certain this record is about the human user themselves.`;
72
143
 
73
- const exposureSection = `## Desired Exposure (\`exposure_desired\`)
144
+ const isUnknownNewPerson = isNewItem && personName === 'Unknown';
145
+ const unknownIdentifierGuard = isUnknownNewPerson
146
+ ? `\nThis person's name is not yet known. ONLY add \`identifiers\` if their name, handle, or email is explicitly stated in the conversation about THEM specifically — not inferred, not guessed.`
147
+ : '';
74
148
 
75
- How much the HUMAN USER wants to talk about this PERSON.
149
+ const identifierSection = `${identityGuard}${unknownIdentifierGuard}
76
150
 
77
- Scale of 0.0 to 1.0:
78
- - 0.0: Never wants to hear about this PERSON again
79
- - 0.5: Average amount of engagement
80
- - 1.0: This PERSON is the sole focus of their existence
151
+ If you spot a platform handle, username, email, nickname, or full name explicitly mentioned in the conversation that isn't already in the person's identifiers, include it in \`identifiers_to_add\` (updates) or \`identifiers\` (new records). Always mark exactly one identifier as \`"is_primary": true\` — prefer the most formal or complete name.
81
152
 
82
- Do not make micro-adjustments. Close enough is OK.
83
-
84
- ## Exposure Impact (\`exposure_impact\`)
85
-
86
- Not in the current data — but include it in your response.
87
-
88
- How much this conversation should count toward exposure tracking:
89
- - "high": Long, detailed conversation exclusively about this PERSON
90
- - "medium": Long OR detailed conversation about this PERSON
91
- - "low": The conversation touched on this PERSON briefly
92
- - "none": Only alluded to or hinted at`;
93
-
94
- const currentDetailsSection = data.existing_item
95
- ? `\`\`\`json
96
- ${formatExistingPerson(data.existing_item)}
97
- \`\`\`
153
+ For persons with a known relationship (Father, Mother, Sibling, etc.), also look for informal terms the HUMAN USER uses to address or refer to THAT SPECIFIC PERSON (\`Dad\`, \`Pop\`, \`Mom\`, \`Sis\`, etc.) and add them as \`{ "type": "Relationship", "value": "..." }\` identifiers.
98
154
 
99
- You are UPDATING an existing PERSON.`
100
- : `**NEW PERSON — NOT YET IN SYSTEM**
155
+ NEVER add dates, ages, birthdays, or anniversaries as identifiers. These are not identifying labels — if known, include them in the description instead.
101
156
 
102
- You are CREATING a new PERSON from what was discovered:
103
- \`\`\`json
104
- {
105
- "name": "${data.new_person_name ?? "Unknown"}",
106
- "description": "${data.new_person_description ?? "Details unknown"}",
107
- "relationship": "${data.new_person_relationship ?? "Unknown"}"
108
- }
109
- \`\`\`
157
+ Known identifier types: ${allTypes}. If unsure of type, use \`Nickname\`.`;
110
158
 
111
- Return all fields based on what you find in the conversation.`;
159
+ // ── OUTPUT FORMAT ─────────────────────────────────────────────────────────
112
160
 
113
- const jsonTemplate = `{
114
- "name": "...",
161
+ const jsonTemplate = isNewItem
162
+ ? `{
163
+ "identifiers": [
164
+ { "type": "Full Name", "value": "Borfinda Lastname", "is_primary": true },
165
+ { "type": "Nickname", "value": "Borfi" }
166
+ ],
167
+ "description": "...",
168
+ "sentiment": 0.0,
169
+ "relationship": "Mother|Friend|Coworker|AI Companion|etc.",
170
+ "exposure_desired": 0.5,
171
+ "exposure_impact": "high|medium|low|none",
172
+ "quotes": [
173
+ { "text": "exact phrase from message", "reason": "why this matters" }
174
+ ]
175
+ }`
176
+ : `{
177
+ "identifiers_to_add": [{ "type": "GitHub", "value": "handle" }],
115
178
  "description": "...",
116
179
  "sentiment": 0.0,
117
180
  "relationship": "Mother|Friend|Coworker|AI Companion|etc.",
118
181
  "exposure_desired": 0.5,
119
182
  "exposure_impact": "high|medium|low|none",
120
183
  "quotes": [
121
- {
122
- "text": "exact phrase from message",
123
- "reason": "why this matters"
124
- }
184
+ { "text": "exact phrase from message", "reason": "why this matters" }
125
185
  ]
126
186
  }`;
127
187
 
128
- const system = `# Task
129
-
130
- You are scanning a conversation to deeply understand a PERSON in the HUMAN USER's life.
188
+ // ── SYSTEM PROMPT ASSEMBLY ─────────────────────────────────────────────────
189
+ // Order: WHO (anchor) → task → participant context → field definitions →
190
+ // output format. Person details are in WHO, not repeated at the bottom.
131
191
 
132
- Your job is to take that analysis and apply it to the record we already have **IF DOING SO WILL PROVIDE THE HUMAN USER WITH A BETTER EXPERIENCE IN THE FUTURE**.
192
+ const system = `${personFocusSection}
133
193
 
134
- This means detail you add should:
135
- 1. Be meaningful, accurate, or still true to the HUMAN USER in six months or more
136
- 2. **NOT** already be present in the description or name of the PERSON
194
+ ${taskSection}
137
195
 
138
- This PERSON will be recorded in the HUMAN USER's profile for agents and personas to later reference.
196
+ ${participantContextSection(data.participant_context)}# Field Definitions
139
197
 
140
- # Field Definitions
141
-
142
- ## Name (\`name\`)
143
- ${nameSection}
198
+ ## Identifiers
199
+ ${identifierSection}
144
200
 
145
201
  ## Description (\`description\`)
146
202
  ${descriptionSection}
@@ -158,40 +214,55 @@ Scale of -1.0 to 1.0:
158
214
 
159
215
  Do not make micro-adjustments. Close enough is OK.
160
216
 
161
- ${relationshipSection}
217
+ ## Relationship (\`relationship\`)
162
218
 
163
- ${exposureSection}
219
+ How the HUMAN USER is currently related to this PERSON.
164
220
 
165
- ## Quotes
221
+ Once known, this field changes infrequently — a "Father" may later be clarified to "Step-Father", but is unlikely to become "Uncle".
222
+
223
+ Keep it concise and specific. Avoid vague labels.
224
+
225
+ Examples: "Unknown" → "Coworker", "Mother" → "Step-Mother", "Fiance" → "Spouse", "AI Persona" → "AI Companion"
226
+
227
+ ## Desired Exposure (\`exposure_desired\`)
228
+
229
+ How much the HUMAN USER wants to talk about this PERSON.
230
+
231
+ Scale of 0.0 to 1.0:
232
+ - 0.0: Never wants to hear about this PERSON again
233
+ - 0.5: Average amount of engagement
234
+ - 1.0: This PERSON is the sole focus of their existence
235
+
236
+ Do not make micro-adjustments. Close enough is OK.
166
237
 
167
- In addition to updating the PERSON, identify any **memorable, funny, important, or stand-out phrases** from the Most Recent Messages that relate to this PERSON.
238
+ ## Exposure Impact (\`exposure_impact\`)
168
239
 
169
- ### What Makes a Quote Worth Preserving
240
+ Not in the current data but include it in your response.
241
+
242
+ How much this conversation should count toward exposure tracking:
243
+ - "high": Long, detailed conversation exclusively about this PERSON
244
+ - "medium": Long OR detailed conversation about this PERSON
245
+ - "low": The conversation touched on this PERSON briefly
246
+ - "none": Only alluded to or hinted at
247
+
248
+ ## Quotes
249
+
250
+ Identify any **memorable, funny, important, or stand-out phrases** from the Most Recent Messages that relate to this PERSON.
170
251
 
171
252
  **Prioritize:**
172
253
  - Humor, wit, colorful language, creative profanity
173
254
  - Emotional outbursts (positive or negative) — the raw stuff
174
255
  - Phrases that reveal how the HUMAN USER feels about this PERSON
175
256
  - Things you'd quote back to them later to make them laugh or think
176
- - Unique expressions or turns of phrase about or from this PERSON
177
- - Quotable moments from EITHER speaker — humans AND AI personas both say memorable things
178
257
 
179
- **NEVER extract these — they are NOT quotes:**
258
+ **NEVER extract these:**
180
259
  - Technical identifiers: ARNs, URLs, file paths, UUIDs, config keys
181
- - AI agent self-talk: "I notice I'm in Plan Mode", "I'll start by...", status updates
182
- - AI apologies or acknowledgments: "You're absolutely right", "I apologize for that"
260
+ - AI agent self-talk or apologies
183
261
  - Generic statements that could apply to anyone
184
- - Credentials, secrets, connection strings, or anything that looks like an access token
185
-
186
- **The litmus test**: Would you bring this up at a bar with a friend? Would it make someone laugh, think, or feel something?
187
- - "She's the only person who can make me feel simultaneously stupid and brilliant" → YES.
188
- - "Borfinda was mentioned in the context of the Minnesota discussion" → NO. That's a note, not a quote.
189
-
190
- **When in doubt, leave it out.** An empty quotes array is always acceptable.
191
262
 
192
- **CRITICAL**: Return the EXACT text as it appears in the message. **WE CAN ONLY USE IT IF WE FIND IT IN THE TEXT.**
263
+ **CRITICAL**: Return the EXACT text as it appears in the message.
193
264
 
194
- # CRITICAL INSTRUCTIONS
265
+ # Output
195
266
 
196
267
  ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided for context only — it has already been processed.
197
268
 
@@ -199,19 +270,17 @@ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided
199
270
  ${jsonTemplate}
200
271
  \`\`\`
201
272
 
202
- When returning a record, **ALWAYS** include \`name\`, \`description\`, and \`sentiment\`.
273
+ When returning a record, **ALWAYS** include \`description\` and \`sentiment\`. Do NOT return \`relationship: "Self"\` unless this record IS the human user themselves.
203
274
 
204
- If you find **NO EVIDENCE** of this PERSON in the "Most Recent Messages", respond with: \`{}\`
275
+ If you find **NO EVIDENCE** of the HUMAN USER's **${personRelationship}** in the "Most Recent Messages", respond with: \`{}\`
205
276
 
206
277
  If **NO CHANGES** are required, respond with: \`{}\`
207
278
 
208
279
  An empty object is the MOST COMMON expected response.
209
-
210
- # Current Details of PERSON
211
-
212
- ${currentDetailsSection}
213
280
  `;
214
281
 
282
+ // ── USER PROMPT ───────────────────────────────────────────────────────────
283
+
215
284
  const earlierSection =
216
285
  data.messages_context.length > 0
217
286
  ? `## Earlier Conversation
@@ -223,19 +292,25 @@ ${formatMessagesAsPlaceholders(data.messages_context, personaName)}
223
292
  const recentSection = `## Most Recent Messages
224
293
  ${formatMessagesAsPlaceholders(data.messages_analyze, personaName)}`;
225
294
 
295
+ const personReminder = personName !== 'Unknown'
296
+ ? `Remember: You are analyzing the record for **${personName}** (${personRelationship}). Focus only on information about this specific person.`
297
+ : `Remember: You are analyzing the record for the HUMAN USER's **${personRelationship}**. Focus only on information about this specific person.`;
298
+
226
299
  const user = `# Conversation
227
300
  ${earlierSection}${recentSection}
228
301
 
229
302
  ---
230
303
 
231
- Analyze the Most Recent Messages and update the PERSON if warranted.
304
+ ${personReminder}
305
+
306
+ Analyze the Most Recent Messages and ${isNewItem ? 'create the PERSON record' : 'update the PERSON record'} if warranted.
232
307
 
233
308
  **Return JSON:**
234
309
  \`\`\`json
235
310
  ${jsonTemplate}
236
311
  \`\`\`
237
312
 
238
- If no changes are needed, respond with: \`{}\``;
313
+ If no ${isNewItem ? 'information found' : 'changes are needed'}, respond with: \`{}\``;
239
314
 
240
315
  return { system, user };
241
316
  }
@@ -250,7 +250,7 @@ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided
250
250
  ${jsonTemplate}
251
251
  \`\`\`
252
252
 
253
- When returning a record, **ALWAYS** include \`name\`, \`description\`, and \`sentiment\`.
253
+ When returning a record, always include \`sentiment\`. Include \`name\` only if you are changing it; omit it to keep the existing name. Always include \`description\` when returning a record.
254
254
 
255
255
  If you find **NO EVIDENCE** of this TOPIC in the "Most Recent Messages", respond with: \`{}\`
256
256
 
@@ -26,7 +26,10 @@ export interface TopicScanPromptData extends BaseScanPromptData {
26
26
  participant_context?: ParticipantContext;
27
27
  }
28
28
 
29
- export interface PersonScanPromptData extends BaseScanPromptData {}
29
+ export interface PersonScanPromptData extends BaseScanPromptData {
30
+ participant_context?: ParticipantContext;
31
+ known_identifier_types?: string[];
32
+ }
30
33
 
31
34
  export interface FactFindPromptData {
32
35
  persona_name: string;
@@ -50,6 +53,7 @@ export interface TopicScanCandidate {
50
53
 
51
54
  export interface PersonScanCandidate {
52
55
  name: string;
56
+ identifiers?: Array<{ type: string; value: string; is_primary?: boolean }>;
53
57
  description: string;
54
58
  relationship: string;
55
59
  reason: string;
@@ -28,19 +28,12 @@ export type {
28
28
 
29
29
  export {
30
30
  buildPersonaTraitExtractionPrompt,
31
- buildPersonaTopicScanPrompt,
32
- buildPersonaTopicMatchPrompt,
33
- buildPersonaTopicUpdatePrompt,
31
+ buildPersonaTopicRatingPrompt,
34
32
  } from "./persona/index.js";
35
33
  export type {
36
34
  PersonaTraitExtractionPromptData,
37
- PersonaTopicScanPromptData,
38
- PersonaTopicScanCandidate,
39
- PersonaTopicScanResult,
40
- PersonaTopicMatchPromptData,
41
- PersonaTopicMatchResult,
42
- PersonaTopicUpdatePromptData,
43
- PersonaTopicUpdateResult,
35
+ PersonaTopicRatingPromptData,
36
+ PersonaTopicRatingResult,
44
37
  TraitResult,
45
38
  } from "./persona/types.js";
46
39
 
@@ -1,17 +1,12 @@
1
1
  import type { Message } from "../core/types.js";
2
+ import { getMessageContent } from "../core/handlers/utils.js";
2
3
 
3
4
  const MESSAGE_PLACEHOLDER_REGEX = /\[mid:([a-zA-Z0-9_-]+):([^\]]+)\]/g;
4
5
 
5
- /**
6
- * Returns the display text for a message from its structured fields.
7
- * - action_response as _italics_
8
- * - verbal_response as plain text
9
- * - silence_reason shown so the user understands why a persona stayed silent
10
- */
11
6
  export function getMessageDisplayText(message: Message): string | null {
12
7
  const parts: string[] = [];
13
- if (message.action_response) parts.push(`_${message.action_response}_`);
14
- if (message.verbal_response) parts.push(message.verbal_response);
8
+ const content = getMessageContent(message);
9
+ if (content) parts.push(content);
15
10
  if (message.silence_reason) {
16
11
  const name = message.speaker_name ?? 'Persona';
17
12
  parts.push(`[${name} chose not to respond because: ${message.silence_reason}]`);
@@ -20,23 +15,14 @@ export function getMessageDisplayText(message: Message): string | null {
20
15
  return parts.join('\n\n');
21
16
  }
22
17
 
23
- /**
24
- * Builds the content string for a ChatMessage sent to the LLM.
25
- * Unlike getMessageDisplayText (which is for frontend rendering and skips silence),
26
- * this includes ALL structured fields so the persona has full conversational context:
27
- * - action_response as _italics_
28
- * - verbal_response as plain text
29
- * - silence_reason as "You chose not to respond because: ..."
30
- */
31
18
  export function buildChatMessageContent(message: Message): string {
32
19
  const parts: string[] = [];
33
- if (message.action_response) parts.push(`_${message.action_response}_`);
34
-
35
- // Synthesis messages: wrap with context for LLM, but stored value is clean prompt
36
- if (message._synthesis && message.verbal_response) {
37
- parts.push(`[The user used your conversation to generate an image. The full prompt was: "${message.verbal_response}"]`);
38
- } else if (message.verbal_response) {
39
- parts.push(message.verbal_response);
20
+ const content = getMessageContent(message);
21
+
22
+ if (message._synthesis && content) {
23
+ parts.push(`[The user used your conversation to generate an image. The full prompt was: "${content}"]`);
24
+ } else if (content) {
25
+ parts.push(content);
40
26
  }
41
27
  if (message.silence_reason) {
42
28
  parts.push(`You chose not to respond because: ${message.silence_reason}`);
@@ -1,16 +1,9 @@
1
1
  export { buildPersonaTraitExtractionPrompt } from "./traits.js";
2
- export { buildPersonaTopicScanPrompt } from "./topics-scan.js";
3
- export { buildPersonaTopicMatchPrompt } from "./topics-match.js";
4
- export { buildPersonaTopicUpdatePrompt } from "./topics-update.js";
2
+ export { buildPersonaTopicRatingPrompt } from "./topics-rate.js";
5
3
  export type {
6
4
  PersonaTraitExtractionPromptData,
7
- PersonaTopicScanPromptData,
8
- PersonaTopicScanCandidate,
9
- PersonaTopicScanResult,
10
- PersonaTopicMatchPromptData,
11
- PersonaTopicMatchResult,
12
- PersonaTopicUpdatePromptData,
13
- PersonaTopicUpdateResult,
5
+ PersonaTopicRatingPromptData,
6
+ PersonaTopicRatingResult,
14
7
  TraitResult,
15
8
  PromptOutput,
16
9
  } from "./types.js";
@@ -0,0 +1,95 @@
1
+ import type { PersonaTopicRatingPromptData, PromptOutput } from "./types.js";
2
+ import { formatMessagesAsPlaceholders } from "../message-utils.js";
3
+
4
+ export function buildPersonaTopicRatingPrompt(data: PersonaTopicRatingPromptData): PromptOutput {
5
+ if (!data.persona_name) {
6
+ throw new Error("buildPersonaTopicRatingPrompt: persona_name is required");
7
+ }
8
+ if (!data.topics || data.topics.length === 0) {
9
+ throw new Error("buildPersonaTopicRatingPrompt: topics array is required and must not be empty");
10
+ }
11
+
12
+ const personaName = data.persona_name;
13
+
14
+ const topicList = data.topics
15
+ .map(t => `- **${t.name}**: ${t.description_hint}`)
16
+ .join("\n");
17
+
18
+ const system = `# Task
19
+
20
+ You are rating how much each of ${personaName}'s topics was discussed in recent messages.
21
+
22
+ Your ONLY job is to rate exposure for the provided topics. Do NOT invent new topics. Do NOT analyze deeply. Just rate what you observe.
23
+
24
+ # ${personaName}'s Topics
25
+
26
+ ${topicList}
27
+
28
+ # Rating Scale
29
+
30
+ For each topic, rate how much it was discussed in the "Most Recent Messages":
31
+
32
+ - **none**: Topic not mentioned or only trivial reference
33
+ - **low**: Mentioned briefly (1-2 messages, passing reference)
34
+ - **medium**: Discussed with some depth (3-5 messages or sustained engagement)
35
+ - **high**: Major focus of conversation (6+ messages or intense discussion)
36
+
37
+ # Critical Rules
38
+
39
+ 1. **ONLY rate what's actually in the messages**. Do not infer or imagine.
40
+ 2. **"none" is the expected answer for most topics most days**. Only rate higher if there's clear evidence.
41
+ 3. **Rate based on the Most Recent Messages section ONLY**. Earlier conversation is context.
42
+ 4. **Do NOT invent new topics**. Only rate the topics listed above.
43
+
44
+ # Response Format
45
+
46
+ Return JSON with ratings for each topic:
47
+
48
+ \`\`\`json
49
+ {
50
+ "ratings": [
51
+ {
52
+ "topic_id": "uuid-here",
53
+ "exposure_impact": "none"
54
+ },
55
+ {
56
+ "topic_id": "another-uuid",
57
+ "exposure_impact": "medium"
58
+ }
59
+ ]
60
+ }
61
+ \`\`\`
62
+
63
+ **Return JSON only.**`;
64
+
65
+ const earlierSection = data.messages_context.length > 0
66
+ ? `## Earlier Conversation (context only)
67
+ ${formatMessagesAsPlaceholders(data.messages_context, personaName)}
68
+
69
+ `
70
+ : '';
71
+
72
+ const recentSection = `## Most Recent Messages (rate exposure based on these)
73
+ ${formatMessagesAsPlaceholders(data.messages_analyze, personaName)}`;
74
+
75
+ const user = `# Conversation
76
+ ${earlierSection}${recentSection}
77
+
78
+ ---
79
+
80
+ Rate how much each of ${personaName}'s topics was discussed in the "Most Recent Messages".
81
+
82
+ **Return JSON:**
83
+ \`\`\`json
84
+ {
85
+ "ratings": [
86
+ {
87
+ "topic_id": "topic-uuid",
88
+ "exposure_impact": "none" | "low" | "medium" | "high"
89
+ }
90
+ ]
91
+ }
92
+ \`\`\``;
93
+
94
+ return { system, user };
95
+ }