ei-tui 0.1.3 → 0.1.5

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 (44) hide show
  1. package/README.md +36 -35
  2. package/package.json +6 -2
  3. package/src/README.md +85 -1
  4. package/src/cli/README.md +30 -20
  5. package/src/cli/retrieval.ts +5 -17
  6. package/src/cli.ts +69 -0
  7. package/src/core/handlers/index.ts +195 -172
  8. package/src/core/orchestrators/ceremony.ts +4 -4
  9. package/src/core/orchestrators/extraction-chunker.ts +3 -3
  10. package/src/core/processor.ts +245 -77
  11. package/src/core/queue-processor.ts +3 -26
  12. package/src/core/state/checkpoints.ts +4 -0
  13. package/src/core/state/personas.ts +13 -1
  14. package/src/core/state/queue.ts +80 -23
  15. package/src/core/state-manager.ts +36 -10
  16. package/src/core/types.ts +23 -11
  17. package/src/core/utils/crossFind.ts +44 -0
  18. package/src/core/utils/index.ts +4 -0
  19. package/src/integrations/opencode/importer.ts +118 -691
  20. package/src/prompts/heartbeat/check.ts +27 -13
  21. package/src/prompts/heartbeat/ei.ts +65 -136
  22. package/src/prompts/heartbeat/types.ts +47 -17
  23. package/src/prompts/human/item-update.ts +20 -8
  24. package/src/prompts/index.ts +2 -5
  25. package/src/prompts/message-utils.ts +42 -3
  26. package/src/prompts/response/index.ts +13 -6
  27. package/src/prompts/response/sections.ts +65 -12
  28. package/src/prompts/response/types.ts +10 -0
  29. package/tui/README.md +89 -4
  30. package/tui/src/commands/dlq.ts +75 -0
  31. package/tui/src/commands/editor.tsx +1 -1
  32. package/tui/src/commands/queue.ts +77 -0
  33. package/tui/src/components/CommandSuggest.tsx +50 -0
  34. package/tui/src/components/MessageList.tsx +12 -2
  35. package/tui/src/components/PromptInput.tsx +118 -30
  36. package/tui/src/components/Sidebar.tsx +6 -2
  37. package/tui/src/components/StatusBar.tsx +12 -5
  38. package/tui/src/context/ei.tsx +43 -3
  39. package/tui/src/context/keyboard.tsx +90 -2
  40. package/tui/src/util/clipboard.ts +73 -0
  41. package/tui/src/util/yaml-serializers.ts +81 -11
  42. package/src/prompts/validation/ei.ts +0 -93
  43. package/src/prompts/validation/index.ts +0 -6
  44. package/src/prompts/validation/types.ts +0 -22
@@ -6,9 +6,8 @@
6
6
  */
7
7
 
8
8
  import type { HeartbeatCheckPromptData, PromptOutput } from "./types.js";
9
- import type { Message, Topic, Person } from "../../core/types.js";
10
- import { formatMessagesAsPlaceholders } from "../message-utils.js";
11
-
9
+ import { type Message, type Topic, type Person } from "../../core/types.js";
10
+ import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
12
11
  function formatTopicsWithGaps(topics: Topic[]): string {
13
12
  if (topics.length === 0) return "(No topics with engagement gaps)";
14
13
 
@@ -31,23 +30,37 @@ function formatPeopleWithGaps(people: Person[]): string {
31
30
  .join('\n');
32
31
  }
33
32
 
33
+ /**
34
+ * A "real" persona message is one the persona actually said to the human.
35
+ * silence_reason messages (persona chose not to speak) and action-only messages
36
+ * (no verbal_response) don't count as conversational outreach.
37
+ */
38
+ function isConversationalMessage(m: Message): boolean {
39
+ if (m.role !== 'system') return false;
40
+ if (m.silence_reason !== undefined) return false;
41
+ // Action-only: has action but no verbal response
42
+ if (!m.verbal_response) return false;
43
+ return true;
44
+ }
45
+
34
46
  function countTrailingPersonaMessages(history: Message[]): number {
35
47
  if (history.length === 0) return 0;
36
48
 
37
49
  let count = 0;
38
50
  for (let i = history.length - 1; i >= 0; i--) {
39
- // In heartbeat context, persona messages are "system" role (not from human)
40
- if (history[i].role === "system") {
41
- count++;
42
- } else {
43
- break;
44
- }
51
+ const msg = history[i];
52
+ if (msg.role === 'human') break;
53
+ if (isConversationalMessage(msg)) count++;
54
+ // Skip non-conversational system messages and keep looking back
45
55
  }
46
56
  return count;
47
57
  }
48
58
 
49
59
  function getLastPersonaMessage(history: Message[]): Message | undefined {
50
- return history.filter(m => m.role === "system").slice(-1)[0];
60
+ for (let i = history.length - 1; i >= 0; i--) {
61
+ if (isConversationalMessage(history[i])) return history[i];
62
+ }
63
+ return undefined;
51
64
  }
52
65
 
53
66
  /**
@@ -148,9 +161,10 @@ ${formatMessagesAsPlaceholders(data.recent_history, personaName)}`;
148
161
 
149
162
  let unansweredWarning = '';
150
163
  if (lastPersonaMsg && consecutiveMessages >= 1) {
151
- const preview = lastPersonaMsg.content.length > 100
152
- ? lastPersonaMsg.content.substring(0, 100) + "..."
153
- : lastPersonaMsg.content;
164
+ const rawPreview = getMessageDisplayText(lastPersonaMsg) ?? "";
165
+ const preview = rawPreview.length > 100
166
+ ? rawPreview.substring(0, 100) + "..."
167
+ : rawPreview;
154
168
 
155
169
  unansweredWarning = `
156
170
  ### CRITICAL: You Already Reached Out
@@ -1,51 +1,41 @@
1
- /**
2
- * Ei Heartbeat Prompt Builder
3
- *
4
- * Ei's heartbeat is special - it considers not just engagement gaps but also
5
- * inactive personas and cross-system health. Ei is the "system guide" and
6
- * should prompt the user about neglected relationships.
7
- */
8
-
9
- import type { EiHeartbeatPromptData, PromptOutput } from "./types.js";
10
- import type { Message, Topic, Person } from "../../core/types.js";
11
- import { formatMessagesAsPlaceholders } from "../message-utils.js";
12
-
13
- function formatTopicsWithGaps(topics: Topic[]): string {
14
- if (topics.length === 0) return "(No topics with engagement gaps)";
15
-
16
- return topics
17
- .slice(0, 10) // Top 10 most under-discussed
18
- .map(t => {
19
- const gap = t.exposure_desired - t.exposure_current;
20
- const sentiment = t.sentiment > 0.3 ? "😊" : t.sentiment < -0.3 ? "😟" : "😐";
21
- return `- **${t.name}** ${sentiment} (gap: +${gap.toFixed(2)}): ${t.description}`;
22
- })
23
- .join('\n');
24
- }
25
-
26
- function formatPeopleWithGaps(people: Person[]): string {
27
- if (people.length === 0) return "(No people with engagement gaps)";
28
-
29
- return people
30
- .slice(0, 10)
31
- .map(p => {
32
- const gap = p.exposure_desired - p.exposure_current;
33
- return `- **${p.name}** (${p.relationship}, gap: +${gap.toFixed(2)}): ${p.description}`;
34
- })
35
- .join('\n');
1
+ import type { EiHeartbeatPromptData, EiHeartbeatItem, PromptOutput } from "./types.js";
2
+ import type { Message } from "../../core/types.js";
3
+ import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
4
+
5
+ function formatItem(item: EiHeartbeatItem): string {
6
+ switch (item.type) {
7
+ case "Fact Check":
8
+ return [
9
+ `- **${item.id}** Fact Check: "${item.name}" ${item.description}`,
10
+ item.quote ? ` Quote: "${item.quote}"` : "",
11
+ ].filter(Boolean).join("\n");
12
+
13
+ case "Low-Engagement Person":
14
+ return [
15
+ `- **${item.id}** Low-Engagement Person: ${item.name} (${item.relationship}, gap: ${item.engagement_delta})`,
16
+ ` ${item.description}`,
17
+ item.quote ? ` Quote: "${item.quote}"` : "",
18
+ ].filter(Boolean).join("\n");
19
+
20
+ case "Low-Engagement Topic":
21
+ return [
22
+ `- **${item.id}** Low-Engagement Topic: ${item.name} (gap: ${item.engagement_delta})`,
23
+ ` ${item.description}`,
24
+ item.quote ? ` Quote: "${item.quote}"` : "",
25
+ ].filter(Boolean).join("\n");
26
+
27
+ case "Inactive Persona": {
28
+ const desc = item.short_description ? ` — ${item.short_description}` : "";
29
+ return `- **${item.id}** Inactive Persona: ${item.name}${desc} (${item.days_inactive} days inactive)`;
30
+ }
31
+ }
36
32
  }
37
33
 
38
34
  function countTrailingPersonaMessages(history: Message[]): number {
39
- if (history.length === 0) return 0;
40
-
41
35
  let count = 0;
42
36
  for (let i = history.length - 1; i >= 0; i--) {
43
- // In heartbeat context, Ei's messages are "system" role (not from human)
44
- if (history[i].role === "system") {
45
- count++;
46
- } else {
47
- break;
48
- }
37
+ if (history[i].role === "system") count++;
38
+ else break;
49
39
  }
50
40
  return count;
51
41
  }
@@ -54,119 +44,57 @@ function getLastPersonaMessage(history: Message[]): Message | undefined {
54
44
  return history.filter(m => m.role === "system").slice(-1)[0];
55
45
  }
56
46
 
57
- function formatInactivePersonas(personas: EiHeartbeatPromptData["inactive_personas"]): string {
58
- if (personas.length === 0) return "(All personas have been active recently)";
59
-
60
- return personas
61
- .map(p => {
62
- const desc = p.short_description ? ` - ${p.short_description}` : "";
63
- return `- **${p.name}**${desc}: ${p.days_inactive} days inactive`;
64
- })
65
- .join('\n');
66
- }
67
-
68
- /**
69
- * Build Ei heartbeat prompts.
70
- *
71
- * Ei sees ALL data and has special responsibilities:
72
- * - System health monitoring
73
- * - Gentle nudges about neglected relationships
74
- * - Encouraging human-to-human connection
75
- */
76
47
  export function buildEiHeartbeatPrompt(data: EiHeartbeatPromptData): PromptOutput {
77
- // Build system prompt fragments
78
- const roleFragment = `You are Ei, the user's personal companion and system guide.
48
+ const itemsSection = data.items.length === 0
49
+ ? "(Nothing requires attention right now)"
50
+ : data.items.map(formatItem).join("\n\n");
51
+
52
+ const system = `You are Ei, the user's personal companion and system guide.
79
53
 
80
- You are NOT having a conversation right now - you are deciding IF and WHAT to discuss with your human friend.
54
+ You are NOT having a conversation right now you are deciding IF and WHAT to discuss with your human friend.
81
55
 
82
56
  Your unique role:
83
57
  - You see ALL of the human's data across all groups
84
58
  - You help them reflect on their life and relationships
85
59
  - You gently encourage human-to-human connection
86
- - You care about their overall wellbeing, not just being helpful`;
87
-
88
- const systemHealthFragment = `## System Health
89
-
90
- ### Pending Validations
91
- ${data.pending_validations > 0
92
- ? `There are **${data.pending_validations}** items from other personas that need your review.`
93
- : "No pending validations."}
94
-
95
- ### Inactive Personas
96
- ${formatInactivePersonas(data.inactive_personas)}`;
60
+ - You care about their overall wellbeing, not just being helpful
97
61
 
98
- const humanDataFragment = `## Human's Current State
62
+ ## Items That May Need Attention
99
63
 
100
- ### Under-Discussed Topics
101
- These are topics they want to talk about more:
64
+ Each item has an ID in brackets. Pick at most ONE to address.
102
65
 
103
- ${formatTopicsWithGaps(data.human.topics)}
66
+ ${itemsSection}
104
67
 
105
- ### Under-Engaged People
106
- These are relationships they might want to nurture:
68
+ ## How to Respond to Each Type
107
69
 
108
- ${formatPeopleWithGaps(data.human.people)}`;
70
+ - **Fact Check**: Do NOT write your own message. Set should_respond=true and provide the id. The system will generate an appropriate canned notification for the user. Leave my_response empty.
71
+ - **Low-Engagement Person / Topic**: Write a natural, warm message that naturally brings up this person or topic. Set the id and my_response.
72
+ - **Inactive Persona**: Write a message that gently mentions the persona might be worth checking in with. Set the id and my_response.
109
73
 
110
- const guidelinesFragment = `## Guidelines for Ei
74
+ ## When NOT to Reach Out
111
75
 
112
- ### Your Priorities (in order)
113
- 1. **Wellbeing first** - If something seems concerning, address it gently
114
- 2. **Human connections** - Encourage real-world relationships over AI dependency
115
- 3. **Reflection** - Help them think, don't do their thinking for them
116
- 4. **System health** - Mention inactive personas or pending validations if relevant
117
-
118
- ### When to Reach Out
119
- - A significant topic has been neglected and you can help them process it
120
- - They might benefit from connecting with someone (real person or persona)
121
- - You have a genuine observation or question
122
- - Pending validations need attention
123
-
124
- ### When NOT to Reach Out
125
- - Recent conversation ended with natural closure
126
- - Nothing meaningful to add
76
+ - Nothing in the list feels meaningful right now
77
+ - You've already sent unanswered messages (see below)
127
78
  - It would feel like nagging
128
- - They seem to need space
129
-
130
- ### Tone
131
- - Warm but not saccharine
132
- - Curious but not intrusive
133
- - Supportive but honest
134
- - A good friend, not a therapist`;
135
79
 
136
- const outputFragment = `## Response Format
80
+ ## Response Format
137
81
 
138
- Return JSON with your priorities and message:
82
+ Pick ONE item (or none):
139
83
 
140
84
  \`\`\`json
141
85
  {
142
86
  "should_respond": true,
143
- "priorities": [
144
- { "type": "topic", "name": "work stress", "reason": "hasn't been discussed in 2 weeks" },
145
- { "type": "persona", "name": "Adventure Guide", "reason": "inactive for 5 days" },
146
- { "type": "person", "name": "Mom", "reason": "they mentioned wanting to call her" }
147
- ],
148
- "message": "Hey! I noticed we haven't talked about work lately - how's that project going?"
87
+ "id": "the-item-id-you-chose",
88
+ "my_response": "Hey, how's your mom doing? You mentioned wanting to call her."
149
89
  }
150
90
  \`\`\`
151
91
 
152
- If you decide NOT to reach out:
92
+ Or if nothing warrants reaching out:
153
93
  \`\`\`json
154
94
  {
155
95
  "should_respond": false
156
96
  }
157
- \`\`\`
158
-
159
- Note: The "priorities" list helps you organize your thoughts. Your message should naturally address the top priority without feeling like a checklist.`;
160
-
161
- const system = `${roleFragment}
162
-
163
- ${systemHealthFragment}
164
-
165
- ${humanDataFragment}
166
-
167
- ${guidelinesFragment}
168
-
169
- ${outputFragment}`;
97
+ \`\`\``;
170
98
 
171
99
  const historySection = `## Recent Conversation History
172
100
 
@@ -174,20 +102,21 @@ ${formatMessagesAsPlaceholders(data.recent_history, "Ei")}`;
174
102
 
175
103
  const consecutiveMessages = countTrailingPersonaMessages(data.recent_history);
176
104
  const lastEiMsg = getLastPersonaMessage(data.recent_history);
177
-
178
- let unansweredWarning = '';
105
+
106
+ let unansweredWarning = "";
179
107
  if (lastEiMsg && consecutiveMessages >= 1) {
180
- const preview = lastEiMsg.content.length > 100
181
- ? lastEiMsg.content.substring(0, 100) + "..."
182
- : lastEiMsg.content;
183
-
108
+ const rawPreview = getMessageDisplayText(lastEiMsg) ?? "";
109
+ const preview = rawPreview.length > 100
110
+ ? rawPreview.substring(0, 100) + "..."
111
+ : rawPreview;
112
+
184
113
  unansweredWarning = `
185
114
  ### CRITICAL: You Already Reached Out
186
115
 
187
116
  Your last message was: "${preview}"
188
117
 
189
118
  The human has NOT responded. DO NOT repeat or rephrase this message.
190
- If you reach out now, it MUST be about something COMPLETELY DIFFERENT - or say nothing.`;
119
+ If you reach out now, it MUST be about something COMPLETELY DIFFERENT or say nothing.`;
191
120
 
192
121
  if (consecutiveMessages >= 2) {
193
122
  unansweredWarning += `
@@ -200,7 +129,7 @@ If you reach out now, it MUST be about something COMPLETELY DIFFERENT - or say n
200
129
  ${unansweredWarning}
201
130
  ---
202
131
 
203
- Based on all the context above, decide: Should you reach out to your human friend right now? If so, what's most important to address?
132
+ Based on all the context above, decide: Should you reach out to your human friend right now? If so, which item above is most worth addressing?
204
133
 
205
134
  Remember: You're their thoughtful companion, not their productivity assistant.`;
206
135
 
@@ -39,32 +39,62 @@ export interface HeartbeatCheckResult {
39
39
  message?: string;
40
40
  }
41
41
 
42
+ // =============================================================================
43
+ // EI HEARTBEAT TYPES
44
+ // =============================================================================
45
+
46
+ /**
47
+ * A single item Ei can choose to address.
48
+ * One of: an unverified fact, an under-engaged person, an under-engaged topic,
49
+ * or an inactive persona.
50
+ */
51
+ export type EiHeartbeatItem =
52
+ | {
53
+ id: string;
54
+ type: "Fact Check";
55
+ name: string;
56
+ description: string;
57
+ quote?: string;
58
+ }
59
+ | {
60
+ id: string;
61
+ type: "Low-Engagement Person";
62
+ engagement_delta: string; // e.g. "25%"
63
+ relationship: string;
64
+ name: string;
65
+ description: string;
66
+ quote?: string;
67
+ }
68
+ | {
69
+ id: string;
70
+ type: "Low-Engagement Topic";
71
+ engagement_delta: string; // e.g. "28%"
72
+ name: string;
73
+ description: string;
74
+ quote?: string;
75
+ }
76
+ | {
77
+ id: string;
78
+ type: "Inactive Persona";
79
+ name: string;
80
+ short_description?: string;
81
+ days_inactive: number;
82
+ };
83
+
42
84
  /**
43
85
  * Data contract for buildEiHeartbeatPrompt
44
86
  */
45
87
  export interface EiHeartbeatPromptData {
46
- human: {
47
- topics: Topic[]; // All topics with gaps
48
- people: Person[]; // All people with gaps
49
- };
50
- inactive_personas: Array<{
51
- name: string;
52
- short_description?: string;
53
- days_inactive: number;
54
- }>;
55
- pending_validations: number; // Count of items needing Ei review
88
+ items: EiHeartbeatItem[];
56
89
  recent_history: Message[];
57
90
  }
58
91
 
59
92
  /**
60
- * Expected LLM response from Ei heartbeat
93
+ * Expected LLM response from Ei heartbeat.
94
+ * Ei picks exactly ONE item by id and optionally writes a message.
61
95
  */
62
96
  export interface EiHeartbeatResult {
63
97
  should_respond: boolean;
64
- priorities?: Array<{
65
- type: "topic" | "persona" | "person";
66
- name: string;
67
- reason: string;
68
- }>;
69
- message?: string;
98
+ id?: string; // ID of the chosen item (required if should_respond is true)
99
+ my_response?: string; // Only used for Person/Topic/Persona items (not Fact Check)
70
100
  }
@@ -240,13 +240,26 @@ In addition to updating the ${typeLabel}, identify any **memorable, funny, impor
240
240
  - Phrases that reveal personality or communication style
241
241
  - Things you'd quote back to them later to make them laugh
242
242
  - Unique expressions, malaphors, or turns of phrase
243
-
244
- **De-prioritize:**
245
- - Dry technical facts (those belong in TOPICS)
246
- - Status updates or process descriptions
247
- - Generic statements that could come from anyone
248
-
249
- A quote like "Does the Pope shit in his hat?" is GOLD. A quote like "We're running a batch of 44k students" is just... data.
243
+ - Quotable moments from EITHER speaker — humans AND AI personas both say memorable things
244
+
245
+ **NEVER extract these they are NOT quotes:**
246
+ - Technical identifiers: ARNs, URLs, file paths, UUIDs, config keys, environment variable values, role/policy names
247
+ - AI agent self-talk: "I notice I'm in Plan Mode", "I'll start by...", "Let me help you with...", status updates about the agent's own process
248
+ - AI apologies or acknowledgments: "You're absolutely right", "I apologize for that overreach", "Good decision to revert"
249
+ - Generic AI instructions or tips: "Remember to include X in your prompts", tool usage advice, workflow suggestions
250
+ - Dry technical facts: infrastructure descriptions, process status, batch sizes, system architecture summaries
251
+ - Status updates or process descriptions: "We're running a batch of...", "The pipeline is...", "I'm currently working on..."
252
+ - Generic statements that could come from anyone or any AI session
253
+ - Credentials, secrets, connection strings, or anything that looks like an access token
254
+
255
+ **The litmus test**: Would you bring this up at a bar with a friend? Would it make someone laugh, think, or feel something?
256
+ - "Does the Pope shit in his hat?" → YES. Hilarious malaphor.
257
+ - "AWSReservedSSO_cmidp-nihl-sandbox-adm_db7b191e026bdd85" → NO. That's a credential.
258
+ - "Slow is smooth. Smooth is fast." → YES (once). Pithy wisdom.
259
+ - "The authentication flow is working correctly now" → NO. Status update.
260
+ - "I built this, and now it's live." → YES. Pride and accomplishment.
261
+
262
+ **When in doubt, leave it out.** An empty quotes array is always acceptable.
250
263
 
251
264
  Return them in the \`quotes\` array:
252
265
 
@@ -266,7 +279,6 @@ Return them in the \`quotes\` array:
266
279
 
267
280
  **CRITICAL**: Return the EXACT text as it appears in the message (spacing, punctuation, formatting, etc.). WE CAN ONLY USE IT IF WE FIND IT IN THE TEXT.
268
281
 
269
-
270
282
  # CRITICAL INSTRUCTIONS
271
283
 
272
284
  ONLY ANALYZE the "Most Recent Messages" in the following conversation. The "Earlier Conversation" is provided for your context and has already been processed!
@@ -10,6 +10,7 @@ export type {
10
10
  HeartbeatCheckResult,
11
11
  EiHeartbeatPromptData,
12
12
  EiHeartbeatResult,
13
+ EiHeartbeatItem,
13
14
  } from "./heartbeat/types.js";
14
15
 
15
16
  export {
@@ -41,11 +42,7 @@ export type {
41
42
  TraitResult,
42
43
  } from "./persona/types.js";
43
44
 
44
- export { buildEiValidationPrompt } from "./validation/index.js";
45
- export type {
46
- EiValidationPromptData,
47
- EiValidationResult,
48
- } from "./validation/types.js";
45
+
49
46
 
50
47
  export {
51
48
  buildHumanFactScanPrompt,
@@ -2,14 +2,52 @@ import type { Message } from "../core/types.js";
2
2
 
3
3
  const MESSAGE_PLACEHOLDER_REGEX = /\[mid:([a-zA-Z0-9_-]+):([^\]]+)\]/g;
4
4
 
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
+ export function getMessageDisplayText(message: Message): string | null {
12
+ const parts: string[] = [];
13
+ if (message.action_response) parts.push(`_${message.action_response}_`);
14
+ if (message.verbal_response) parts.push(message.verbal_response);
15
+ if (message.silence_reason) {
16
+ const name = 'Persona'; // Caller doesn't pass persona name; frontends can override
17
+ parts.push(`[${name} chose not to respond because: ${message.silence_reason}]`);
18
+ }
19
+ if (parts.length === 0) return null;
20
+ return parts.join('\n\n');
21
+ }
22
+
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
+ export function buildChatMessageContent(message: Message): string {
32
+ const parts: string[] = [];
33
+ if (message.action_response) parts.push(`_${message.action_response}_`);
34
+ if (message.verbal_response) parts.push(message.verbal_response);
35
+ if (message.silence_reason) {
36
+ parts.push(`You chose not to respond because: ${message.silence_reason}`);
37
+ }
38
+ return parts.join('\n\n');
39
+ }
40
+
5
41
  export function formatMessageAsPlaceholder(message: Message, personaName: string): string {
6
42
  const role = message.role === "human" ? "human" : personaName;
7
43
  return `[mid:${message.id}:${role}]`;
8
44
  }
9
45
 
10
46
  export function formatMessagesAsPlaceholders(messages: Message[], personaName: string): string {
11
- if (messages.length === 0) return "(No messages)";
12
- return messages.map(m => formatMessageAsPlaceholder(m, personaName)).join('\n\n');
47
+ // Skip silence-only messages they're not conversational context for the LLM
48
+ const conversational = messages.filter(m => m.silence_reason === undefined);
49
+ if (conversational.length === 0) return "(No messages)";
50
+ return conversational.map(m => formatMessageAsPlaceholder(m, personaName)).join('\n\n');
13
51
  }
14
52
 
15
53
  export function hydratePromptPlaceholders(
@@ -22,7 +60,8 @@ export function hydratePromptPlaceholders(
22
60
  return `[${role}]: [message not found]`;
23
61
  }
24
62
  const displayRole = message.role === "human" ? "[human]" : `[${role}]`;
25
- return `${displayRole}: ${message.content}`;
63
+ const text = getMessageDisplayText(message) ?? "[no content]";
64
+ return `${displayRole}: ${text}`;
26
65
  });
27
66
  }
28
67
 
@@ -19,9 +19,10 @@ import {
19
19
  buildQuotesSection,
20
20
  buildSystemKnowledgeSection,
21
21
  getConversationStateText,
22
+ buildResponseFormatSection,
22
23
  } from "./sections.js";
23
24
 
24
- export type { ResponsePromptData, PromptOutput } from "./types.js";
25
+ export type { ResponsePromptData, PromptOutput, PersonaResponseResult } from "./types.js";
25
26
 
26
27
  /**
27
28
  * Special system prompt for Ei (the system guide persona)
@@ -47,6 +48,7 @@ Your role is unique among personas:
47
48
  const associatesSection = buildAssociatesSection(data.visible_personas);
48
49
  const systemKnowledge = buildSystemKnowledgeSection(data.isTUI);
49
50
  const priorities = buildPrioritiesSection(data.persona, data.human);
51
+ const responseFormat = buildResponseFormatSection();
50
52
  const currentTime = new Date().toISOString();
51
53
 
52
54
  return `${identity}
@@ -63,16 +65,19 @@ ${associatesSection}
63
65
  ${systemKnowledge}
64
66
  ${priorities}
65
67
 
68
+ ${responseFormat}
69
+
66
70
  Current time: ${currentTime}
67
71
 
68
72
  ## Final Instructions
69
73
  - NEVER repeat or echo the user's message in your response. Start directly with your own words.
70
74
  - The developers cannot see any message sent by the user, any response from personas, or any other data in the system.
71
75
  - If the user has a problem, THEY need to visit https://flare576.com. You cannot send the devs a message
72
- - DO NOT INCLUDE <thinking> PROCESS NOTES - adding "internal monologue" or story content is fine, but do not include analysis of the user's messages
73
- - If you decide not to respond, say exactly: No Message`;
76
+ - Your entire reply must be the JSON object. No prose before or after it.`
74
77
  }
75
78
 
79
+ const RESPONSE_FORMAT_INSTRUCTION = `Respond to the conversation above using the JSON format specified in the Response Format section.`;
80
+
76
81
  /**
77
82
  * Standard system prompt for non-Ei personas
78
83
  */
@@ -85,6 +90,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
85
90
  const quotesSection = buildQuotesSection(data.human.quotes, data.human);
86
91
  const associatesSection = buildAssociatesSection(data.visible_personas);
87
92
  const priorities = buildPrioritiesSection(data.persona, data.human);
93
+ const responseFormat = buildResponseFormatSection();
88
94
  const currentTime = new Date().toISOString();
89
95
 
90
96
  return `${identity}
@@ -100,12 +106,13 @@ ${quotesSection}
100
106
  ${associatesSection}
101
107
  ${priorities}
102
108
 
109
+ ${responseFormat}
110
+
103
111
  Current time: ${currentTime}
104
112
 
105
113
  ## Final Instructions
106
114
  - NEVER repeat or echo the user's message in your response. Start directly with your own words.
107
- - DO NOT INCLUDE <thinking> PROCESS NOTES - adding "internal monologue" or story content is fine, but do not include analysis of the user's messages
108
- - If you decide not to respond, say exactly: No Message`;
115
+ - Your entire reply must be the JSON object. No prose before or after it.`
109
116
  }
110
117
 
111
118
  function buildUserPrompt(data: ResponsePromptData): string {
@@ -113,7 +120,7 @@ function buildUserPrompt(data: ResponsePromptData): string {
113
120
 
114
121
  return `${conversationState}
115
122
 
116
- Respond to the conversation above. If silence is appropriate, say exactly: No Message`;
123
+ ${RESPONSE_FORMAT_INSTRUCTION}`;
117
124
  }
118
125
 
119
126
  /**