ei-tui 0.1.21 → 0.1.23
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 +1 -1
- package/src/core/handlers/heartbeat.ts +3 -0
- package/src/core/handlers/human-matching.ts +29 -3
- package/src/core/llm-client.ts +1 -1
- package/src/core/processor.ts +37 -7
- package/src/core/state/personas.ts +10 -0
- package/src/core/state-manager.ts +6 -0
- package/src/core/types/entities.ts +10 -0
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/llm.ts +3 -0
- package/src/prompts/message-utils.ts +7 -1
- package/src/prompts/response/index.ts +3 -3
- package/src/prompts/response/sections.ts +3 -2
- package/tui/src/context/ei.tsx +13 -0
- package/tui/src/context/keyboard.tsx +2 -1
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ContextStatus,
|
|
3
|
+
LLMNextStep,
|
|
3
4
|
ValidationLevel,
|
|
4
5
|
type LLMResponse,
|
|
5
6
|
type Message,
|
|
@@ -24,6 +25,7 @@ export function handleHeartbeatCheck(response: LLMResponse, state: StateManager)
|
|
|
24
25
|
|
|
25
26
|
const now = new Date().toISOString();
|
|
26
27
|
state.persona_update(personaId, { last_heartbeat: now });
|
|
28
|
+
state.queue_clearPersonaResponses(personaId, LLMNextStep.HandleHeartbeatCheck);
|
|
27
29
|
|
|
28
30
|
if (!result.should_respond) {
|
|
29
31
|
console.log(`[handleHeartbeatCheck] ${personaDisplayName} chose not to reach out`);
|
|
@@ -52,6 +54,7 @@ export function handleEiHeartbeat(response: LLMResponse, state: StateManager): v
|
|
|
52
54
|
}
|
|
53
55
|
const now = new Date().toISOString();
|
|
54
56
|
state.persona_update("ei", { last_heartbeat: now });
|
|
57
|
+
state.queue_clearPersonaResponses("ei", LLMNextStep.HandleEiHeartbeat);
|
|
55
58
|
if (!result.should_respond || !result.id) {
|
|
56
59
|
console.log("[handleEiHeartbeat] Ei chose not to reach out");
|
|
57
60
|
return;
|
|
@@ -27,9 +27,27 @@ export function handleHumanItemMatch(response: LLMResponse, state: StateManager)
|
|
|
27
27
|
const candidateType = response.request.data.candidateType as DataItemType;
|
|
28
28
|
const personaId = response.request.data.personaId as string;
|
|
29
29
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
30
|
-
const
|
|
30
|
+
const messageIdsToMark = response.request.data.message_ids_to_mark as string[] | undefined;
|
|
31
31
|
const allMessages = state.messages_get(personaId);
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
let messages_context: Message[];
|
|
34
|
+
let messages_analyze: Message[];
|
|
35
|
+
|
|
36
|
+
if (messageIdsToMark && messageIdsToMark.length > 0) {
|
|
37
|
+
const messageIdSet = new Set(messageIdsToMark);
|
|
38
|
+
messages_analyze = allMessages.filter(m => messageIdSet.has(m.id));
|
|
39
|
+
const analyzeStartTime = messages_analyze[0]?.timestamp ?? '9999';
|
|
40
|
+
messages_context = allMessages.filter(m =>
|
|
41
|
+
!messageIdSet.has(m.id) && new Date(m.timestamp).getTime() < new Date(analyzeStartTime).getTime()
|
|
42
|
+
);
|
|
43
|
+
} else {
|
|
44
|
+
// Fallback to existing behavior
|
|
45
|
+
const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
|
|
46
|
+
const split = splitMessagesByTimestamp(allMessages, analyzeFrom);
|
|
47
|
+
messages_context = split.messages_context;
|
|
48
|
+
messages_analyze = split.messages_analyze;
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
const context: ExtractionContext & { itemName: string; itemValue: string; itemCategory?: string } = {
|
|
34
52
|
personaId,
|
|
35
53
|
personaDisplayName,
|
|
@@ -212,6 +230,12 @@ export async function handleHumanItemUpdate(response: LLMResponse, state: StateM
|
|
|
212
230
|
console.log(`[handleHumanItemUpdate] ${isNewItem ? "Created" : "Updated"} ${candidateType} "${result.name}"`);
|
|
213
231
|
}
|
|
214
232
|
|
|
233
|
+
function normalizeQuotes(text: string): string {
|
|
234
|
+
return text
|
|
235
|
+
.replace(/[\u201C\u201D]/g, '"') // Curly double quotes to straight
|
|
236
|
+
.replace(/[\u2018\u2019]/g, "'"); // Curly single quotes to straight
|
|
237
|
+
}
|
|
238
|
+
|
|
215
239
|
async function validateAndStoreQuotes(
|
|
216
240
|
candidates: Array<{ text: string; reason: string }> | undefined,
|
|
217
241
|
messages: Message[],
|
|
@@ -226,7 +250,9 @@ async function validateAndStoreQuotes(
|
|
|
226
250
|
let found = false;
|
|
227
251
|
for (const message of messages) {
|
|
228
252
|
const msgText = getMessageText(message);
|
|
229
|
-
const
|
|
253
|
+
const normalizedMsg = normalizeQuotes(msgText);
|
|
254
|
+
const normalizedQuote = normalizeQuotes(candidate.text);
|
|
255
|
+
const start = normalizedMsg.indexOf(normalizedQuote);
|
|
230
256
|
if (start !== -1) {
|
|
231
257
|
const end = start + candidate.text.length;
|
|
232
258
|
|
package/src/core/llm-client.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function resolveModel(modelSpec?: string, accounts?: ProviderAccount[]):
|
|
|
60
60
|
if (accounts) {
|
|
61
61
|
const searchName = provider || modelSpec; // If no ":", the whole spec might be an account name
|
|
62
62
|
const matchingAccount = accounts.find(
|
|
63
|
-
(acc) => acc.name.toLowerCase() === searchName.toLowerCase() && acc.enabled
|
|
63
|
+
(acc) => acc.name.toLowerCase() === searchName.toLowerCase() && acc.enabled && acc.type === "llm"
|
|
64
64
|
);
|
|
65
65
|
if (matchingAccount) {
|
|
66
66
|
// If bare account name was used, get model from account's default_model
|
package/src/core/processor.ts
CHANGED
|
@@ -589,22 +589,42 @@ export class Processor {
|
|
|
589
589
|
console.log(`[Processor ${this.instanceId}] stopped`);
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
592
|
+
getStateManager(): StateManager {
|
|
593
|
+
return this.stateManager;
|
|
594
|
+
}
|
|
595
595
|
|
|
596
|
+
async pause(): Promise<void> {
|
|
597
|
+
console.log(`[Processor ${this.instanceId}] pause() called`);
|
|
598
|
+
this.running = false;
|
|
596
599
|
this.queueProcessor.abort();
|
|
597
600
|
this.importAbortController.abort();
|
|
598
601
|
if (this.openCodeImportInProgress) {
|
|
599
|
-
console.log(`[Processor ${this.instanceId}]
|
|
602
|
+
console.log(`[Processor ${this.instanceId}] Clearing openCodeImportInProgress flag`);
|
|
600
603
|
this.openCodeImportInProgress = false;
|
|
601
604
|
}
|
|
602
605
|
if (this.claudeCodeImportInProgress) {
|
|
603
|
-
console.log(`[Processor ${this.instanceId}]
|
|
606
|
+
console.log(`[Processor ${this.instanceId}] Clearing claudeCodeImportInProgress flag`);
|
|
604
607
|
this.claudeCodeImportInProgress = false;
|
|
605
608
|
}
|
|
606
|
-
|
|
607
609
|
await this.stateManager.flush();
|
|
610
|
+
console.log(`[Processor ${this.instanceId}] pause() complete (main loop stopped, state flushed)`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async resume(): Promise<void> {
|
|
614
|
+
console.log(`[Processor ${this.instanceId}] resume() called`);
|
|
615
|
+
if (this.stopped) {
|
|
616
|
+
throw new Error(`Cannot resume a stopped processor (instanceId: ${this.instanceId})`);
|
|
617
|
+
}
|
|
618
|
+
this.importAbortController = new AbortController();
|
|
619
|
+
this.running = true;
|
|
620
|
+
this.runLoop();
|
|
621
|
+
console.log(`[Processor ${this.instanceId}] resume() complete (main loop restarted)`);
|
|
622
|
+
}
|
|
623
|
+
async saveAndExit(): Promise<{ success: boolean; error?: string }> {
|
|
624
|
+
console.log(`[Processor ${this.instanceId}] saveAndExit() called`);
|
|
625
|
+
this.interface.onSaveAndExitStart?.();
|
|
626
|
+
|
|
627
|
+
await this.pause();
|
|
608
628
|
|
|
609
629
|
const human = this.stateManager.getHuman();
|
|
610
630
|
const hasSyncCreds =
|
|
@@ -616,7 +636,7 @@ export class Processor {
|
|
|
616
636
|
|
|
617
637
|
if (!result.success) {
|
|
618
638
|
console.log(`[Processor ${this.instanceId}] Remote sync failed: ${result.error}`);
|
|
619
|
-
this.
|
|
639
|
+
await this.resume();
|
|
620
640
|
this.interface.onSaveAndExitFinish?.();
|
|
621
641
|
return { success: false, error: result.error };
|
|
622
642
|
}
|
|
@@ -1208,6 +1228,16 @@ const toolNextSteps = new Set([
|
|
|
1208
1228
|
return removed;
|
|
1209
1229
|
}
|
|
1210
1230
|
|
|
1231
|
+
async addMessageOnly(personaId: string, message: Message): Promise<void> {
|
|
1232
|
+
this.stateManager.messages_append(personaId, message);
|
|
1233
|
+
this.interface.onMessageAdded?.(personaId);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async updateMessage(personaId: string, messageId: string, updates: Partial<Message>): Promise<void> {
|
|
1237
|
+
this.stateManager.messages_update(personaId, messageId, updates);
|
|
1238
|
+
this.interface.onMessageAdded?.(personaId);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1211
1241
|
// ==========================================================================
|
|
1212
1242
|
// HUMAN DATA API
|
|
1213
1243
|
// ==========================================================================
|
|
@@ -126,6 +126,16 @@ export class PersonaState {
|
|
|
126
126
|
data.entity.last_updated = new Date().toISOString();
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
messages_update(personaId: string, messageId: string, updates: Partial<Message>): boolean {
|
|
130
|
+
const data = this.personas.get(personaId);
|
|
131
|
+
if (!data) return false;
|
|
132
|
+
const msg = data.messages.find((m) => m.id === messageId);
|
|
133
|
+
if (!msg) return false;
|
|
134
|
+
Object.assign(msg, updates);
|
|
135
|
+
data.entity.last_updated = new Date().toISOString();
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
messages_sort(personaId: string): void {
|
|
130
140
|
const data = this.personas.get(personaId);
|
|
131
141
|
if (!data) return;
|
|
@@ -247,6 +247,12 @@ export class StateManager {
|
|
|
247
247
|
this.scheduleSave();
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
messages_update(personaId: string, messageId: string, updates: Partial<Message>): boolean {
|
|
251
|
+
const result = this.personaState.messages_update(personaId, messageId, updates);
|
|
252
|
+
this.scheduleSave();
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
250
256
|
messages_sort(personaId: string): void {
|
|
251
257
|
this.personaState.messages_sort(personaId);
|
|
252
258
|
this.scheduleSave();
|
|
@@ -60,6 +60,9 @@ export interface ProviderAccount {
|
|
|
60
60
|
|
|
61
61
|
// Provider-specific extras (e.g., OpenRouter needs HTTP-Referer, X-Title)
|
|
62
62
|
extra_headers?: Record<string, string>;
|
|
63
|
+
|
|
64
|
+
// Image provider-specific
|
|
65
|
+
workflow_json?: Record<string, any>; // ComfyUI workflow template (image providers only)
|
|
63
66
|
|
|
64
67
|
// Metadata
|
|
65
68
|
enabled?: boolean; // Default: true
|
|
@@ -134,6 +137,13 @@ export interface PersonaCreationInput {
|
|
|
134
137
|
tools?: string[]; // IDs of ToolDefinitions to assign at creation time
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
// ComfyUI z-image-turbo basic string
|
|
141
|
+
// Prompt - "57:27"."inputs"."text"
|
|
142
|
+
// Width - "57:13"."inputs"."width"
|
|
143
|
+
// Height - "57:13"."inputs"."height"
|
|
144
|
+
// Steps - "57:3"."inputs"."steps"
|
|
145
|
+
// Cfg - "57:3"."inputs"."cfg"
|
|
146
|
+
export const COMFY_PROMPT_TEMPLATE = {"9":{"inputs":{"filename_prefix":"z-image-turbo","images":["57:8",0]},"class_type":"SaveImage","_meta":{"title":"Save Image"}},"57:30":{"inputs":{"clip_name":"qwen_3_4b.safetensors","type":"lumina2","device":"default"},"class_type":"CLIPLoader","_meta":{"title":"Load CLIP"}},"57:29":{"inputs":{"vae_name":"ae.safetensors"},"class_type":"VAELoader","_meta":{"title":"Load VAE"}},"57:33":{"inputs":{"conditioning":["57:27",0]},"class_type":"ConditioningZeroOut","_meta":{"title":"ConditioningZeroOut"}},"57:8":{"inputs":{"samples":["57:3",0],"vae":["57:29",0]},"class_type":"VAEDecode","_meta":{"title":"VAE Decode"}},"57:28":{"inputs":{"unet_name":"z_image_turbo_bf16.safetensors","weight_dtype":"default"},"class_type":"UNETLoader","_meta":{"title":"Load Diffusion Model"}},"57:27":{"inputs":{"text":"This is a test prompt","clip":["57:30",0]},"class_type":"CLIPTextEncode","_meta":{"title":"CLIP Text Encode (Prompt)"}},"57:13":{"inputs":{"width":768,"height":768,"batch_size":1},"class_type":"EmptySD3LatentImage","_meta":{"title":"EmptySD3LatentImage"}},"57:11":{"inputs":{"shift":3,"model":["57:28",0]},"class_type":"ModelSamplingAuraFlow","_meta":{"title":"ModelSamplingAuraFlow"}},"57:3":{"inputs":{"seed":407776369182481,"steps":8,"cfg":1,"sampler_name":"res_multistep","scheduler":"simple","denoise":1,"model":["57:11",0],"positive":["57:27",0],"negative":["57:33",0],"latent_image":["57:13",0]},"class_type":"KSampler","_meta":{"title":"KSampler"}}};
|
|
137
147
|
// Message pruning thresholds (shared by ceremony and import)
|
|
138
148
|
export const MESSAGE_MIN_COUNT = 200;
|
|
139
149
|
export const MESSAGE_MAX_AGE_DAYS = 14;
|
package/src/core/types/enums.ts
CHANGED
package/src/core/types/llm.ts
CHANGED
|
@@ -21,6 +21,9 @@ export interface Message {
|
|
|
21
21
|
r?: boolean; // tRait extraction completed
|
|
22
22
|
p?: boolean; // Person extraction completed
|
|
23
23
|
o?: boolean; // tOpic extraction completed
|
|
24
|
+
// Image generation fields (web-only, ephemeral)
|
|
25
|
+
_synthesis?: boolean; // True if message was created by multi-message synthesis
|
|
26
|
+
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface ChatMessage {
|
|
@@ -31,7 +31,13 @@ export function getMessageDisplayText(message: Message): string | null {
|
|
|
31
31
|
export function buildChatMessageContent(message: Message): string {
|
|
32
32
|
const parts: string[] = [];
|
|
33
33
|
if (message.action_response) parts.push(`_${message.action_response}_`);
|
|
34
|
-
|
|
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);
|
|
40
|
+
}
|
|
35
41
|
if (message.silence_reason) {
|
|
36
42
|
parts.push(`You chose not to respond because: ${message.silence_reason}`);
|
|
37
43
|
}
|
|
@@ -29,7 +29,7 @@ export type { ResponsePromptData, PromptOutput, PersonaResponseResult } from "./
|
|
|
29
29
|
* Special system prompt for Ei (the system guide persona)
|
|
30
30
|
*/
|
|
31
31
|
function buildEiSystemPrompt(data: ResponsePromptData): string {
|
|
32
|
-
const identity = `You are Ei, the user's personal companion and system guide.
|
|
32
|
+
const identity = `You are Ei, the user's personal companion and system guide.
|
|
33
33
|
|
|
34
34
|
You are the central hub of this experience - a thoughtful AI who genuinely cares about the human's wellbeing and growth. You listen, remember, and help them reflect. You're curious about their life but never intrusive.
|
|
35
35
|
|
|
@@ -75,7 +75,7 @@ Current time: ${currentTime}
|
|
|
75
75
|
- NEVER repeat or echo the user's message in your response. Start directly with your own words.
|
|
76
76
|
- The developers cannot see any message sent by the user, any response from personas, or any other data in the system.
|
|
77
77
|
- If the user has a problem, THEY need to visit https://flare576.com. You cannot send the devs a message
|
|
78
|
-
-
|
|
78
|
+
- Format your response as specified in the Response Format section above.`
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
const RESPONSE_FORMAT_INSTRUCTION = `Respond to the conversation above using the JSON format specified in the Response Format section.`;
|
|
@@ -115,7 +115,7 @@ Current time: ${currentTime}
|
|
|
115
115
|
|
|
116
116
|
## Final Instructions
|
|
117
117
|
- NEVER repeat or echo the user's message in your response. Start directly with your own words.
|
|
118
|
-
-
|
|
118
|
+
- Format your response as specified in the Response Format section above.`
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function buildUserPrompt(data: ResponsePromptData): string {
|
|
@@ -20,7 +20,7 @@ export function buildIdentitySection(persona: ResponsePromptData["persona"]): st
|
|
|
20
20
|
|| persona.short_description
|
|
21
21
|
|| "a conversational companion";
|
|
22
22
|
|
|
23
|
-
return `You are ${persona.name}${aliasText}.
|
|
23
|
+
return `You are ${persona.name}${aliasText}.
|
|
24
24
|
|
|
25
25
|
${description}`;
|
|
26
26
|
}
|
|
@@ -449,7 +449,8 @@ Rules:
|
|
|
449
449
|
- \`action_response\` alone is valid — a smile, a shrug, or a thoughtful pause can speak volumes
|
|
450
450
|
- \`reason\` is only used when \`should_respond\` is false
|
|
451
451
|
- Do NOT include \`<thinking>\` blocks or analysis outside the JSON
|
|
452
|
-
- The JSON must be valid - use double quotes, no trailing commas
|
|
452
|
+
- The JSON must be valid - use double quotes, no trailing commas
|
|
453
|
+
- **Your entire reply should be the JSON object** — no prose, no preamble, no closing commentary`
|
|
453
454
|
}
|
|
454
455
|
|
|
455
456
|
// =============================================================================
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -109,6 +109,7 @@ export interface EiContextValue {
|
|
|
109
109
|
getToolList: () => ToolDefinition[];
|
|
110
110
|
updateToolProvider: (id: string, updates: Partial<Omit<ToolProvider, 'id' | 'created_at'>>) => Promise<boolean>;
|
|
111
111
|
updateTool: (id: string, updates: Partial<Omit<ToolDefinition, 'id' | 'created_at'>>) => Promise<boolean>;
|
|
112
|
+
cleanupTimers: () => void;
|
|
112
113
|
}
|
|
113
114
|
const EiContext = createContext<EiContextValue>();
|
|
114
115
|
|
|
@@ -143,6 +144,17 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
143
144
|
}, 5000);
|
|
144
145
|
};
|
|
145
146
|
|
|
147
|
+
const cleanupTimers = () => {
|
|
148
|
+
if (notificationTimer) {
|
|
149
|
+
clearTimeout(notificationTimer);
|
|
150
|
+
notificationTimer = null;
|
|
151
|
+
}
|
|
152
|
+
if (readTimer) {
|
|
153
|
+
clearTimeout(readTimer);
|
|
154
|
+
readTimer = null;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
146
158
|
const refreshPersonas = async () => {
|
|
147
159
|
if (!processor) return;
|
|
148
160
|
const list = await processor.getPersonaList();
|
|
@@ -658,6 +670,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
658
670
|
getToolList,
|
|
659
671
|
updateToolProvider,
|
|
660
672
|
updateTool,
|
|
673
|
+
cleanupTimers,
|
|
661
674
|
};
|
|
662
675
|
return (
|
|
663
676
|
<Switch>
|
|
@@ -33,7 +33,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
33
33
|
const [focusedPanel, setFocusedPanel] = createSignal<Panel>("input");
|
|
34
34
|
const [sidebarVisible, setSidebarVisible] = createSignal(true);
|
|
35
35
|
const renderer = useRenderer();
|
|
36
|
-
const { queueStatus, abortCurrentOperation, resumeQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification, messages, recallPendingMessages } = useEi();
|
|
36
|
+
const { queueStatus, abortCurrentOperation, resumeQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification, messages, recallPendingMessages, cleanupTimers } = useEi();
|
|
37
37
|
|
|
38
38
|
let messageScrollRef: ScrollBoxRenderable | null = null;
|
|
39
39
|
let textareaRef: TextareaRenderable | null = null;
|
|
@@ -56,6 +56,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
56
56
|
const toggleSidebar = () => setSidebarVisible(!sidebarVisible());
|
|
57
57
|
|
|
58
58
|
const exitApp = async () => {
|
|
59
|
+
cleanupTimers();
|
|
59
60
|
showNotification("Saving and syncing...", "info");
|
|
60
61
|
const result = await saveAndExit();
|
|
61
62
|
if (!result.success) {
|