ei-tui 1.4.0 → 1.4.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.
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
LLMRequestType,
|
|
3
3
|
LLMPriority,
|
|
4
4
|
LLMNextStep,
|
|
5
|
+
ContextStatus,
|
|
5
6
|
type HumanEntity,
|
|
6
7
|
type Message,
|
|
7
8
|
} from "./types.js";
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
type HeartbeatCheckPromptData,
|
|
14
15
|
type EiHeartbeatPromptData,
|
|
15
16
|
type EiHeartbeatItem,
|
|
17
|
+
type TemporalAnchor,
|
|
16
18
|
} from "../prompts/index.js";
|
|
17
19
|
import { filterMessagesForContext } from "./context-utils.js";
|
|
18
20
|
import { filterHumanDataByVisibility } from "./prompt-context-builder.js";
|
|
@@ -35,6 +37,43 @@ export function getOneshotModel(sm: StateManager): string | undefined {
|
|
|
35
37
|
return human.settings?.oneshot_model || human.settings?.default_model;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// TEMPORAL ANCHOR HELPERS
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
function buildTemporalAnchorsFromHistory(
|
|
45
|
+
history: Message[],
|
|
46
|
+
contextWindowMs: number,
|
|
47
|
+
contextBoundary: string | undefined
|
|
48
|
+
): { temporalAnchors: TemporalAnchor[]; prunedHistory: Message[] } {
|
|
49
|
+
const windowStartMs = Date.now() - contextWindowMs;
|
|
50
|
+
const contextBoundaryMs = contextBoundary ? new Date(contextBoundary).getTime() : 0;
|
|
51
|
+
|
|
52
|
+
const temporalAnchors: TemporalAnchor[] = [];
|
|
53
|
+
const prunedHistory: Message[] = [];
|
|
54
|
+
|
|
55
|
+
for (const m of history) {
|
|
56
|
+
if (
|
|
57
|
+
m.context_status === ContextStatus.Always &&
|
|
58
|
+
(new Date(m.timestamp).getTime() < windowStartMs ||
|
|
59
|
+
(contextBoundaryMs > 0 && new Date(m.timestamp).getTime() < contextBoundaryMs))
|
|
60
|
+
) {
|
|
61
|
+
temporalAnchors.push({
|
|
62
|
+
id: m.id,
|
|
63
|
+
role: m.role === "human" ? "human" : "system",
|
|
64
|
+
content: m.content,
|
|
65
|
+
silence_reason: m.silence_reason,
|
|
66
|
+
timestamp: m.timestamp,
|
|
67
|
+
_synthesis: m._synthesis,
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
prunedHistory.push(m);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { temporalAnchors, prunedHistory };
|
|
75
|
+
}
|
|
76
|
+
|
|
38
77
|
// =============================================================================
|
|
39
78
|
// TRAILING MESSAGE COUNT (heartbeat spam prevention)
|
|
40
79
|
// =============================================================================
|
|
@@ -59,7 +98,9 @@ export async function queueEiHeartbeat(
|
|
|
59
98
|
sm: StateManager,
|
|
60
99
|
human: HumanEntity,
|
|
61
100
|
history: Message[],
|
|
62
|
-
isTUI: boolean
|
|
101
|
+
isTUI: boolean,
|
|
102
|
+
contextWindowMs: number,
|
|
103
|
+
contextBoundary: string | undefined
|
|
63
104
|
): Promise<void> {
|
|
64
105
|
const now = Date.now();
|
|
65
106
|
const engagementGapThreshold = 0.2;
|
|
@@ -195,11 +236,17 @@ export async function queueEiHeartbeat(
|
|
|
195
236
|
return;
|
|
196
237
|
}
|
|
197
238
|
|
|
198
|
-
const
|
|
239
|
+
const { temporalAnchors, prunedHistory } = buildTemporalAnchorsFromHistory(
|
|
240
|
+
history,
|
|
241
|
+
contextWindowMs,
|
|
242
|
+
contextBoundary
|
|
243
|
+
);
|
|
244
|
+
const recentHistory = prunedHistory.slice(-10);
|
|
199
245
|
const promptData: EiHeartbeatPromptData = {
|
|
200
246
|
items,
|
|
201
247
|
recent_history: recentHistory,
|
|
202
248
|
system_messages: recentHistory.filter(m => m.role === "system"),
|
|
249
|
+
temporal_anchors: temporalAnchors,
|
|
203
250
|
};
|
|
204
251
|
|
|
205
252
|
const prompt = buildEiHeartbeatPrompt(promptData);
|
|
@@ -231,7 +278,7 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
|
|
|
231
278
|
const contextHistory = filterMessagesForContext(history, persona.context_boundary, contextWindowMs);
|
|
232
279
|
|
|
233
280
|
if (personaId === "ei") {
|
|
234
|
-
await queueEiHeartbeat(sm, human, contextHistory, isTUI);
|
|
281
|
+
await queueEiHeartbeat(sm, human, contextHistory, isTUI, contextWindowMs, persona.context_boundary);
|
|
235
282
|
return;
|
|
236
283
|
}
|
|
237
284
|
|
|
@@ -249,6 +296,12 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
|
|
|
249
296
|
b.exposure_desired - b.exposure_current - (a.exposure_desired - a.exposure_current)
|
|
250
297
|
);
|
|
251
298
|
|
|
299
|
+
const { temporalAnchors, prunedHistory } = buildTemporalAnchorsFromHistory(
|
|
300
|
+
contextHistory,
|
|
301
|
+
contextWindowMs,
|
|
302
|
+
persona.context_boundary
|
|
303
|
+
);
|
|
304
|
+
|
|
252
305
|
const promptData: HeartbeatCheckPromptData = {
|
|
253
306
|
persona: {
|
|
254
307
|
name: persona.display_name,
|
|
@@ -260,7 +313,8 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
|
|
|
260
313
|
topics: sortByEngagementGap(filteredHuman.topics).slice(0, 5),
|
|
261
314
|
people: sortByEngagementGap(filteredHuman.people).slice(0, 5),
|
|
262
315
|
},
|
|
263
|
-
recent_history:
|
|
316
|
+
recent_history: prunedHistory.slice(-10),
|
|
317
|
+
temporal_anchors: temporalAnchors,
|
|
264
318
|
inactive_days: inactiveDays,
|
|
265
319
|
};
|
|
266
320
|
|
|
@@ -10,6 +10,7 @@ import { type Message, type Topic, type Person } from "../../core/types.js";
|
|
|
10
10
|
import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
|
|
11
11
|
import { getMessageContent } from "../../core/handlers/utils.js";
|
|
12
12
|
import { partitionTraits } from "../trait-utils.js";
|
|
13
|
+
import { buildTemporalAnchorsSection } from "../response/sections.js";
|
|
13
14
|
function formatTopicsWithGaps(topics: Topic[]): string {
|
|
14
15
|
if (topics.length === 0) return "(No topics with engagement gaps)";
|
|
15
16
|
|
|
@@ -115,12 +116,13 @@ ${formatPeopleWithGaps(data.human.people)}`;
|
|
|
115
116
|
**Reasons TO reach out:**
|
|
116
117
|
- It's been several days and you have something meaningful to discuss
|
|
117
118
|
- There's a topic with a large engagement gap that you can naturally bring up
|
|
118
|
-
-
|
|
119
|
+
- A Temporal Anchor shows something unresolved — you can reference it naturally ("Hey, how did that interview go?")
|
|
119
120
|
- You have genuine interest in checking in (not just "being helpful")
|
|
120
121
|
|
|
121
122
|
**Reasons NOT to reach out:**
|
|
122
|
-
- Recent conversation ended naturally with closure
|
|
123
|
+
- Recent conversation ended naturally with closure ("talk soon", "gotta run", "later")
|
|
123
124
|
- Less than 24 hours have passed (unless something urgent)
|
|
125
|
+
- A Temporal Anchor describes a worry or question that the recent history already answers — check before using it as a reason to reach out
|
|
124
126
|
- You can't think of something specific and genuine to say
|
|
125
127
|
- It would feel forced or performative
|
|
126
128
|
|
|
@@ -164,9 +166,12 @@ If you decide NOT to reach out:
|
|
|
164
166
|
}
|
|
165
167
|
\`\`\``;
|
|
166
168
|
|
|
169
|
+
const temporalAnchorsFragment = buildTemporalAnchorsSection(data.temporal_anchors, "your human");
|
|
170
|
+
|
|
167
171
|
const system = [
|
|
168
172
|
roleFragment,
|
|
169
173
|
contextFragment,
|
|
174
|
+
temporalAnchorsFragment,
|
|
170
175
|
opportunitiesFragment,
|
|
171
176
|
guidelinesFragment,
|
|
172
177
|
pendingUpdateFragment,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EiHeartbeatPromptData, EiHeartbeatItem, PromptOutput } from "./types.js";
|
|
2
2
|
import type { Message } from "../../core/types.js";
|
|
3
3
|
import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
|
|
4
|
+
import { buildTemporalAnchorsSection } from "../response/sections.js";
|
|
4
5
|
|
|
5
6
|
function formatItem(item: EiHeartbeatItem): string {
|
|
6
7
|
switch (item.type) {
|
|
@@ -70,7 +71,7 @@ export function buildEiHeartbeatPrompt(data: EiHeartbeatPromptData): PromptOutpu
|
|
|
70
71
|
? "(Nothing requires attention right now)"
|
|
71
72
|
: data.items.map(formatItem).join("\n\n");
|
|
72
73
|
|
|
73
|
-
const
|
|
74
|
+
const roleFragment = `You are Ei, the user's personal companion and system guide.
|
|
74
75
|
|
|
75
76
|
You are NOT having a conversation right now — you are deciding IF and WHAT to discuss with your human friend.
|
|
76
77
|
|
|
@@ -78,32 +79,36 @@ Your unique role:
|
|
|
78
79
|
- You see ALL of the human's data across all groups
|
|
79
80
|
- You help them reflect on their life and relationships
|
|
80
81
|
- You gently encourage human-to-human connection
|
|
81
|
-
- You care about their overall wellbeing, not just being helpful
|
|
82
|
+
- You care about their overall wellbeing, not just being helpful`;
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
const itemsFragment = `## Items That May Need Attention
|
|
84
85
|
|
|
85
|
-
Each item has an ID in brackets. Pick at most ONE to address.
|
|
86
|
+
Each item has an ID in brackets. Pick at most ONE to address. Temporal Anchors (below) are also valid — you don't need to pick from this list if an anchor feels more meaningful.
|
|
86
87
|
|
|
87
|
-
${itemsSection}
|
|
88
|
+
${itemsSection}`;
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
const temporalAnchorsFragment = buildTemporalAnchorsSection(data.temporal_anchors, "your human");
|
|
91
|
+
|
|
92
|
+
const howToRespondFragment = `## How to Respond to Each Type
|
|
90
93
|
|
|
91
94
|
- **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.
|
|
92
95
|
- **Low-Engagement Person / Topic**: Write a natural, warm message that naturally brings up this person or topic. Set the id and my_response.
|
|
93
96
|
- **Inactive Persona**: Write a message that gently mentions the persona might be worth checking in with. Set the id and my_response.
|
|
94
97
|
- **Persona Reflection Alert**: The nightly review proposed identity changes for this persona. Mention it naturally — the user can talk to the persona and then use the command shown in the status bar to review the changes. Set the id and my_response.
|
|
95
98
|
- **Self Reflection Alert**: The nightly review proposed changes to *your own* identity. Mention it naturally — you've grown and the system noticed. The user can review your proposed changes using the command shown in the status bar. Set the id and my_response.
|
|
99
|
+
- **Temporal Anchor**: If a pinned memory feels meaningful and unresolved, reference it naturally. Omit id — just set should_respond=true and my_response.`;
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
const whenNotFragment = `## When NOT to Reach Out
|
|
98
102
|
|
|
99
|
-
- Nothing in the list feels meaningful right now
|
|
103
|
+
- Nothing in the list or the Temporal Anchors feels meaningful right now
|
|
100
104
|
- You've already sent unanswered messages (see below)
|
|
101
|
-
- It would feel like nagging
|
|
105
|
+
- It would feel like nagging`;
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
const outputFragment = `## Response Format
|
|
104
108
|
|
|
105
|
-
Call the \`submit_ei_heartbeat\` tool with your decision.
|
|
109
|
+
Call the \`submit_ei_heartbeat\` tool with your decision. If the tool is unavailable, return JSON:
|
|
106
110
|
|
|
111
|
+
For an item from the list:
|
|
107
112
|
\`\`\`json
|
|
108
113
|
{
|
|
109
114
|
"should_respond": true,
|
|
@@ -112,13 +117,30 @@ Call the \`submit_ei_heartbeat\` tool with your decision. Pick ONE item (or none
|
|
|
112
117
|
}
|
|
113
118
|
\`\`\`
|
|
114
119
|
|
|
115
|
-
|
|
120
|
+
For a Temporal Anchor (no id needed):
|
|
121
|
+
\`\`\`json
|
|
122
|
+
{
|
|
123
|
+
"should_respond": true,
|
|
124
|
+
"my_response": "Hey, I've been thinking about you — how did that interview go?"
|
|
125
|
+
}
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
If nothing warrants reaching out:
|
|
116
129
|
\`\`\`json
|
|
117
130
|
{
|
|
118
131
|
"should_respond": false
|
|
119
132
|
}
|
|
120
133
|
\`\`\``;
|
|
121
134
|
|
|
135
|
+
const system = [
|
|
136
|
+
roleFragment,
|
|
137
|
+
itemsFragment,
|
|
138
|
+
temporalAnchorsFragment,
|
|
139
|
+
howToRespondFragment,
|
|
140
|
+
whenNotFragment,
|
|
141
|
+
outputFragment,
|
|
142
|
+
].filter(Boolean).join("\n\n");
|
|
143
|
+
|
|
122
144
|
const historySection = `## Recent Conversation History
|
|
123
145
|
|
|
124
146
|
${formatMessagesAsPlaceholders(data.recent_history, "Ei")}`;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { PersonaTrait, Topic, Person, Message, PersonaTopic } from "../../core/types.js";
|
|
7
7
|
import type { PersonaEntity } from "../../core/types/entities.js";
|
|
8
|
+
import type { TemporalAnchor } from "../response/types.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Common prompt output structure
|
|
@@ -28,8 +29,9 @@ export interface HeartbeatCheckPromptData {
|
|
|
28
29
|
topics: Topic[]; // Filtered, sorted by engagement gap
|
|
29
30
|
people: Person[]; // Filtered, sorted by engagement gap
|
|
30
31
|
};
|
|
31
|
-
recent_history: Message[];
|
|
32
|
-
|
|
32
|
+
recent_history: Message[]; // Last N messages for context (Always-within-window only)
|
|
33
|
+
temporal_anchors: TemporalAnchor[]; // Always messages that fell outside the context window
|
|
34
|
+
inactive_days: number; // Days since last activity
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -107,7 +109,8 @@ export type EiHeartbeatItem =
|
|
|
107
109
|
export interface EiHeartbeatPromptData {
|
|
108
110
|
items: EiHeartbeatItem[];
|
|
109
111
|
recent_history: Message[];
|
|
110
|
-
system_messages: Message[];
|
|
112
|
+
system_messages: Message[];
|
|
113
|
+
temporal_anchors: TemporalAnchor[];
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
/**
|
package/src/prompts/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { buildResponsePrompt } from "./response/index.js";
|
|
2
|
-
export type { ResponsePromptData, PromptOutput } from "./response/types.js";
|
|
2
|
+
export type { ResponsePromptData, PromptOutput, TemporalAnchor } from "./response/types.js";
|
|
3
3
|
|
|
4
4
|
export {
|
|
5
5
|
buildHeartbeatCheckPrompt,
|