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.
- package/package.json +1 -1
- package/src/core/constants/built-in-identifier-types.ts +24 -0
- package/src/core/embedding-service.ts +24 -1
- package/src/core/handlers/dedup.ts +34 -4
- package/src/core/handlers/heartbeat.ts +16 -0
- package/src/core/handlers/human-extraction.ts +201 -7
- package/src/core/handlers/human-matching.ts +71 -22
- package/src/core/handlers/index.ts +52 -14
- package/src/core/handlers/persona-generation.ts +2 -0
- package/src/core/handlers/persona-response.ts +37 -22
- package/src/core/handlers/persona-topics.ts +35 -271
- package/src/core/handlers/rewrite.ts +3 -0
- package/src/core/handlers/rooms.ts +41 -20
- package/src/core/handlers/utils.ts +10 -8
- package/src/core/heartbeat-manager.ts +60 -2
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +3 -2
- package/src/core/orchestrators/ceremony.ts +54 -144
- package/src/core/orchestrators/dedup-phase.ts +0 -199
- package/src/core/orchestrators/extraction-chunker.ts +8 -3
- package/src/core/orchestrators/human-extraction.ts +37 -85
- package/src/core/orchestrators/index.ts +4 -8
- package/src/core/orchestrators/person-migration.ts +55 -0
- package/src/core/orchestrators/persona-topics.ts +64 -89
- package/src/core/orchestrators/room-extraction.ts +34 -0
- package/src/core/persona-manager.ts +21 -2
- package/src/core/personas/opencode-agent.ts +1 -0
- package/src/core/processor.ts +51 -14
- package/src/core/prompt-context-builder.ts +38 -5
- package/src/core/queue-processor.ts +4 -2
- package/src/core/room-manager.ts +6 -7
- package/src/core/state/human.ts +6 -0
- package/src/core/state/personas.ts +35 -10
- package/src/core/state/rooms.ts +21 -0
- package/src/core/state-manager.ts +61 -0
- package/src/core/types/data-items.ts +12 -0
- package/src/core/types/entities.ts +3 -0
- package/src/core/types/enums.ts +2 -7
- package/src/core/types/llm.ts +2 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/core/utils/identifier-utils.ts +19 -0
- package/src/core/utils/index.ts +2 -1
- package/src/core/utils/levenshtein.ts +18 -0
- package/src/integrations/claude-code/importer.ts +1 -0
- package/src/integrations/cursor/importer.ts +1 -0
- package/src/prompts/ceremony/index.ts +1 -0
- package/src/prompts/ceremony/person-migration.ts +77 -0
- package/src/prompts/ceremony/rewrite.ts +1 -1
- package/src/prompts/ceremony/user-dedup.ts +15 -1
- package/src/prompts/heartbeat/check.ts +28 -12
- package/src/prompts/heartbeat/ei.ts +2 -0
- package/src/prompts/heartbeat/types.ts +12 -0
- package/src/prompts/human/index.ts +0 -2
- package/src/prompts/human/person-scan.ts +58 -14
- package/src/prompts/human/person-update.ts +171 -96
- package/src/prompts/human/topic-update.ts +1 -1
- package/src/prompts/human/types.ts +5 -1
- package/src/prompts/index.ts +3 -10
- package/src/prompts/message-utils.ts +9 -23
- package/src/prompts/persona/index.ts +3 -10
- package/src/prompts/persona/topics-rate.ts +95 -0
- package/src/prompts/persona/types.ts +8 -48
- package/src/prompts/response/index.ts +3 -7
- package/src/prompts/response/sections.ts +7 -57
- package/src/prompts/room/index.ts +1 -1
- package/src/prompts/room/sections.ts +8 -31
- package/tui/src/commands/me.tsx +14 -7
- package/tui/src/commands/persona.tsx +120 -83
- package/tui/src/components/MessageList.tsx +9 -4
- package/tui/src/components/RoomMessageList.tsx +10 -5
- package/tui/src/context/keyboard.tsx +2 -2
- package/tui/src/util/cyp-editor.tsx +13 -8
- package/tui/src/util/yaml-context.ts +66 -0
- package/tui/src/util/yaml-human.ts +274 -0
- package/tui/src/util/yaml-persona.ts +479 -0
- package/tui/src/util/yaml-provider.ts +215 -0
- package/tui/src/util/yaml-queue.ts +81 -0
- package/tui/src/util/yaml-quotes.ts +46 -0
- package/tui/src/util/yaml-serializers.ts +9 -1417
- package/tui/src/util/yaml-settings.ts +223 -0
- package/tui/src/util/yaml-shared.ts +32 -0
- package/tui/src/util/yaml-toolkit.ts +55 -0
- package/src/prompts/human/person-match.ts +0 -65
- package/src/prompts/persona/topics-match.ts +0 -70
- package/src/prompts/persona/topics-scan.ts +0 -98
- package/src/prompts/persona/topics-update.ts +0 -154
package/src/core/processor.ts
CHANGED
|
@@ -33,8 +33,9 @@ import { yoloMerge } from "../storage/merge.js";
|
|
|
33
33
|
import { StateManager } from "./state-manager.js";
|
|
34
34
|
import { QueueProcessor } from "./queue-processor.js";
|
|
35
35
|
import { handlers } from "./handlers/index.js";
|
|
36
|
-
import { normalizeRoomMessages } from "./handlers/utils.js";
|
|
37
|
-
import {
|
|
36
|
+
import { normalizeRoomMessages, getMessageContent } from "./handlers/utils.js";
|
|
37
|
+
import { sanitizeEiPersonaIdentifiers } from "./utils/identifier-utils.js";
|
|
38
|
+
import { ContextStatus as ContextStatusEnum, RoomMode } from "./types.js";
|
|
38
39
|
import { registerReadMemoryExecutor, registerFileReadExecutor } from "./tools/index.js";
|
|
39
40
|
import { createReadMemoryExecutor } from "./tools/builtin/read-memory.js";
|
|
40
41
|
import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
|
|
@@ -167,6 +168,9 @@ export class Processor {
|
|
|
167
168
|
this.instanceId = ++processorInstanceCount;
|
|
168
169
|
console.log(`[Processor ${this.instanceId}] CREATED`);
|
|
169
170
|
this.detectEnvironment();
|
|
171
|
+
this.stateManager.setQueueChangeListener(() => {
|
|
172
|
+
this.interface.onQueueStateChanged?.("busy");
|
|
173
|
+
});
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
private detectEnvironment(): void {
|
|
@@ -229,6 +233,7 @@ export class Processor {
|
|
|
229
233
|
}
|
|
230
234
|
this.bootstrapTools();
|
|
231
235
|
this.seedBuiltinFacts();
|
|
236
|
+
this.migrateLearnedOn();
|
|
232
237
|
this.seedSettings();
|
|
233
238
|
registerReadMemoryExecutor(createReadMemoryExecutor(this.searchHumanData.bind(this), this.getPersonaList.bind(this)));
|
|
234
239
|
if (this.isTUI) {
|
|
@@ -606,7 +611,8 @@ export class Processor {
|
|
|
606
611
|
max_calls_per_interaction: 1,
|
|
607
612
|
});
|
|
608
613
|
|
|
609
|
-
// submit_response tool — auto-injected for
|
|
614
|
+
// submit_response tool — auto-injected for Heartbeat steps only (HandleHeartbeatCheck).
|
|
615
|
+
// PersonaResponse and RoomResponse agents now use natural Markdown output instead.
|
|
610
616
|
// Not user-configurable; invisible in the tools UI. Terminates the tool loop immediately
|
|
611
617
|
// when called; its arguments become response.parsed.
|
|
612
618
|
this.stateManager.tools_upsertBuiltin({
|
|
@@ -796,6 +802,7 @@ export class Processor {
|
|
|
796
802
|
sentiment: 0,
|
|
797
803
|
validated_date: '',
|
|
798
804
|
last_updated: now,
|
|
805
|
+
learned_on: now,
|
|
799
806
|
};
|
|
800
807
|
human.facts.push(newFact);
|
|
801
808
|
seededCount++;
|
|
@@ -807,6 +814,27 @@ export class Processor {
|
|
|
807
814
|
}
|
|
808
815
|
}
|
|
809
816
|
|
|
817
|
+
private migrateLearnedOn(): void {
|
|
818
|
+
const human = this.stateManager.getHuman();
|
|
819
|
+
|
|
820
|
+
const backfill = <T extends { learned_on?: string; last_updated: string }>(items: T[]): T[] =>
|
|
821
|
+
items.map(item => item.learned_on ? item : { ...item, learned_on: item.last_updated });
|
|
822
|
+
|
|
823
|
+
const facts = backfill(human.facts);
|
|
824
|
+
const topics = backfill(human.topics);
|
|
825
|
+
const people = backfill(human.people);
|
|
826
|
+
|
|
827
|
+
const changed =
|
|
828
|
+
facts.some((f, i) => f !== human.facts[i]) ||
|
|
829
|
+
topics.some((t, i) => t !== human.topics[i]) ||
|
|
830
|
+
people.some((p, i) => p !== human.people[i]);
|
|
831
|
+
|
|
832
|
+
if (changed) {
|
|
833
|
+
this.stateManager.setHuman({ ...human, facts, topics, people });
|
|
834
|
+
console.log("[Processor] Backfilled learned_on for existing data items");
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
810
838
|
private seedSettings(): void {
|
|
811
839
|
const human = this.stateManager.getHuman();
|
|
812
840
|
let modified = false;
|
|
@@ -1020,6 +1048,7 @@ const toolNextSteps = new Set([
|
|
|
1020
1048
|
LLMNextStep.HandleEiHeartbeat,
|
|
1021
1049
|
LLMNextStep.HandleToolContinuation,
|
|
1022
1050
|
LLMNextStep.HandleDedupCurate,
|
|
1051
|
+
LLMNextStep.HandlePersonIdentifierMigration,
|
|
1023
1052
|
]);
|
|
1024
1053
|
const toolPersonaId =
|
|
1025
1054
|
personaId ??
|
|
@@ -1034,8 +1063,13 @@ const toolNextSteps = new Set([
|
|
|
1034
1063
|
(request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
1035
1064
|
request.data.originalNextStep === LLMNextStep.HandleDedupCurate);
|
|
1036
1065
|
|
|
1066
|
+
const isPersonMigrationRequest =
|
|
1067
|
+
request.next_step === LLMNextStep.HandlePersonIdentifierMigration ||
|
|
1068
|
+
(request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
1069
|
+
request.data.originalNextStep === LLMNextStep.HandlePersonIdentifierMigration);
|
|
1070
|
+
|
|
1037
1071
|
let tools: ToolDefinition[] = [];
|
|
1038
|
-
if (isDedupRequest) {
|
|
1072
|
+
if (isDedupRequest || isPersonMigrationRequest) {
|
|
1039
1073
|
const readMemory = this.stateManager.tools_getByName("read_memory");
|
|
1040
1074
|
if (readMemory?.enabled) {
|
|
1041
1075
|
tools = [readMemory];
|
|
@@ -1046,8 +1080,6 @@ const toolNextSteps = new Set([
|
|
|
1046
1080
|
|
|
1047
1081
|
// Auto-inject each handler's dedicated submit tool — infrastructure, not user-visible.
|
|
1048
1082
|
const submitToolByStep: Partial<Record<string, string>> = {
|
|
1049
|
-
[LLMNextStep.HandlePersonaResponse]: "submit_response",
|
|
1050
|
-
[LLMNextStep.HandleRoomResponse]: "submit_response",
|
|
1051
1083
|
[LLMNextStep.HandleHeartbeatCheck]: "submit_heartbeat_check",
|
|
1052
1084
|
[LLMNextStep.HandleEiHeartbeat]: "submit_ei_heartbeat",
|
|
1053
1085
|
[LLMNextStep.HandleDedupCurate]: "submit_dedup_decisions",
|
|
@@ -1063,8 +1095,11 @@ const toolNextSteps = new Set([
|
|
|
1063
1095
|
}
|
|
1064
1096
|
}
|
|
1065
1097
|
|
|
1098
|
+
const toolPersonaName = toolPersonaId
|
|
1099
|
+
? (this.stateManager.persona_getById(toolPersonaId)?.display_name ?? toolPersonaId)
|
|
1100
|
+
: "none";
|
|
1066
1101
|
console.log(
|
|
1067
|
-
`[Tools] Dispatch for ${request.next_step} persona=${
|
|
1102
|
+
`[Tools] Dispatch for ${request.next_step} persona=${toolPersonaName}: ${tools.length} tool(s) attached`
|
|
1068
1103
|
);
|
|
1069
1104
|
|
|
1070
1105
|
this.queueProcessor.start(
|
|
@@ -1391,16 +1426,19 @@ const toolNextSteps = new Set([
|
|
|
1391
1426
|
|
|
1392
1427
|
if (!roomId || !parentMessageId || !personaDisplayName) return request;
|
|
1393
1428
|
|
|
1429
|
+
const room = this.stateManager.getRoom(roomId);
|
|
1430
|
+
if (room?.mode !== RoomMode.FreeForAll) return request;
|
|
1431
|
+
|
|
1394
1432
|
const siblings = this.stateManager.getRoomChildren(roomId, parentMessageId)
|
|
1395
|
-
.filter((m: RoomMessage) => m.role === "persona" && m
|
|
1433
|
+
.filter((m: RoomMessage) => m.role === "persona" && getMessageContent(m))
|
|
1396
1434
|
.map((m: RoomMessage) => ({
|
|
1397
1435
|
name: this.stateManager.persona_getById(m.persona_id ?? "")?.display_name ?? "Participant",
|
|
1398
|
-
verbal_response: m
|
|
1436
|
+
verbal_response: getMessageContent(m),
|
|
1399
1437
|
}));
|
|
1400
1438
|
|
|
1401
1439
|
if (siblings.length === 0) return request;
|
|
1402
1440
|
|
|
1403
|
-
const siblingSection = buildSiblingAwarenessSection(siblings
|
|
1441
|
+
const siblingSection = buildSiblingAwarenessSection(siblings);
|
|
1404
1442
|
return { ...request, system: request.system + "\n\n" + siblingSection };
|
|
1405
1443
|
}
|
|
1406
1444
|
|
|
@@ -1586,9 +1624,7 @@ const toolNextSteps = new Set([
|
|
|
1586
1624
|
|
|
1587
1625
|
if (
|
|
1588
1626
|
response.request.next_step === LLMNextStep.HandlePersonaTraitExtraction ||
|
|
1589
|
-
response.request.next_step === LLMNextStep.
|
|
1590
|
-
response.request.next_step === LLMNextStep.HandlePersonaTopicMatch ||
|
|
1591
|
-
response.request.next_step === LLMNextStep.HandlePersonaTopicUpdate
|
|
1627
|
+
response.request.next_step === LLMNextStep.HandlePersonaTopicRating
|
|
1592
1628
|
) {
|
|
1593
1629
|
const personaId = response.request.data.personaId as string;
|
|
1594
1630
|
if (personaId) {
|
|
@@ -1819,7 +1855,8 @@ const toolNextSteps = new Set([
|
|
|
1819
1855
|
}
|
|
1820
1856
|
|
|
1821
1857
|
async upsertPerson(person: Person): Promise<void> {
|
|
1822
|
-
|
|
1858
|
+
const sanitized = { ...person, identifiers: sanitizeEiPersonaIdentifiers(person.identifiers ?? [], this.stateManager) };
|
|
1859
|
+
await upsertPerson(this.stateManager, sanitized);
|
|
1823
1860
|
this.interface.onHumanUpdated?.();
|
|
1824
1861
|
}
|
|
1825
1862
|
|
|
@@ -4,11 +4,43 @@ import { getEmbeddingService, findTopK } from "./embedding-service.js";
|
|
|
4
4
|
import type { ResponsePromptData, PromptOutput } from "../prompts/index.js";
|
|
5
5
|
import { buildRoomResponsePrompt } from "../prompts/room/index.js";
|
|
6
6
|
import type { RoomParticipantIdentity } from "../prompts/room/types.js";
|
|
7
|
-
import { normalizeRoomMessages } from "./handlers/utils.js";
|
|
7
|
+
import { normalizeRoomMessages, getMessageContent } from "./handlers/utils.js";
|
|
8
8
|
|
|
9
9
|
const QUOTE_LIMIT = 10;
|
|
10
10
|
const DATA_ITEM_LIMIT = 15;
|
|
11
11
|
const SIMILARITY_THRESHOLD = 0.3;
|
|
12
|
+
const HUMAN_CONTEXT_COMBINED_LIMIT = 10;
|
|
13
|
+
const HUMAN_CONTEXT_MIN_EACH = 2;
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// COMBINED TOPICS + PEOPLE CAP
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
function capTopicsAndPeople<T extends { id: string }, P extends { id: string }>(
|
|
20
|
+
topics: T[],
|
|
21
|
+
people: P[]
|
|
22
|
+
): { topics: T[]; people: P[] } {
|
|
23
|
+
const guaranteed = {
|
|
24
|
+
topics: topics.slice(0, HUMAN_CONTEXT_MIN_EACH),
|
|
25
|
+
people: people.slice(0, HUMAN_CONTEXT_MIN_EACH),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let remaining = HUMAN_CONTEXT_COMBINED_LIMIT - guaranteed.topics.length - guaranteed.people.length;
|
|
29
|
+
const extraTopics: T[] = [];
|
|
30
|
+
const extraPeople: P[] = [];
|
|
31
|
+
let ti = HUMAN_CONTEXT_MIN_EACH;
|
|
32
|
+
let pi = HUMAN_CONTEXT_MIN_EACH;
|
|
33
|
+
|
|
34
|
+
while (remaining > 0 && (ti < topics.length || pi < people.length)) {
|
|
35
|
+
if (ti < topics.length && remaining > 0) { extraTopics.push(topics[ti++]); remaining--; }
|
|
36
|
+
if (pi < people.length && remaining > 0) { extraPeople.push(people[pi++]); remaining--; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
topics: [...guaranteed.topics, ...extraTopics],
|
|
41
|
+
people: [...guaranteed.people, ...extraPeople],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
12
44
|
|
|
13
45
|
// =============================================================================
|
|
14
46
|
// EMBEDDING-BASED RELEVANCE SELECTION
|
|
@@ -38,7 +70,6 @@ async function selectRelevantItems<T extends { id: string; embedding?: number[]
|
|
|
38
70
|
}
|
|
39
71
|
}
|
|
40
72
|
|
|
41
|
-
// Fallback: return top items by recency
|
|
42
73
|
return [...items]
|
|
43
74
|
.sort((a, b) => {
|
|
44
75
|
const aTime = (a as { last_updated?: string }).last_updated ?? "";
|
|
@@ -83,12 +114,13 @@ export async function filterHumanDataByVisibility(
|
|
|
83
114
|
const DEFAULT_GROUP = "General";
|
|
84
115
|
|
|
85
116
|
if (persona.id === "ei") {
|
|
86
|
-
const [facts,
|
|
117
|
+
const [facts, rawTopics, rawPeople, quotes] = await Promise.all([
|
|
87
118
|
selectRelevantItems(human.facts, DATA_ITEM_LIMIT, currentMessage),
|
|
88
119
|
selectRelevantItems(human.topics, DATA_ITEM_LIMIT, currentMessage),
|
|
89
120
|
selectRelevantItems(human.people, DATA_ITEM_LIMIT, currentMessage),
|
|
90
121
|
selectRelevantQuotes(human.quotes ?? [], currentMessage),
|
|
91
122
|
]);
|
|
123
|
+
const { topics, people } = capTopicsAndPeople(rawTopics, rawPeople);
|
|
92
124
|
return {
|
|
93
125
|
facts,
|
|
94
126
|
topics,
|
|
@@ -118,12 +150,13 @@ export async function filterHumanDataByVisibility(
|
|
|
118
150
|
return effectiveGroups.some((g) => visibleGroups.has(g));
|
|
119
151
|
});
|
|
120
152
|
|
|
121
|
-
const [facts,
|
|
153
|
+
const [facts, rawTopics, rawPeople, quotes] = await Promise.all([
|
|
122
154
|
selectRelevantItems(filterByGroup(human.facts), DATA_ITEM_LIMIT, currentMessage),
|
|
123
155
|
selectRelevantItems(filterByGroup(human.topics), DATA_ITEM_LIMIT, currentMessage),
|
|
124
156
|
selectRelevantItems(filterByGroup(human.people), DATA_ITEM_LIMIT, currentMessage),
|
|
125
157
|
selectRelevantQuotes(groupFilteredQuotes, currentMessage),
|
|
126
158
|
]);
|
|
159
|
+
const { topics, people } = capTopicsAndPeople(rawTopics, rawPeople);
|
|
127
160
|
|
|
128
161
|
return {
|
|
129
162
|
facts,
|
|
@@ -241,7 +274,7 @@ export async function buildRoomResponsePromptData(
|
|
|
241
274
|
const sourceMessages = byTime.length >= byCount.length ? byTime : byCount;
|
|
242
275
|
|
|
243
276
|
const lastMessage = sourceMessages[sourceMessages.length - 1];
|
|
244
|
-
const currentMessage = lastMessage
|
|
277
|
+
const currentMessage = lastMessage ? getMessageContent(lastMessage) : undefined;
|
|
245
278
|
|
|
246
279
|
const filteredHuman = await filterHumanDataByVisibility(human, respondingPersona, currentMessage);
|
|
247
280
|
|
|
@@ -113,10 +113,12 @@ export class QueueProcessor {
|
|
|
113
113
|
// =========================================================================
|
|
114
114
|
let messages: ChatMessage[] = [];
|
|
115
115
|
|
|
116
|
-
const
|
|
116
|
+
const isPersonaResponse = request.next_step === LLMNextStep.HandlePersonaResponse
|
|
117
|
+
|| request.next_step === LLMNextStep.HandleRoomResponse
|
|
118
|
+
|| request.type === "response" as LLMRequestType;
|
|
117
119
|
const isToolContinuation = request.next_step === LLMNextStep.HandleToolContinuation;
|
|
118
120
|
|
|
119
|
-
if (
|
|
121
|
+
if (isPersonaResponse || isToolContinuation) {
|
|
120
122
|
const personaId = request.data.personaId as string | undefined;
|
|
121
123
|
const isRoomRequest = !!(request.data.roomId as string | undefined);
|
|
122
124
|
// Room conversation is embedded in the prompt via placeholders — don't inject persona history.
|
package/src/core/room-manager.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { StateManager } from "./state-manager.js";
|
|
|
4
4
|
import { buildRoomResponsePromptData } from "./prompt-context-builder.js";
|
|
5
5
|
import { buildRoomJudgePrompt } from "../prompts/room/index.js";
|
|
6
6
|
import type { RoomHistoryMessage, RoomJudgeCandidate } from "../prompts/room/types.js";
|
|
7
|
+
import { getMessageContent } from "./handlers/utils.js";
|
|
7
8
|
|
|
8
9
|
export function getRoomList(sm: StateManager, includeArchived = false): RoomSummary[] {
|
|
9
10
|
return sm.getRoomList(includeArchived);
|
|
@@ -45,7 +46,7 @@ async function queueRoomPersonaResponses(
|
|
|
45
46
|
const model = persona.model ?? sm.getHuman().settings?.default_model ?? "";
|
|
46
47
|
|
|
47
48
|
sm.queue_enqueue({
|
|
48
|
-
type: LLMRequestType.
|
|
49
|
+
type: LLMRequestType.Raw,
|
|
49
50
|
priority: LLMPriority.Room,
|
|
50
51
|
system: promptOutput.system,
|
|
51
52
|
user: promptOutput.user,
|
|
@@ -201,7 +202,7 @@ export async function sendFfaMessage(
|
|
|
201
202
|
const model = persona.model ?? sm.getHuman().settings?.default_model ?? "";
|
|
202
203
|
|
|
203
204
|
sm.queue_enqueue({
|
|
204
|
-
type: LLMRequestType.
|
|
205
|
+
type: LLMRequestType.Raw,
|
|
205
206
|
priority: LLMPriority.Room,
|
|
206
207
|
system: promptOutput.system,
|
|
207
208
|
user: promptOutput.user,
|
|
@@ -282,8 +283,7 @@ export async function activateRoom(
|
|
|
282
283
|
? (human.settings?.name_display ?? "Human")
|
|
283
284
|
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? "Unknown"),
|
|
284
285
|
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
285
|
-
verbal_response: m
|
|
286
|
-
action_response: m.action_response,
|
|
286
|
+
verbal_response: getMessageContent(m) || undefined,
|
|
287
287
|
silence_reason: m.silence_reason,
|
|
288
288
|
}));
|
|
289
289
|
|
|
@@ -293,8 +293,7 @@ export async function activateRoom(
|
|
|
293
293
|
? (human.settings?.name_display ?? "Human")
|
|
294
294
|
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? "Unknown"),
|
|
295
295
|
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
296
|
-
verbal_response: m
|
|
297
|
-
action_response: m.action_response,
|
|
296
|
+
verbal_response: getMessageContent(m) || undefined,
|
|
298
297
|
silence_reason: m.silence_reason,
|
|
299
298
|
}));
|
|
300
299
|
|
|
@@ -380,7 +379,7 @@ export async function selectCYPBranch(
|
|
|
380
379
|
const model = persona.model ?? sm.getHuman().settings?.default_model ?? "";
|
|
381
380
|
|
|
382
381
|
sm.queue_enqueue({
|
|
383
|
-
type: LLMRequestType.
|
|
382
|
+
type: LLMRequestType.Raw,
|
|
384
383
|
priority: LLMPriority.Room,
|
|
385
384
|
system: promptOutput.system,
|
|
386
385
|
user: promptOutput.user,
|
package/src/core/state/human.ts
CHANGED
|
@@ -89,6 +89,12 @@ export class HumanState {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
person_upsert(person: Person): void {
|
|
92
|
+
const identifiers = person.identifiers ?? [];
|
|
93
|
+
person = { ...person, identifiers };
|
|
94
|
+
const primary = identifiers.find(i => i.is_primary) ?? identifiers[0];
|
|
95
|
+
if (primary) {
|
|
96
|
+
person = { ...person, name: primary.value };
|
|
97
|
+
}
|
|
92
98
|
const idx = this.human.people.findIndex((p) => p.id === person.id);
|
|
93
99
|
person.last_updated = new Date().toISOString();
|
|
94
100
|
if (idx >= 0) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { PersonaEntity, Message, ContextStatus } from "../types.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (msg.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
return
|
|
3
|
+
function migrateMessage(msg: Message): Message {
|
|
4
|
+
if (msg.content) return msg;
|
|
5
|
+
if (msg.role === 'human') return msg;
|
|
6
|
+
if (msg.silence_reason) return msg;
|
|
7
|
+
const parts: string[] = [];
|
|
8
|
+
if (msg.action_response) parts.push(`_${msg.action_response}_`);
|
|
9
|
+
if (msg.verbal_response) parts.push(msg.verbal_response);
|
|
10
|
+
if (parts.length === 0) return msg;
|
|
11
|
+
const { verbal_response: _vr, action_response: _ar, ...rest } = msg;
|
|
12
|
+
return { ...rest, content: parts.join('\n\n') };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface PersonaData {
|
|
@@ -246,4 +246,29 @@ export class PersonaState {
|
|
|
246
246
|
}
|
|
247
247
|
return count;
|
|
248
248
|
}
|
|
249
|
+
|
|
250
|
+
messages_getUnextractedForPersona(personaId: string, shortId: string, sinceTimestamp?: string): Message[] {
|
|
251
|
+
const data = this.personas.get(personaId);
|
|
252
|
+
if (!data) return [];
|
|
253
|
+
return data.messages
|
|
254
|
+
.filter(m => {
|
|
255
|
+
if (sinceTimestamp && new Date(m.timestamp).getTime() < new Date(sinceTimestamp).getTime()) return false;
|
|
256
|
+
return !m.persona_extracted?.[shortId];
|
|
257
|
+
})
|
|
258
|
+
.map(m => ({ ...m }));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
messages_markPersonaExtracted(personaId: string, messageIds: string[], shortId: string): number {
|
|
262
|
+
const data = this.personas.get(personaId);
|
|
263
|
+
if (!data) return 0;
|
|
264
|
+
const idsSet = new Set(messageIds);
|
|
265
|
+
let count = 0;
|
|
266
|
+
for (const msg of data.messages) {
|
|
267
|
+
if (idsSet.has(msg.id) && !msg.persona_extracted?.[shortId]) {
|
|
268
|
+
msg.persona_extracted = { ...msg.persona_extracted, [shortId]: true };
|
|
269
|
+
count++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return count;
|
|
273
|
+
}
|
|
249
274
|
}
|
package/src/core/state/rooms.ts
CHANGED
|
@@ -179,4 +179,25 @@ export class RoomState {
|
|
|
179
179
|
}
|
|
180
180
|
return count;
|
|
181
181
|
}
|
|
182
|
+
|
|
183
|
+
messages_getUnextractedForPersona(roomId: string, shortId: string): RoomMessage[] {
|
|
184
|
+
const activePath = new Set(this.messages_getActivePath(roomId).map(m => m.id));
|
|
185
|
+
return (this.rooms.get(roomId)?.messages ?? [])
|
|
186
|
+
.filter(m => activePath.has(m.id) && !m.persona_extracted?.[shortId])
|
|
187
|
+
.map(m => ({ ...m }));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
messages_markPersonaExtracted(roomId: string, messageIds: string[], shortId: string): number {
|
|
191
|
+
const room = this.rooms.get(roomId);
|
|
192
|
+
if (!room) return 0;
|
|
193
|
+
const ids = new Set(messageIds);
|
|
194
|
+
let count = 0;
|
|
195
|
+
for (const msg of room.messages) {
|
|
196
|
+
if (ids.has(msg.id) && !msg.persona_extracted?.[shortId]) {
|
|
197
|
+
msg.persona_extracted = { ...msg.persona_extracted, [shortId]: true };
|
|
198
|
+
count++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return count;
|
|
202
|
+
}
|
|
182
203
|
}
|
|
@@ -34,6 +34,11 @@ export class StateManager {
|
|
|
34
34
|
private roomState = new RoomState();
|
|
35
35
|
private queueState = new QueueState();
|
|
36
36
|
private persistenceState = new PersistenceState();
|
|
37
|
+
private queueChangeListener?: () => void;
|
|
38
|
+
|
|
39
|
+
setQueueChangeListener(listener: () => void): void {
|
|
40
|
+
this.queueChangeListener = listener;
|
|
41
|
+
}
|
|
37
42
|
private providers: ToolProvider[] = [];
|
|
38
43
|
private tools: ToolDefinition[] = [];
|
|
39
44
|
private embeddingWarning = false;
|
|
@@ -62,6 +67,34 @@ export class StateManager {
|
|
|
62
67
|
this.migrateMessageFlags();
|
|
63
68
|
this.migrateInterestedPersonas();
|
|
64
69
|
this.migrateProviderModel();
|
|
70
|
+
this.migrateRoomMessageContent();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private migrateRoomMessageContent(): void {
|
|
74
|
+
const rooms = this.roomState.getAll(true);
|
|
75
|
+
let migratedCount = 0;
|
|
76
|
+
|
|
77
|
+
for (const room of rooms) {
|
|
78
|
+
for (const msg of room.messages) {
|
|
79
|
+
if (msg.content) continue;
|
|
80
|
+
if (msg.role === 'human') continue;
|
|
81
|
+
if (msg.silence_reason) continue;
|
|
82
|
+
const parts: string[] = [];
|
|
83
|
+
const legacy = msg as RoomMessage & { verbal_response?: string; action_response?: string };
|
|
84
|
+
if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
|
|
85
|
+
if (legacy.verbal_response) parts.push(legacy.verbal_response);
|
|
86
|
+
if (parts.length === 0) continue;
|
|
87
|
+
msg.content = parts.join('\n\n');
|
|
88
|
+
delete (msg as any).verbal_response;
|
|
89
|
+
delete (msg as any).action_response;
|
|
90
|
+
migratedCount++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (migratedCount > 0) {
|
|
95
|
+
this.scheduleSave();
|
|
96
|
+
console.log(`[StateManager] Migrated ${migratedCount} room messages to unified content field`);
|
|
97
|
+
}
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
/**
|
|
@@ -150,6 +183,7 @@ export class StateManager {
|
|
|
150
183
|
exposure_current: 0.3,
|
|
151
184
|
exposure_desired: 0.3,
|
|
152
185
|
last_updated: fact.last_updated,
|
|
186
|
+
learned_on: fact.last_updated,
|
|
153
187
|
learned_by: fact.learned_by,
|
|
154
188
|
last_changed_by: fact.last_changed_by,
|
|
155
189
|
persona_groups: fact.persona_groups,
|
|
@@ -518,6 +552,16 @@ export class StateManager {
|
|
|
518
552
|
return count;
|
|
519
553
|
}
|
|
520
554
|
|
|
555
|
+
getRoomUnextractedMessagesForPersona(roomId: string, shortId: string): RoomMessage[] {
|
|
556
|
+
return this.roomState.messages_getUnextractedForPersona(roomId, shortId);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
markRoomMessagesPersonaExtracted(roomId: string, messageIds: string[], shortId: string): number {
|
|
560
|
+
const count = this.roomState.messages_markPersonaExtracted(roomId, messageIds, shortId);
|
|
561
|
+
if (count > 0) this.scheduleSave();
|
|
562
|
+
return count;
|
|
563
|
+
}
|
|
564
|
+
|
|
521
565
|
private scheduleSave(): void {
|
|
522
566
|
this.persistenceState.scheduleSave(this.buildStorageState());
|
|
523
567
|
}
|
|
@@ -565,6 +609,12 @@ export class StateManager {
|
|
|
565
609
|
return result;
|
|
566
610
|
}
|
|
567
611
|
|
|
612
|
+
human_person_getByIdentifier(type: string | null, value: string): Person | undefined {
|
|
613
|
+
return this.getHuman().people.find(p =>
|
|
614
|
+
p.identifiers?.some(i => (!type || i.type === type) && i.value === value)
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
568
618
|
human_quote_add(quote: Quote): void {
|
|
569
619
|
this.humanState.quote_add(quote);
|
|
570
620
|
this.scheduleSave();
|
|
@@ -713,6 +763,16 @@ export class StateManager {
|
|
|
713
763
|
return result;
|
|
714
764
|
}
|
|
715
765
|
|
|
766
|
+
messages_getUnextractedForPersona(personaId: string, shortId: string, sinceTimestamp?: string): Message[] {
|
|
767
|
+
return this.personaState.messages_getUnextractedForPersona(personaId, shortId, sinceTimestamp);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
messages_markPersonaExtracted(personaId: string, messageIds: string[], shortId: string): number {
|
|
771
|
+
const result = this.personaState.messages_markPersonaExtracted(personaId, messageIds, shortId);
|
|
772
|
+
if (result > 0) this.scheduleSave();
|
|
773
|
+
return result;
|
|
774
|
+
}
|
|
775
|
+
|
|
716
776
|
queue_enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts" | "state">): string {
|
|
717
777
|
const requestWithModel = {
|
|
718
778
|
...request,
|
|
@@ -720,6 +780,7 @@ export class StateManager {
|
|
|
720
780
|
};
|
|
721
781
|
const id = this.queueState.enqueue(requestWithModel);
|
|
722
782
|
this.scheduleSave();
|
|
783
|
+
this.queueChangeListener?.();
|
|
723
784
|
return id;
|
|
724
785
|
}
|
|
725
786
|
|
|
@@ -10,6 +10,7 @@ export interface DataItemBase {
|
|
|
10
10
|
description: string;
|
|
11
11
|
sentiment: number;
|
|
12
12
|
last_updated: string;
|
|
13
|
+
learned_on?: string; // ISO timestamp when item first entered the system. Immutable after creation. Pairs with learned_by.
|
|
13
14
|
last_mentioned?: string; // Set by extraction only, never ceremony. Used for --recent sorting.
|
|
14
15
|
learned_by?: string; // Persona ID that originally learned this item (stable UUID)
|
|
15
16
|
last_changed_by?: string; // Persona ID that most recently updated this item (stable UUID)
|
|
@@ -54,7 +55,18 @@ export interface PersonaTopic {
|
|
|
54
55
|
last_updated: string; // ISO timestamp
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
export interface PersonIdentifier {
|
|
59
|
+
type: string; // User-extensible. "nickname", "github", "ei_persona", etc. NOT an enum.
|
|
60
|
+
value: string; // The identifier value. For ei_persona: the Persona UUID.
|
|
61
|
+
is_primary?: boolean; // True = this is the display name. Synced to DataItemBase.name on write.
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
export interface Person extends DataItemBase {
|
|
65
|
+
identifiers?: PersonIdentifier[];
|
|
66
|
+
// DataItemBase.name stays. Must always equal:
|
|
67
|
+
// identifiers.find(i => i.is_primary)?.value ?? identifiers[0]?.value ?? name
|
|
68
|
+
// State manager syncs name on every write.
|
|
69
|
+
validated_date?: string; // ISO timestamp. Empty string or absent = candidate for Ei heartbeat intro. Same contract as Fact.validated_date.
|
|
58
70
|
relationship: string;
|
|
59
71
|
exposure_current: number;
|
|
60
72
|
exposure_desired: number;
|
|
@@ -93,6 +93,7 @@ export interface HumanSettings {
|
|
|
93
93
|
default_model?: string; // Will store ModelConfig.id GUID post-migration
|
|
94
94
|
oneshot_model?: string; // Model for AI-assist (wand) requests; falls back to default_model. Will store ModelConfig.id GUID post-migration.
|
|
95
95
|
rewrite_model?: string; // Model for rewrite ceremony step; must be capable (Sonnet/Opus class). Unset = rewrite disabled. Will store ModelConfig.id GUID post-migration.
|
|
96
|
+
people_migration_complete?: boolean; // Set to true when all Person records have identifiers. Ceremony migration step short-circuits when true.
|
|
96
97
|
queue_paused?: boolean;
|
|
97
98
|
skip_quote_delete_confirm?: boolean;
|
|
98
99
|
name_display?: string;
|
|
@@ -147,6 +148,8 @@ export interface PersonaEntity {
|
|
|
147
148
|
last_heartbeat?: string;
|
|
148
149
|
last_extraction?: string;
|
|
149
150
|
tools?: string[]; // IDs of ToolDefinitions this persona can use. Empty/absent = no tool access.
|
|
151
|
+
reflection_last_asked?: string; // ISO timestamp. Set ONLY when Persona explicitly surfaces identity drift (mentioned_reflection: true).
|
|
152
|
+
description_embedding?: number[]; // Embedding of long_description (short_description fallback). Excludes traits. See embedding-service.ts:getPersonaDescriptionText.
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
export interface PersonaCreationInput {
|
package/src/core/types/enums.ts
CHANGED
|
@@ -32,18 +32,12 @@ export enum LLMNextStep {
|
|
|
32
32
|
HandleHumanPersonScan = "handleHumanPersonScan",
|
|
33
33
|
HandleTopicMatch = "handleTopicMatch",
|
|
34
34
|
HandleTopicUpdate = "handleTopicUpdate",
|
|
35
|
-
HandlePersonMatch = "handlePersonMatch",
|
|
36
35
|
HandlePersonUpdate = "handlePersonUpdate",
|
|
37
36
|
HandlePersonaTraitExtraction = "handlePersonaTraitExtraction",
|
|
38
|
-
|
|
39
|
-
HandlePersonaTopicMatch = "handlePersonaTopicMatch",
|
|
40
|
-
HandlePersonaTopicUpdate = "handlePersonaTopicUpdate",
|
|
37
|
+
HandlePersonaTopicRating = "handlePersonaTopicRating",
|
|
41
38
|
HandleHeartbeatCheck = "handleHeartbeatCheck",
|
|
42
39
|
HandleEiHeartbeat = "handleEiHeartbeat",
|
|
43
40
|
HandleOneShot = "handleOneShot",
|
|
44
|
-
HandlePersonaExpire = "handlePersonaExpire",
|
|
45
|
-
HandlePersonaExplore = "handlePersonaExplore",
|
|
46
|
-
HandleDescriptionCheck = "handleDescriptionCheck",
|
|
47
41
|
// Tool calling continuation (second LLM call after tool execution, may loop for more tool calls).
|
|
48
42
|
// data.toolHistory: serialized LLMHistoryMessage[] (assistant + tool result messages)
|
|
49
43
|
// data.toolCallCounts: serialized Map entries [[name, count], ...] carrying per-tool call counts
|
|
@@ -56,6 +50,7 @@ export enum LLMNextStep {
|
|
|
56
50
|
HandleRoomResponse = "handleRoomResponse",
|
|
57
51
|
HandleRoomJudge = "handleRoomJudge",
|
|
58
52
|
HandlePersonaPreview = "handlePersonaPreview",
|
|
53
|
+
HandlePersonIdentifierMigration = "handlePersonIdentifierMigration",
|
|
59
54
|
}
|
|
60
55
|
|
|
61
56
|
export enum ProviderType {
|
package/src/core/types/llm.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { LLMRequestType, LLMPriority, LLMNextStep } from "./enums.js";
|
|
|
8
8
|
export interface Message {
|
|
9
9
|
id: string;
|
|
10
10
|
role: "human" | "system";
|
|
11
|
+
content?: string; // Raw Markdown response (primary path going forward)
|
|
11
12
|
verbal_response?: string; // Human text or persona's spoken reply
|
|
12
13
|
action_response?: string; // Stage direction / action the persona performs
|
|
13
14
|
silence_reason?: string; // Why the persona chose not to respond (not shown to LLM)
|
|
@@ -21,6 +22,7 @@ export interface Message {
|
|
|
21
22
|
t?: boolean; // Topic extraction completed
|
|
22
23
|
p?: boolean; // Person extraction completed
|
|
23
24
|
e?: boolean; // Event (epic) extraction completed
|
|
25
|
+
persona_extracted?: Record<string, true>; // Per-persona topic scan tracking. Key = personaId.slice(0, 8)
|
|
24
26
|
// Image generation fields (web-only, ephemeral)
|
|
25
27
|
_synthesis?: boolean; // True if message was created by multi-message synthesis
|
|
26
28
|
speaker_name?: string; // Display name of actual speaker; set on room messages for clean hydration
|
package/src/core/types/rooms.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface RoomMessage {
|
|
|
10
10
|
parent_id: string | null;
|
|
11
11
|
role: "human" | "persona";
|
|
12
12
|
persona_id?: string;
|
|
13
|
+
content?: string;
|
|
13
14
|
verbal_response?: string;
|
|
14
15
|
action_response?: string;
|
|
15
16
|
silence_reason?: string;
|
|
@@ -21,6 +22,7 @@ export interface RoomMessage {
|
|
|
21
22
|
t?: boolean;
|
|
22
23
|
p?: boolean;
|
|
23
24
|
e?: boolean;
|
|
25
|
+
persona_extracted?: Record<string, true>; // Per-persona topic scan tracking. Key = personaId.slice(0, 8)
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface RoomEntity {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PersonIdentifier } from "../types/data-items.js";
|
|
2
|
+
import type { StateManager } from "../state-manager.js";
|
|
3
|
+
|
|
4
|
+
export const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
|
+
|
|
6
|
+
export function sanitizeEiPersonaIdentifiers(
|
|
7
|
+
identifiers: PersonIdentifier[],
|
|
8
|
+
state: StateManager
|
|
9
|
+
): PersonIdentifier[] {
|
|
10
|
+
return identifiers.map(id => {
|
|
11
|
+
if (id.type !== 'Ei Persona' && id.type !== 'AI Persona') return id;
|
|
12
|
+
if (UUID_REGEX.test(id.value)) return { ...id, type: 'Ei Persona' };
|
|
13
|
+
const matched = state.persona_getAll().find(p =>
|
|
14
|
+
p.display_name === id.value || p.aliases?.includes(id.value)
|
|
15
|
+
);
|
|
16
|
+
if (matched) return { ...id, type: 'Ei Persona', value: matched.id };
|
|
17
|
+
return id.type === 'AI Persona' ? id : { ...id, type: 'Nickname' };
|
|
18
|
+
});
|
|
19
|
+
}
|
package/src/core/utils/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { crossFind } from "./crossFind.js";
|
|
2
2
|
import { applyDecayToValue } from "./decay.js";
|
|
3
|
+
import { UUID_REGEX, sanitizeEiPersonaIdentifiers } from "./identifier-utils.js";
|
|
3
4
|
|
|
4
|
-
export { crossFind, applyDecayToValue };
|
|
5
|
+
export { crossFind, applyDecayToValue, UUID_REGEX, sanitizeEiPersonaIdentifiers };
|