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.
- package/README.md +36 -35
- package/package.json +6 -2
- package/src/README.md +85 -1
- package/src/cli/README.md +30 -20
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +195 -172
- package/src/core/orchestrators/ceremony.ts +4 -4
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/processor.ts +245 -77
- package/src/core/queue-processor.ts +3 -26
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/personas.ts +13 -1
- package/src/core/state/queue.ts +80 -23
- package/src/core/state-manager.ts +36 -10
- package/src/core/types.ts +23 -11
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +118 -691
- package/src/prompts/heartbeat/check.ts +27 -13
- package/src/prompts/heartbeat/ei.ts +65 -136
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/human/item-update.ts +20 -8
- package/src/prompts/index.ts +2 -5
- package/src/prompts/message-utils.ts +42 -3
- package/src/prompts/response/index.ts +13 -6
- package/src/prompts/response/sections.ts +65 -12
- package/src/prompts/response/types.ts +10 -0
- package/tui/README.md +89 -4
- package/tui/src/commands/dlq.ts +75 -0
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/commands/queue.ts +77 -0
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/MessageList.tsx +12 -2
- package/tui/src/components/PromptInput.tsx +118 -30
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/components/StatusBar.tsx +12 -5
- package/tui/src/context/ei.tsx +43 -3
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +81 -11
- package/src/prompts/validation/ei.ts +0 -93
- package/src/prompts/validation/index.ts +0 -6
- 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
|
|
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
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
62
|
+
## Items That May Need Attention
|
|
99
63
|
|
|
100
|
-
|
|
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
|
-
${
|
|
66
|
+
${itemsSection}
|
|
104
67
|
|
|
105
|
-
|
|
106
|
-
These are relationships they might want to nurture:
|
|
68
|
+
## How to Respond to Each Type
|
|
107
69
|
|
|
108
|
-
|
|
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
|
-
|
|
74
|
+
## When NOT to Reach Out
|
|
111
75
|
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
80
|
+
## Response Format
|
|
137
81
|
|
|
138
|
-
|
|
82
|
+
Pick ONE item (or none):
|
|
139
83
|
|
|
140
84
|
\`\`\`json
|
|
141
85
|
{
|
|
142
86
|
"should_respond": true,
|
|
143
|
-
"
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
|
|
249
|
-
|
|
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!
|
package/src/prompts/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
123
|
+
${RESPONSE_FORMAT_INSTRUCTION}`;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/**
|