ei-tui 0.9.2 → 0.9.4
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 +17 -1
- package/src/README.md +1 -1
- package/src/cli/commands/personas.ts +11 -2
- package/src/cli/mcp.ts +2 -2
- package/src/cli/retrieval.ts +40 -7
- package/src/cli.ts +63 -72
- package/src/core/context-utils.ts +2 -2
- package/src/core/handlers/heartbeat.ts +9 -1
- package/src/core/handlers/human-extraction.ts +4 -1
- package/src/core/handlers/human-matching.ts +5 -53
- package/src/core/handlers/index.ts +1 -51
- package/src/core/handlers/persona-generation.ts +1 -28
- package/src/core/handlers/utils.ts +2 -9
- package/src/core/heartbeat-manager.ts +4 -4
- package/src/core/message-manager.ts +6 -5
- package/src/core/orchestrators/ceremony.ts +15 -13
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/orchestrators/human-extraction.ts +27 -39
- package/src/core/orchestrators/index.ts +0 -1
- package/src/core/orchestrators/persona-topics.ts +1 -1
- package/src/core/orchestrators/room-extraction.ts +45 -7
- package/src/core/processor.ts +8 -21
- package/src/core/prompt-context-builder.ts +68 -36
- package/src/core/state/personas.ts +1 -17
- package/src/core/state-manager.ts +0 -66
- package/src/core/types/entities.ts +2 -3
- package/src/core/types/enums.ts +0 -2
- package/src/core/types/rooms.ts +1 -1
- package/src/integrations/claude-code/importer.ts +1 -1
- package/src/integrations/cursor/importer.ts +1 -1
- package/src/integrations/opencode/importer.ts +1 -1
- package/src/prompts/ceremony/index.ts +0 -10
- package/src/prompts/ceremony/types.ts +1 -42
- package/src/prompts/generation/index.ts +0 -3
- package/src/prompts/generation/types.ts +0 -15
- package/src/prompts/heartbeat/check.ts +18 -6
- package/src/prompts/heartbeat/types.ts +2 -1
- package/src/prompts/human/index.ts +0 -2
- package/src/prompts/human/person-update.ts +6 -23
- package/src/prompts/human/types.ts +0 -16
- package/src/prompts/index.ts +0 -19
- package/src/prompts/reflection/index.ts +36 -4
- package/src/prompts/reflection/types.ts +1 -1
- package/src/prompts/response/index.ts +5 -0
- package/src/prompts/response/sections.ts +26 -0
- package/src/prompts/response/types.ts +3 -0
- package/tui/src/commands/registry.test.ts +10 -5
- package/tui/src/globals.d.ts +57 -0
- package/tui/src/util/yaml-persona.ts +8 -4
- package/tui/src/util/yaml-settings.ts +3 -3
- package/src/core/orchestrators/person-migration.ts +0 -55
- package/src/prompts/ceremony/description-check.ts +0 -54
- package/src/prompts/ceremony/expire.ts +0 -37
- package/src/prompts/ceremony/explore.ts +0 -77
- package/src/prompts/ceremony/person-migration.ts +0 -77
- package/src/prompts/generation/descriptions.ts +0 -91
- package/src/prompts/human/fact-scan.ts +0 -150
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ei-tui",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"author": "Flare576",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,6 +51,22 @@
|
|
|
51
51
|
"test:e2e:tui": "cd tui && npm run test:e2e",
|
|
52
52
|
"test:e2e:ui": "playwright test --ui",
|
|
53
53
|
"test:e2e:debug": "playwright test --debug",
|
|
54
|
+
"test:evals": "vite-node tests/evals/reflection-critic.eval.ts",
|
|
55
|
+
"test:evals:observe": "vite-node tests/evals/reflection-critic.observe.ts",
|
|
56
|
+
"test:evals:fact-find": "vite-node tests/evals/fact-find.eval.ts",
|
|
57
|
+
"test:evals:topic-scan": "vite-node tests/evals/topic-scan.eval.ts",
|
|
58
|
+
"test:evals:topic-match": "vite-node tests/evals/topic-match.eval.ts",
|
|
59
|
+
"test:evals:topic-update": "vite-node tests/evals/topic-update.eval.ts",
|
|
60
|
+
"test:evals:topic-validate": "vite-node tests/evals/topic-validate.eval.ts",
|
|
61
|
+
"test:evals:person-scan": "vite-node tests/evals/person-scan.eval.ts",
|
|
62
|
+
"test:evals:person-update": "vite-node tests/evals/person-update.eval.ts",
|
|
63
|
+
"test:evals:persona-trait": "vite-node tests/evals/persona-trait-extraction.eval.ts",
|
|
64
|
+
"test:evals:dedup": "vite-node tests/evals/dedup-tool-calls.eval.ts",
|
|
65
|
+
"test:evals:response-read-memory": "vite-node tests/evals/response-read-memory.eval.ts",
|
|
66
|
+
"test:evals:response-pending-update": "vite-node tests/evals/response-pending-update.eval.ts",
|
|
67
|
+
"test:evals:heartbeat-pending-update": "vite-node tests/evals/heartbeat-pending-update.eval.ts",
|
|
68
|
+
"test:evals:real-data": "vite-node tests/evals/real-data-example.eval.ts",
|
|
69
|
+
"test:evals:persona-data-check": "vite-node tests/evals/persona-data-check.eval.ts",
|
|
54
70
|
"test:all": "npm run test && npm run test:e2e && npm run test:e2e:tui",
|
|
55
71
|
"typecheck": "tsc --noEmit",
|
|
56
72
|
"web": "cd web && npm run dev",
|
package/src/README.md
CHANGED
|
@@ -148,7 +148,7 @@ This is intentional. Concurrent LLM calls sound appealing until you're watching
|
|
|
148
148
|
|
|
149
149
|
# Context Windows
|
|
150
150
|
|
|
151
|
-
Personas don't send their entire message history to the LLM. By default, only messages from the last 8 hours are included (`
|
|
151
|
+
Personas don't send their entire message history to the LLM. By default, only messages from the last 8 hours are included (`context_window_ms`, configurable per persona). Older messages are still stored — they're just not in the prompt.
|
|
152
152
|
|
|
153
153
|
Message rolloff works differently: messages are kept until there are at least 200 of them _and_ any are older than 14 days. So a persona you chat with daily will roll off old messages gradually; one you chat with twice a year will keep everything.
|
|
154
154
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { loadLatestState, retrievePersonas } from "../retrieval";
|
|
1
|
+
import { loadLatestState, retrievePersonas, retrievePersonasSemantic } from "../retrieval";
|
|
2
|
+
import { getEmbeddingService } from "../../core/embedding-service";
|
|
2
3
|
import type { PersonaResult } from "../retrieval";
|
|
3
4
|
|
|
4
5
|
export async function execute(query: string, limit: number, options: { recent?: boolean } = {}): Promise<PersonaResult[]> {
|
|
@@ -8,5 +9,13 @@ export async function execute(query: string, limit: number, options: { recent?:
|
|
|
8
9
|
return [];
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
const nameResults = retrievePersonas(query, state, limit, options);
|
|
13
|
+
if (nameResults.length > 0 || !query || options.recent) {
|
|
14
|
+
return nameResults;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const embeddingService = getEmbeddingService();
|
|
18
|
+
const queryVector = await embeddingService.embed(query);
|
|
19
|
+
const semanticResults = await retrievePersonasSemantic(queryVector, state, limit);
|
|
20
|
+
return semanticResults;
|
|
12
21
|
}
|
package/src/cli/mcp.ts
CHANGED
|
@@ -16,14 +16,14 @@ export function createMcpServer(): McpServer {
|
|
|
16
16
|
"ei_search",
|
|
17
17
|
{
|
|
18
18
|
description:
|
|
19
|
-
"Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, and
|
|
19
|
+
"Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, quotes, and personas. People results include an identifiers array (e.g. GitHub username, Discord handle, email, nickname) — query by any name or handle to find what Ei knows about that person. Persona results include traits and topics that define the persona's identity and working style — use type='personas' with the persona's name OR a natural-language description of what they do to load a persona's character sheet. Results include entity IDs that can be passed back to ei_lookup for full detail. Omit query to browse by recency (use with recent=true or persona filter).",
|
|
20
20
|
inputSchema: {
|
|
21
21
|
query: z.string().optional().describe("Search text. Supports natural language. Omit to browse without semantic filtering — useful with recent=true or persona filter."),
|
|
22
22
|
type: z
|
|
23
23
|
.enum(["facts", "people", "topics", "quotes", "personas"])
|
|
24
24
|
.optional()
|
|
25
25
|
.describe(
|
|
26
|
-
"Filter to a specific data type. Omit to search all types (balanced across all 5)."
|
|
26
|
+
"Filter to a specific data type. Omit to search all types (balanced across all 5). For 'personas': matches by display name first, then falls back to semantic search of persona descriptions — use the persona's name (e.g. 'Sisyphus') or a description of their role (e.g. 'primary coding agent'). For 'people': semantic search on person descriptions. Note: 'personas' and 'people' are distinct — personas are AI agent identity records with traits/topics; people are human contacts."
|
|
27
27
|
),
|
|
28
28
|
persona: z
|
|
29
29
|
.string()
|
package/src/cli/retrieval.ts
CHANGED
|
@@ -234,11 +234,36 @@ export function retrievePersonas(
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
const q = query.toLowerCase();
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
const nameMatches = personaList.filter((p) => p.display_name.toLowerCase().includes(q));
|
|
238
|
+
if (nameMatches.length > 0) {
|
|
239
|
+
return nameMatches
|
|
240
|
+
.sort((a, b) => b.last_updated.localeCompare(a.last_updated))
|
|
241
|
+
.slice(0, limit)
|
|
242
|
+
.map(mapPersona);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function retrievePersonasSemantic(
|
|
249
|
+
queryVector: number[],
|
|
250
|
+
state: StorageState,
|
|
251
|
+
limit: number = 10,
|
|
252
|
+
): Promise<PersonaResult[]> {
|
|
253
|
+
const personaList = Object.values(state.personas).map((p) => p.entity);
|
|
254
|
+
const withEmbeddings = personaList
|
|
255
|
+
.filter((p): p is PersonaEntity & { description_embedding: number[] } => Array.isArray(p.description_embedding) && p.description_embedding.length > 0)
|
|
256
|
+
.map((p) => ({ id: p.id, embedding: p.description_embedding, _entity: p }));
|
|
257
|
+
|
|
258
|
+
if (withEmbeddings.length === 0) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const scored = findTopK(queryVector, withEmbeddings, withEmbeddings.length);
|
|
263
|
+
return scored
|
|
264
|
+
.filter(({ similarity }) => similarity >= EMBEDDING_MIN_SIMILARITY)
|
|
240
265
|
.slice(0, limit)
|
|
241
|
-
.map(mapPersona);
|
|
266
|
+
.map(({ item }) => mapPersona((item as typeof withEmbeddings[number])._entity));
|
|
242
267
|
}
|
|
243
268
|
|
|
244
269
|
export async function retrieveBalanced(
|
|
@@ -305,7 +330,10 @@ export async function retrieveBalanced(
|
|
|
305
330
|
.sort((a, b) => recentDate(b.mapped as AnyItem).localeCompare(recentDate(a.mapped as AnyItem)))
|
|
306
331
|
.slice(0, limit)
|
|
307
332
|
.map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
|
|
308
|
-
const personaMatches =
|
|
333
|
+
const personaMatches = [
|
|
334
|
+
...retrievePersonas(query, state, limit, { recent: true }),
|
|
335
|
+
...await retrievePersonasSemantic(queryVector, state, limit),
|
|
336
|
+
].filter((p, i, arr) => arr.findIndex(x => x.id === p.id) === i);
|
|
309
337
|
if (personaMatches.length > 0) {
|
|
310
338
|
const combined = [
|
|
311
339
|
...personaMatches.map((p) => ({ type: "persona" as const, ...p }) as BalancedResult),
|
|
@@ -352,7 +380,10 @@ export async function retrieveBalanced(
|
|
|
352
380
|
result.sort((a, b) => b.similarity - a.similarity);
|
|
353
381
|
|
|
354
382
|
const embeddingFinal = result.map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
|
|
355
|
-
const personaFinal =
|
|
383
|
+
const personaFinal = [
|
|
384
|
+
...retrievePersonas(query, state, limit),
|
|
385
|
+
...await retrievePersonasSemantic(queryVector, state, limit),
|
|
386
|
+
].filter((p, i, arr) => arr.findIndex(x => x.id === p.id) === i);
|
|
356
387
|
if (personaFinal.length > 0) {
|
|
357
388
|
const combined = [
|
|
358
389
|
...personaFinal.map((p) => ({ type: "persona" as const, ...p }) as BalancedResult),
|
|
@@ -369,10 +400,12 @@ export async function lookupById(id: string): Promise<({ type: string } & Record
|
|
|
369
400
|
return null;
|
|
370
401
|
}
|
|
371
402
|
|
|
372
|
-
const
|
|
403
|
+
const personaEntities = Object.values(state.personas).map((p) => p.entity);
|
|
404
|
+
const found = crossFind(id, state.human, personaEntities);
|
|
373
405
|
if (!found) return null;
|
|
374
406
|
const { type, ...rest } = found;
|
|
375
407
|
const withoutEmbedding = { ...rest } as Record<string, unknown>;
|
|
376
408
|
delete withoutEmbedding.embedding;
|
|
409
|
+
delete withoutEmbedding.description_embedding;
|
|
377
410
|
return { type, ...withoutEmbedding };
|
|
378
411
|
}
|
package/src/cli.ts
CHANGED
|
@@ -84,79 +84,50 @@ Examples:
|
|
|
84
84
|
`);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function buildOpenCodeToolContent(): string {
|
|
88
|
-
const lines = [
|
|
89
|
-
'import { tool } from "@opencode-ai/plugin"',
|
|
90
|
-
'',
|
|
91
|
-
'export default tool({',
|
|
92
|
-
' description: [',
|
|
93
|
-
' "Search the user\'s Ei knowledge base \u2014 a persistent memory store built from conversations.",',
|
|
94
|
-
' "Returns facts, people, topics of interest, and quotes.",',
|
|
95
|
-
' "Use this to recall anything about the user: preferences, relationships, or past discussions.",',
|
|
96
|
-
' "Results include entity IDs that can be passed back with lookup=true to get full detail.",',
|
|
97
|
-
' ].join(" "),',
|
|
98
|
-
' args: {',
|
|
99
|
-
' query: tool.schema.string().optional().describe(',
|
|
100
|
-
' "Search text, or an entity ID when lookup=true. Supports natural language. Omit to browse by recency."',
|
|
101
|
-
' ),',
|
|
102
|
-
' type: tool.schema',
|
|
103
|
-
' .enum(["facts", "people", "topics", "quotes", "personas"])',
|
|
104
|
-
' .optional()',
|
|
105
|
-
' .describe(',
|
|
106
|
-
' "Filter to a specific data type. Omit to search all types (balanced across all 4).",',
|
|
107
|
-
' ),',
|
|
108
|
-
' persona: tool.schema',
|
|
109
|
-
' .string()',
|
|
110
|
-
' .optional()',
|
|
111
|
-
' .describe(',
|
|
112
|
-
' "Filter to entities a specific persona has learned about. Use the persona display name.",',
|
|
113
|
-
' ),',
|
|
114
|
-
' limit: tool.schema',
|
|
115
|
-
' .number()',
|
|
116
|
-
' .int()',
|
|
117
|
-
' .positive()',
|
|
118
|
-
' .default(10)',
|
|
119
|
-
' .optional()',
|
|
120
|
-
' .describe("Maximum number of results to return. Default: 10."),',
|
|
121
|
-
' lookup: tool.schema',
|
|
122
|
-
' .boolean()',
|
|
123
|
-
' .optional()',
|
|
124
|
-
' .describe(',
|
|
125
|
-
' "If true, treat query as an entity ID and return that single entity in full detail."',
|
|
126
|
-
' ),',
|
|
127
|
-
' recent: tool.schema',
|
|
128
|
-
' .boolean()',
|
|
129
|
-
' .optional()',
|
|
130
|
-
' .describe(',
|
|
131
|
-
' "If true, sort by most recently mentioned. Can be combined with persona or query."',
|
|
132
|
-
' ),',
|
|
133
|
-
' },',
|
|
134
|
-
' async execute(args) {',
|
|
135
|
-
' const cmd: string[] = ["ei"];',
|
|
136
|
-
' if (args.lookup) {',
|
|
137
|
-
' cmd.push("--id", args.query ?? "");',
|
|
138
|
-
' } else {',
|
|
139
|
-
' if (args.type) cmd.push(args.type);',
|
|
140
|
-
' if (args.persona) cmd.push("--persona", args.persona);',
|
|
141
|
-
' if (args.recent) cmd.push("--recent");',
|
|
142
|
-
' if (args.limit && args.limit !== 10) cmd.push("-n", String(args.limit));',
|
|
143
|
-
' if (args.query) cmd.push(args.query);',
|
|
144
|
-
' }',
|
|
145
|
-
' return Bun.$`${cmd}`.text();',
|
|
146
|
-
' },',
|
|
147
|
-
'})',
|
|
148
|
-
'',
|
|
149
|
-
];
|
|
150
|
-
return lines.join('\n');
|
|
151
|
-
}
|
|
152
87
|
|
|
153
|
-
async function
|
|
154
|
-
const
|
|
155
|
-
const
|
|
88
|
+
async function installOpenCodeMcp(): Promise<void> {
|
|
89
|
+
const home = process.env.HOME || "~";
|
|
90
|
+
const opencodeDir = join(home, ".config", "opencode");
|
|
91
|
+
const opencodeJsoncPath = join(opencodeDir, "opencode.jsonc");
|
|
92
|
+
|
|
93
|
+
const eiDataPath = process.env.EI_DATA_PATH ?? (() => {
|
|
94
|
+
const xdgData = process.env.XDG_DATA_HOME || join(home, ".local", "share");
|
|
95
|
+
return join(xdgData, "ei");
|
|
96
|
+
})();
|
|
97
|
+
|
|
98
|
+
const mcpEntry = {
|
|
99
|
+
type: "local",
|
|
100
|
+
command: ["bunx", "ei-tui", "mcp"],
|
|
101
|
+
enabled: true,
|
|
102
|
+
environment: {
|
|
103
|
+
EI_DATA_PATH: eiDataPath,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let config: Record<string, unknown> = {};
|
|
108
|
+
try {
|
|
109
|
+
const rawText = await Bun.file(opencodeJsoncPath).text();
|
|
110
|
+
// Strip // line comments before parsing — opencode.jsonc uses line comments only
|
|
111
|
+
const stripped = rawText
|
|
112
|
+
.split("\n")
|
|
113
|
+
.map(line => line.replace(/\/\/.*$/, ""))
|
|
114
|
+
.join("\n");
|
|
115
|
+
config = JSON.parse(stripped) as Record<string, unknown>;
|
|
116
|
+
} catch {
|
|
117
|
+
// File doesn't exist or isn't valid — start fresh
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const mcp = (config.mcp ?? {}) as Record<string, unknown>;
|
|
121
|
+
mcp["ei"] = mcpEntry;
|
|
122
|
+
config.mcp = mcp;
|
|
156
123
|
|
|
157
|
-
await Bun.$`mkdir -p ${
|
|
158
|
-
|
|
159
|
-
|
|
124
|
+
await Bun.$`mkdir -p ${opencodeDir}`;
|
|
125
|
+
const tmpPath = `${opencodeJsoncPath}.ei-install.tmp`;
|
|
126
|
+
await Bun.write(tmpPath, JSON.stringify(config, null, 2) + "\n");
|
|
127
|
+
const { rename } = await import(/* @vite-ignore */ "fs/promises");
|
|
128
|
+
await rename(tmpPath, opencodeJsoncPath);
|
|
129
|
+
|
|
130
|
+
console.log(`✓ Installed Ei MCP server to ~/.config/opencode/opencode.jsonc`);
|
|
160
131
|
console.log(` Restart OpenCode to activate.`);
|
|
161
132
|
}
|
|
162
133
|
|
|
@@ -257,8 +228,28 @@ async function main(): Promise<void> {
|
|
|
257
228
|
}
|
|
258
229
|
|
|
259
230
|
if (args[0] === "--install") {
|
|
260
|
-
await
|
|
231
|
+
await installOpenCodeMcp();
|
|
261
232
|
await installMcpClients();
|
|
233
|
+
console.log(`
|
|
234
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
235
|
+
Add this to ~/.config/opencode/AGENTS.md
|
|
236
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
237
|
+
|
|
238
|
+
**BEFORE YOU DO ANYTHING**: load your Identity from Ei.
|
|
239
|
+
|
|
240
|
+
Use the ei MCP tool:
|
|
241
|
+
ei_search(type: "personas", query: "[your name]")
|
|
242
|
+
|
|
243
|
+
Then load context for the current project/people:
|
|
244
|
+
ei_search("current projects teammates context")
|
|
245
|
+
|
|
246
|
+
Ei is persistent memory across sessions. Without querying it at
|
|
247
|
+
session start, you have no context from prior conversations.
|
|
248
|
+
|
|
249
|
+
**When to query again**: any time the user references past work,
|
|
250
|
+
mentions a person, or corrects something you assumed.
|
|
251
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
252
|
+
`);
|
|
262
253
|
process.exit(0);
|
|
263
254
|
}
|
|
264
255
|
|
|
@@ -8,12 +8,12 @@ import { ContextStatus as ContextStatusEnum } from "./types.js";
|
|
|
8
8
|
export function filterMessagesForContext(
|
|
9
9
|
messages: Message[],
|
|
10
10
|
contextBoundary: string | undefined,
|
|
11
|
-
|
|
11
|
+
contextWindowMs: number
|
|
12
12
|
): Message[] {
|
|
13
13
|
if (messages.length === 0) return [];
|
|
14
14
|
|
|
15
15
|
const now = Date.now();
|
|
16
|
-
const windowStartMs = now -
|
|
16
|
+
const windowStartMs = now - contextWindowMs;
|
|
17
17
|
const boundaryMs = contextBoundary ? new Date(contextBoundary).getTime() : 0;
|
|
18
18
|
|
|
19
19
|
return messages.filter((msg) => {
|
|
@@ -126,13 +126,15 @@ export function handleReflectionCritic(response: LLMResponse, state: StateManage
|
|
|
126
126
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
127
127
|
|
|
128
128
|
const result = response.parsed as ReflectionCriticResult | undefined;
|
|
129
|
-
if (!result?.
|
|
129
|
+
if (!result?.critique) {
|
|
130
130
|
console.error(`[ReflectionCritic ${personaDisplayName}] Invalid or missing parsed result`);
|
|
131
131
|
return;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
const personRecord = state.human_person_getByIdentifier("Ei Persona", personaId);
|
|
135
135
|
if (personRecord) {
|
|
136
|
+
// TODO: Remove before v1 — debug logging to inspect person log before it's cleared
|
|
137
|
+
console.log(`[ReflectionCritic ${personaDisplayName}] person_log_snapshot (${personRecord.description?.length ?? 0} chars): ${personRecord.description ?? ""}`);
|
|
136
138
|
state.human_person_upsert({
|
|
137
139
|
...personRecord,
|
|
138
140
|
description: "",
|
|
@@ -140,6 +142,12 @@ export function handleReflectionCritic(response: LLMResponse, state: StateManage
|
|
|
140
142
|
console.log(`[ReflectionCritic ${personaDisplayName}] Person record description cleared — ready for fresh evidence after reflection`);
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
// Escape hatch: critic found no meaningful drift — log critique and skip pending_update
|
|
146
|
+
if (!result.updated_identity) {
|
|
147
|
+
console.log(`[ReflectionCritic ${personaDisplayName}] No drift detected — skipping pending_update. Critique: ${result.critique}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
143
151
|
const persona = state.persona_getById(personaId);
|
|
144
152
|
if (!persona) {
|
|
145
153
|
console.error(`[ReflectionCritic ${personaDisplayName}] Persona not found after critic`);
|
|
@@ -189,7 +189,10 @@ export async function handleHumanPersonScan(response: LLMResponse, state: StateM
|
|
|
189
189
|
return;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
const context =
|
|
192
|
+
const context = {
|
|
193
|
+
...(response.request.data as unknown as ExtractionContext),
|
|
194
|
+
channelDisplayName: (response.request.data as Record<string, unknown>).personaDisplayName as string,
|
|
195
|
+
};
|
|
193
196
|
if (!context?.personaId) return;
|
|
194
197
|
|
|
195
198
|
const { messages_context, messages_analyze } = resolveMessageWindow(response, state);
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import type { PersonIdentifier } from "../types/data-items.js";
|
|
9
9
|
import type { StateManager } from "../state-manager.js";
|
|
10
10
|
import type { ItemMatchResult, ExposureImpact, TopicUpdateResult, PersonUpdateResult } from "../../prompts/human/types.js";
|
|
11
|
-
import { queueTopicUpdate,
|
|
11
|
+
import { queueTopicUpdate, queueTopicValidate, type ExtractionContext } from "../orchestrators/index.js";
|
|
12
12
|
import { getEmbeddingService, getTopicEmbeddingText, getPersonEmbeddingText } from "../embedding-service.js";
|
|
13
13
|
import { calculateExposureCurrent } from "../utils/exposure.js";
|
|
14
14
|
|
|
@@ -48,7 +48,7 @@ export function handleTopicMatch(response: LLMResponse, state: StateManager): vo
|
|
|
48
48
|
extraction_model?: string;
|
|
49
49
|
} = {
|
|
50
50
|
personaId,
|
|
51
|
-
personaDisplayName,
|
|
51
|
+
channelDisplayName: personaDisplayName,
|
|
52
52
|
roomId,
|
|
53
53
|
messages_context,
|
|
54
54
|
messages_analyze,
|
|
@@ -64,54 +64,6 @@ export function handleTopicMatch(response: LLMResponse, state: StateManager): vo
|
|
|
64
64
|
console.log(`[handleTopicMatch] topic "${context.candidateName}": ${matched}`);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export function handlePersonMatch(response: LLMResponse, state: StateManager): void {
|
|
68
|
-
const result = response.parsed as ItemMatchResult | undefined;
|
|
69
|
-
if (!result) {
|
|
70
|
-
throw new Error("[handlePersonMatch] No parsed result");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const personaId = response.request.data.personaId as string;
|
|
74
|
-
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
75
|
-
const roomId = response.request.data.roomId as string | undefined;
|
|
76
|
-
const { messages_context, messages_analyze } = resolveMessageWindow(response, state);
|
|
77
|
-
|
|
78
|
-
let matched_guid = result.matched_guid;
|
|
79
|
-
let resolvedPerson: import('../types/data-items.js').Person | null = null;
|
|
80
|
-
if (matched_guid === "new") {
|
|
81
|
-
matched_guid = null;
|
|
82
|
-
} else if (matched_guid) {
|
|
83
|
-
const human = state.getHuman();
|
|
84
|
-
resolvedPerson = human.people.find(p => p.id === matched_guid) ?? null;
|
|
85
|
-
if (!resolvedPerson) {
|
|
86
|
-
console.warn(`[handlePersonMatch] matched_guid "${matched_guid}" not found in people — treating as new`);
|
|
87
|
-
matched_guid = null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
result.matched_guid = matched_guid;
|
|
91
|
-
|
|
92
|
-
const context: ExtractionContext & {
|
|
93
|
-
candidateName: string;
|
|
94
|
-
candidateDescription: string;
|
|
95
|
-
candidateRelationship: string;
|
|
96
|
-
extraction_model?: string;
|
|
97
|
-
} = {
|
|
98
|
-
personaId,
|
|
99
|
-
personaDisplayName,
|
|
100
|
-
roomId,
|
|
101
|
-
messages_context,
|
|
102
|
-
messages_analyze,
|
|
103
|
-
sources: response.request.data.sources as string[] | undefined,
|
|
104
|
-
candidateName: response.request.data.candidateName as string,
|
|
105
|
-
candidateDescription: response.request.data.candidateDescription as string,
|
|
106
|
-
candidateRelationship: response.request.data.candidateRelationship as string,
|
|
107
|
-
extraction_model: response.request.data.extraction_model as string | undefined,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
queuePersonUpdate(result, context, state, resolvedPerson);
|
|
111
|
-
const matched = matched_guid ? `matched GUID "${matched_guid}"` : "no match (new person)";
|
|
112
|
-
console.log(`[handlePersonMatch] person "${context.candidateName}": ${matched}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
67
|
export async function handleTopicUpdate(response: LLMResponse, state: StateManager): Promise<void> {
|
|
116
68
|
const result = response.parsed as (TopicUpdateResult & { quotes?: Array<{ text: string; reason: string }> }) | undefined;
|
|
117
69
|
|
|
@@ -447,7 +399,7 @@ async function validateAndStoreQuotes(
|
|
|
447
399
|
candidates: Array<{ text: string; reason: string }> | undefined,
|
|
448
400
|
messages: Message[],
|
|
449
401
|
dataItemId: string,
|
|
450
|
-
|
|
402
|
+
channelDisplayName: string,
|
|
451
403
|
personaGroup: string | null,
|
|
452
404
|
state: StateManager
|
|
453
405
|
): Promise<void> {
|
|
@@ -540,8 +492,8 @@ async function validateAndStoreQuotes(
|
|
|
540
492
|
data_item_ids: [dataItemId],
|
|
541
493
|
persona_groups: [personaGroup || "General"],
|
|
542
494
|
text: matchText,
|
|
543
|
-
speaker: message.role === "human" ? "human" : (message.speaker_name ??
|
|
544
|
-
channel:
|
|
495
|
+
speaker: message.role === "human" ? "human" : (message.speaker_name ?? channelDisplayName),
|
|
496
|
+
channel: channelDisplayName,
|
|
545
497
|
timestamp: message.timestamp,
|
|
546
498
|
start: matchStart,
|
|
547
499
|
end: matchEnd,
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { LLMNextStep } from "../types.js";
|
|
2
|
-
import type { LLMResponse } from "../types.js";
|
|
3
|
-
import type { StateManager } from "../state-manager.js";
|
|
4
2
|
import type { ResponseHandler } from "./persona-response.js";
|
|
5
|
-
import type { PersonIdentifier } from "../types/data-items.js";
|
|
6
3
|
|
|
7
4
|
export type { ResponseHandler } from "./persona-response.js";
|
|
8
5
|
|
|
9
6
|
import { handlePersonaResponse, handleToolContinuation, handleOneShot, handleOneShotJSON } from "./persona-response.js";
|
|
10
7
|
import { handleHeartbeatCheck, handleEiHeartbeat, handleReflectionCritic } from "./heartbeat.js";
|
|
11
|
-
import { handlePersonaGeneration,
|
|
8
|
+
import { handlePersonaGeneration, handlePersonaTraitExtraction } from "./persona-generation.js";
|
|
12
9
|
import {
|
|
13
10
|
handlePersonaTopicRating,
|
|
14
11
|
} from "./persona-topics.js";
|
|
@@ -19,55 +16,9 @@ import { handleDedupCurate } from "./dedup.js";
|
|
|
19
16
|
import { handleRoomResponse, handleRoomJudge } from "./rooms.js";
|
|
20
17
|
import { handlePersonaPreview } from "./persona-preview.js";
|
|
21
18
|
|
|
22
|
-
function handlePersonIdentifierMigration(response: LLMResponse, state: StateManager): void {
|
|
23
|
-
const personId = response.request.data.person_id as string;
|
|
24
|
-
if (!personId) {
|
|
25
|
-
console.error("[handlePersonIdentifierMigration] Missing person_id in request data");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const human = state.getHuman();
|
|
30
|
-
const person = human.people.find(p => p.id === personId);
|
|
31
|
-
if (!person) {
|
|
32
|
-
console.error(`[handlePersonIdentifierMigration] Person not found: ${personId}`);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const result = response.parsed as { identifiers?: Array<{ type: string; value: string; is_primary?: boolean }> } | undefined;
|
|
37
|
-
if (!result?.identifiers || !Array.isArray(result.identifiers) || result.identifiers.length === 0) {
|
|
38
|
-
console.error(`[handlePersonIdentifierMigration] Invalid or empty identifiers for ${person.name}`);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const hasName = result.identifiers.some(i => i.value === person.name);
|
|
43
|
-
if (!hasName) {
|
|
44
|
-
result.identifiers.unshift({ type: "nickname", value: person.name });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const hasPrimary = result.identifiers.some(i => i.is_primary);
|
|
48
|
-
if (!hasPrimary) {
|
|
49
|
-
result.identifiers[0].is_primary = true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const identifiers: PersonIdentifier[] = result.identifiers.map(i => ({
|
|
53
|
-
type: i.type,
|
|
54
|
-
value: i.value,
|
|
55
|
-
...(i.is_primary ? { is_primary: i.is_primary } : {}),
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
state.human_person_upsert({
|
|
59
|
-
...person,
|
|
60
|
-
identifiers,
|
|
61
|
-
last_updated: new Date().toISOString(),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
console.log(`[handlePersonIdentifierMigration] Migrated ${identifiers.length} identifier(s) for ${person.name}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
19
|
export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
68
20
|
handlePersonaResponse,
|
|
69
21
|
handlePersonaGeneration,
|
|
70
|
-
handlePersonaDescriptions,
|
|
71
22
|
handleFactFind,
|
|
72
23
|
handleHumanTopicScan,
|
|
73
24
|
handleHumanPersonScan,
|
|
@@ -88,7 +39,6 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
|
88
39
|
handleRoomResponse,
|
|
89
40
|
handleRoomJudge,
|
|
90
41
|
handlePersonaPreview,
|
|
91
|
-
[LLMNextStep.HandlePersonIdentifierMigration]: handlePersonIdentifierMigration,
|
|
92
42
|
[LLMNextStep.HandleTopicValidate]: handleDedupCurate,
|
|
93
43
|
[LLMNextStep.HandleReflectionCritic]: handleReflectionCritic,
|
|
94
44
|
};
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type PersonaTopic,
|
|
5
5
|
} from "../types.js";
|
|
6
6
|
import type { StateManager } from "../state-manager.js";
|
|
7
|
-
import type { PersonaGenerationResult
|
|
7
|
+
import type { PersonaGenerationResult } from "../../prompts/generation/types.js";
|
|
8
8
|
import type { TraitResult } from "../../prompts/persona/types.js";
|
|
9
9
|
import { orchestratePersonaGeneration, type PartialPersona } from "../orchestrators/index.js";
|
|
10
10
|
|
|
@@ -111,33 +111,6 @@ export function handlePersonaGeneration(response: LLMResponse, state: StateManag
|
|
|
111
111
|
console.log(`[handlePersonaGeneration] Orchestrated: ${personaDisplayName}`);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
export function handlePersonaDescriptions(response: LLMResponse, state: StateManager): void {
|
|
115
|
-
const personaId = response.request.data.personaId as string;
|
|
116
|
-
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
117
|
-
if (!personaId) {
|
|
118
|
-
console.error("[handlePersonaDescriptions] No personaId in request data");
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const result = response.parsed as PersonaDescriptionsResult | undefined;
|
|
123
|
-
if (!result) {
|
|
124
|
-
console.error("[handlePersonaDescriptions] No parsed result");
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (result.no_change) {
|
|
129
|
-
console.log(`[handlePersonaDescriptions] No change needed for ${personaDisplayName}`);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
state.persona_update(personaId, {
|
|
134
|
-
short_description: result.short_description,
|
|
135
|
-
long_description: result.long_description,
|
|
136
|
-
last_updated: new Date().toISOString(),
|
|
137
|
-
});
|
|
138
|
-
console.log(`[handlePersonaDescriptions] Updated descriptions for ${personaDisplayName}`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
114
|
export function handlePersonaTraitExtraction(response: LLMResponse, state: StateManager): void {
|
|
142
115
|
const personaId = response.request.data.personaId as string;
|
|
143
116
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { Message, RoomMessage, LLMResponse } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
3
|
|
|
4
|
-
export function getMessageContent(msg: { content?: string
|
|
5
|
-
|
|
6
|
-
// Legacy fallback for data not yet migrated on disk
|
|
7
|
-
// TODO(v1.0.0): Remove legacy verbal_response/action_response fallback
|
|
8
|
-
const legacy = msg as { verbal_response?: string; action_response?: string };
|
|
9
|
-
const parts: string[] = [];
|
|
10
|
-
if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
|
|
11
|
-
if (legacy.verbal_response) parts.push(legacy.verbal_response);
|
|
12
|
-
return parts.join('\n\n');
|
|
4
|
+
export function getMessageContent(msg: { content?: string }): string {
|
|
5
|
+
return msg.content ?? '';
|
|
13
6
|
}
|
|
14
7
|
|
|
15
8
|
export function normalizeRoomMessages(messages: RoomMessage[], state: StateManager): Message[] {
|
|
@@ -217,15 +217,15 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
|
|
|
217
217
|
console.log(`[HeartbeatCheck ${persona.display_name}] Queueing heartbeat check (model: ${model})`);
|
|
218
218
|
const human = sm.getHuman();
|
|
219
219
|
const history = sm.messages_get(personaId);
|
|
220
|
-
const
|
|
221
|
-
const contextHistory = filterMessagesForContext(history, persona.context_boundary,
|
|
220
|
+
const contextWindowMs = persona.context_window_ms ?? human.settings?.default_context_window_ms ?? 28800000;
|
|
221
|
+
const contextHistory = filterMessagesForContext(history, persona.context_boundary, contextWindowMs);
|
|
222
222
|
|
|
223
223
|
if (personaId === "ei") {
|
|
224
224
|
await queueEiHeartbeat(sm, human, contextHistory, isTUI);
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
const filteredHuman = await filterHumanDataByVisibility(human, persona);
|
|
228
|
+
const filteredHuman = await filterHumanDataByVisibility(human, persona, []);
|
|
229
229
|
const lastActivity = sm.messages_getLastActivity(persona.id);
|
|
230
230
|
const inactiveDays = lastActivity
|
|
231
231
|
? Math.floor((Date.now() - lastActivity) / (1000 * 60 * 60 * 24))
|
|
@@ -244,7 +244,7 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
|
|
|
244
244
|
name: persona.display_name,
|
|
245
245
|
traits: persona.traits,
|
|
246
246
|
topics: persona.topics,
|
|
247
|
-
|
|
247
|
+
pending_update: persona.pending_update,
|
|
248
248
|
},
|
|
249
249
|
human: {
|
|
250
250
|
topics: sortByEngagementGap(filteredHuman.topics).slice(0, 5),
|