ei-tui 0.1.17 → 0.1.19
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/embedding-service.ts +11 -2
- package/src/core/handlers/index.ts +325 -1
- package/src/core/llm-client.ts +26 -6
- package/src/core/orchestrators/ceremony.ts +80 -1
- package/src/core/orchestrators/human-extraction.ts +36 -28
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/processor.ts +300 -6
- package/src/core/queue-processor.ts +150 -11
- package/src/core/state-manager.ts +121 -0
- package/src/core/tools/builtin/file-read.ts +58 -0
- package/src/core/tools/builtin/read-memory.ts +54 -0
- package/src/core/tools/builtin/web-search.ts +89 -0
- package/src/core/tools/index.ts +186 -0
- package/src/core/tools/types.ts +27 -0
- package/src/core/types.ts +56 -1
- package/src/integrations/opencode/importer.ts +9 -71
- package/src/prompts/ceremony/index.ts +7 -0
- package/src/prompts/ceremony/rewrite.ts +166 -0
- package/src/prompts/ceremony/types.ts +56 -1
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +1 -1
- package/tui/README.md +1 -0
- package/tui/src/commands/tools.tsx +44 -0
- package/tui/src/components/PromptInput.tsx +2 -0
- package/tui/src/components/ToolkitListOverlay.tsx +178 -0
- package/tui/src/context/ei.tsx +28 -2
- package/tui/src/util/persona-editor.tsx +8 -4
- package/tui/src/util/toolkit-editor.tsx +83 -0
- package/tui/src/util/yaml-serializers.ts +134 -7
package/package.json
CHANGED
|
@@ -201,8 +201,17 @@ function createBunService(): EmbeddingService {
|
|
|
201
201
|
if (embedderPromise) return embedderPromise;
|
|
202
202
|
|
|
203
203
|
embedderPromise = (async () => {
|
|
204
|
-
const mod = await
|
|
205
|
-
|
|
204
|
+
const [mod, os, path] = await Promise.all([
|
|
205
|
+
import(/* @vite-ignore */ FASTEMBED_MODULE),
|
|
206
|
+
import('os'),
|
|
207
|
+
import('path'),
|
|
208
|
+
]);
|
|
209
|
+
// Use EI_DATA_PATH if set, otherwise fall back to ~/.local/share/ei/embeddings.
|
|
210
|
+
// Must be absolute so the cache is stable regardless of cwd.
|
|
211
|
+
const cacheDir = process.env.EI_DATA_PATH
|
|
212
|
+
? path.join(process.env.EI_DATA_PATH, 'embeddings')
|
|
213
|
+
: path.join(os.homedir(), '.local', 'share', 'ei', 'embeddings');
|
|
214
|
+
embedder = await mod.FlagEmbedding.init({ model: mod.EmbeddingModel.AllMiniLML6V2, cacheDir });
|
|
206
215
|
return embedder;
|
|
207
216
|
})();
|
|
208
217
|
|
|
@@ -29,8 +29,13 @@ import type {
|
|
|
29
29
|
PersonaExpireResult,
|
|
30
30
|
PersonaExploreResult,
|
|
31
31
|
DescriptionCheckResult,
|
|
32
|
+
RewriteItemType,
|
|
33
|
+
RewriteScanResult,
|
|
34
|
+
RewriteResult,
|
|
35
|
+
RewriteSubjectMatch,
|
|
32
36
|
} from "../../prompts/ceremony/types.js";
|
|
33
|
-
import {
|
|
37
|
+
import { buildRewritePrompt } from "../../prompts/ceremony/rewrite.js";
|
|
38
|
+
import {
|
|
34
39
|
orchestratePersonaGeneration,
|
|
35
40
|
queueItemMatch,
|
|
36
41
|
queueItemUpdate,
|
|
@@ -59,6 +64,30 @@ import { crossFind } from "../utils/index.js";
|
|
|
59
64
|
|
|
60
65
|
export type ResponseHandler = (response: LLMResponse, state: StateManager) => void | Promise<void>;
|
|
61
66
|
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// REWRITE HANDLER INJECTION
|
|
69
|
+
// searchHumanData lives on Processor — inject it at startup to avoid circular deps.
|
|
70
|
+
// Same pattern as registerReadMemoryExecutor in tools/index.ts.
|
|
71
|
+
// =============================================================================
|
|
72
|
+
|
|
73
|
+
type SearchHumanDataFn = (
|
|
74
|
+
query: string,
|
|
75
|
+
options?: { types?: Array<'fact' | 'trait' | 'topic' | 'person' | 'quote'>; limit?: number }
|
|
76
|
+
) => Promise<{
|
|
77
|
+
facts: Fact[];
|
|
78
|
+
traits: Trait[];
|
|
79
|
+
topics: Topic[];
|
|
80
|
+
people: Person[];
|
|
81
|
+
quotes: Quote[];
|
|
82
|
+
}>;
|
|
83
|
+
|
|
84
|
+
let _searchHumanData: SearchHumanDataFn | null = null;
|
|
85
|
+
|
|
86
|
+
/** Called by Processor at startup to inject searchHumanData for rewrite handlers. */
|
|
87
|
+
export function registerSearchHumanData(fn: SearchHumanDataFn): void {
|
|
88
|
+
_searchHumanData = fn;
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
function splitMessagesByTimestamp(
|
|
63
92
|
messages: Message[],
|
|
64
93
|
analyzeFromTimestamp: string | null
|
|
@@ -1117,6 +1146,298 @@ function handlePersonaTopicUpdate(response: LLMResponse, state: StateManager): v
|
|
|
1117
1146
|
|
|
1118
1147
|
|
|
1119
1148
|
|
|
1149
|
+
// =============================================================================
|
|
1150
|
+
// REWRITE HANDLERS (Ceremony — fire-and-forget)
|
|
1151
|
+
// =============================================================================
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* handleRewriteScan — Phase 1 of Rewrite.
|
|
1155
|
+
* LLM returns an array of subject strings found in the bloated item.
|
|
1156
|
+
* For each subject we search the knowledge base, then queue Phase 2.
|
|
1157
|
+
*/
|
|
1158
|
+
async function handleRewriteScan(response: LLMResponse, state: StateManager): Promise<void> {
|
|
1159
|
+
const itemId = response.request.data.itemId as string;
|
|
1160
|
+
const itemType = response.request.data.itemType as RewriteItemType;
|
|
1161
|
+
const rewriteModel = response.request.data.rewriteModel as string;
|
|
1162
|
+
|
|
1163
|
+
if (!itemId || !itemType) {
|
|
1164
|
+
console.error("[handleRewriteScan] Missing itemId or itemType in request data");
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const subjects = response.parsed as RewriteScanResult | undefined;
|
|
1169
|
+
if (!subjects || !Array.isArray(subjects) || subjects.length === 0) {
|
|
1170
|
+
console.log(`[handleRewriteScan] No extra subjects found for ${itemType} "${itemId}" — item is cohesive`);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (!_searchHumanData) {
|
|
1175
|
+
console.error("[handleRewriteScan] searchHumanData not injected — cannot search for matches");
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Re-read the item from current state (it may have changed since scan was queued)
|
|
1180
|
+
const human = state.getHuman();
|
|
1181
|
+
const allItems: DataItemBase[] = [
|
|
1182
|
+
...human.facts, ...human.traits, ...human.topics, ...human.people,
|
|
1183
|
+
];
|
|
1184
|
+
const currentItem = allItems.find(i => i.id === itemId);
|
|
1185
|
+
if (!currentItem) {
|
|
1186
|
+
console.warn(`[handleRewriteScan] Item ${itemId} no longer exists — skipping rewrite`);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Search for matches per subject, excluding the original item
|
|
1191
|
+
const subjectMatches: RewriteSubjectMatch[] = [];
|
|
1192
|
+
for (const searchTerm of subjects) {
|
|
1193
|
+
try {
|
|
1194
|
+
const results = await _searchHumanData(searchTerm, {
|
|
1195
|
+
types: ["fact", "trait", "topic", "person"],
|
|
1196
|
+
limit: 4, // fetch 4 so we can exclude original and still have 3
|
|
1197
|
+
});
|
|
1198
|
+
const allMatches: DataItemBase[] = [
|
|
1199
|
+
...results.facts, ...results.traits, ...results.topics, ...results.people,
|
|
1200
|
+
].filter(m => m.id !== itemId); // exclude original
|
|
1201
|
+
subjectMatches.push({ searchTerm, matches: allMatches.slice(0, 3) });
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
console.warn(`[handleRewriteScan] Search failed for "${searchTerm}":`, err);
|
|
1204
|
+
subjectMatches.push({ searchTerm, matches: [] });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Build Phase 2 prompt and queue it
|
|
1209
|
+
const prompt = buildRewritePrompt({
|
|
1210
|
+
item: currentItem,
|
|
1211
|
+
itemType,
|
|
1212
|
+
subjects: subjectMatches,
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
state.queue_enqueue({
|
|
1216
|
+
type: LLMRequestType.JSON,
|
|
1217
|
+
priority: LLMPriority.Normal,
|
|
1218
|
+
system: prompt.system,
|
|
1219
|
+
user: prompt.user,
|
|
1220
|
+
next_step: LLMNextStep.HandleRewriteRewrite,
|
|
1221
|
+
model: rewriteModel,
|
|
1222
|
+
data: {
|
|
1223
|
+
itemId,
|
|
1224
|
+
itemType,
|
|
1225
|
+
},
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
console.log(`[handleRewriteScan] Queued Phase 2 for ${itemType} "${currentItem.name}" with ${subjectMatches.length} subject(s)`);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* handleRewriteRewrite — Phase 2 of Rewrite.
|
|
1233
|
+
* LLM returns { existing: [...], new: [...] }.
|
|
1234
|
+
* Upsert existing items by id, create new items with defensive defaults + embeddings.
|
|
1235
|
+
*/
|
|
1236
|
+
async function handleRewriteRewrite(response: LLMResponse, state: StateManager): Promise<void> {
|
|
1237
|
+
const itemId = response.request.data.itemId as string;
|
|
1238
|
+
const itemType = response.request.data.itemType as RewriteItemType;
|
|
1239
|
+
|
|
1240
|
+
if (!itemId || !itemType) {
|
|
1241
|
+
console.error("[handleRewriteRewrite] Missing itemId or itemType in request data");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const result = response.parsed as RewriteResult | undefined;
|
|
1246
|
+
if (!result || (!result.existing?.length && !result.new?.length)) {
|
|
1247
|
+
console.log(`[handleRewriteRewrite] No changes returned for ${itemType} "${itemId}"`);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const human = state.getHuman();
|
|
1252
|
+
const now = new Date().toISOString();
|
|
1253
|
+
|
|
1254
|
+
// Look up the original item to inherit persona_groups
|
|
1255
|
+
const allItems: DataItemBase[] = [
|
|
1256
|
+
...human.facts, ...human.traits, ...human.topics, ...human.people,
|
|
1257
|
+
];
|
|
1258
|
+
const originalItem = allItems.find(i => i.id === itemId);
|
|
1259
|
+
const inheritedGroups = originalItem?.persona_groups;
|
|
1260
|
+
|
|
1261
|
+
// Helper: resolve actual type from existing records (don't trust LLM's type field)
|
|
1262
|
+
const resolveExistingType = (id: string): RewriteItemType | null => {
|
|
1263
|
+
if (human.facts.find(f => f.id === id)) return "fact";
|
|
1264
|
+
if (human.traits.find(t => t.id === id)) return "trait";
|
|
1265
|
+
if (human.topics.find(t => t.id === id)) return "topic";
|
|
1266
|
+
if (human.people.find(p => p.id === id)) return "person";
|
|
1267
|
+
return null;
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
let existingCount = 0;
|
|
1271
|
+
let newCount = 0;
|
|
1272
|
+
|
|
1273
|
+
// --- Process existing items ---
|
|
1274
|
+
for (const item of result.existing ?? []) {
|
|
1275
|
+
if (!item.id || !item.name || !item.description) {
|
|
1276
|
+
console.warn(`[handleRewriteRewrite] Skipping existing item with missing fields: ${JSON.stringify(item).slice(0, 100)}`);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Resolve type from actual records, not from LLM response
|
|
1281
|
+
const resolvedType = resolveExistingType(item.id);
|
|
1282
|
+
if (!resolvedType) {
|
|
1283
|
+
console.warn(`[handleRewriteRewrite] Existing item id "${item.id}" not found in human data — skipping`);
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
let embedding: number[] | undefined;
|
|
1288
|
+
try {
|
|
1289
|
+
const embeddingService = getEmbeddingService();
|
|
1290
|
+
const text = getItemEmbeddingText({ name: item.name, description: item.description });
|
|
1291
|
+
embedding = await embeddingService.embed(text);
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
console.warn(`[handleRewriteRewrite] Failed to compute embedding for existing ${resolvedType} "${item.name}":`, err);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
switch (resolvedType) {
|
|
1297
|
+
case "fact": {
|
|
1298
|
+
const existing = human.facts.find(f => f.id === item.id)!;
|
|
1299
|
+
state.human_fact_upsert({
|
|
1300
|
+
...existing,
|
|
1301
|
+
name: item.name,
|
|
1302
|
+
description: item.description,
|
|
1303
|
+
sentiment: item.sentiment ?? existing.sentiment,
|
|
1304
|
+
last_updated: now,
|
|
1305
|
+
embedding,
|
|
1306
|
+
});
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
case "trait": {
|
|
1310
|
+
const existing = human.traits.find(t => t.id === item.id)!;
|
|
1311
|
+
state.human_trait_upsert({
|
|
1312
|
+
...existing,
|
|
1313
|
+
name: item.name,
|
|
1314
|
+
description: item.description,
|
|
1315
|
+
sentiment: item.sentiment ?? existing.sentiment,
|
|
1316
|
+
strength: item.strength ?? existing.strength,
|
|
1317
|
+
last_updated: now,
|
|
1318
|
+
embedding,
|
|
1319
|
+
});
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
case "topic": {
|
|
1323
|
+
const existing = human.topics.find(t => t.id === item.id)!;
|
|
1324
|
+
state.human_topic_upsert({
|
|
1325
|
+
...existing,
|
|
1326
|
+
name: item.name,
|
|
1327
|
+
description: item.description,
|
|
1328
|
+
sentiment: item.sentiment ?? existing.sentiment,
|
|
1329
|
+
last_updated: now,
|
|
1330
|
+
embedding,
|
|
1331
|
+
});
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
case "person": {
|
|
1335
|
+
const existing = human.people.find(p => p.id === item.id)!;
|
|
1336
|
+
state.human_person_upsert({
|
|
1337
|
+
...existing,
|
|
1338
|
+
name: item.name,
|
|
1339
|
+
description: item.description,
|
|
1340
|
+
sentiment: item.sentiment ?? existing.sentiment,
|
|
1341
|
+
last_updated: now,
|
|
1342
|
+
embedding,
|
|
1343
|
+
});
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
existingCount++;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// --- Process new items ---
|
|
1351
|
+
for (const item of result.new ?? []) {
|
|
1352
|
+
if (!item.type || !item.name || !item.description) {
|
|
1353
|
+
console.warn(`[handleRewriteRewrite] Skipping new item with missing fields: ${JSON.stringify(item).slice(0, 100)}`);
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
let embedding: number[] | undefined;
|
|
1358
|
+
try {
|
|
1359
|
+
const embeddingService = getEmbeddingService();
|
|
1360
|
+
const text = getItemEmbeddingText({ name: item.name, description: item.description });
|
|
1361
|
+
embedding = await embeddingService.embed(text);
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
console.warn(`[handleRewriteRewrite] Failed to compute embedding for new ${item.type} "${item.name}":`, err);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const baseFields = {
|
|
1367
|
+
id: crypto.randomUUID(),
|
|
1368
|
+
name: item.name,
|
|
1369
|
+
description: item.description,
|
|
1370
|
+
sentiment: item.sentiment ?? 0,
|
|
1371
|
+
last_updated: now,
|
|
1372
|
+
learned_by: "ei",
|
|
1373
|
+
persona_groups: inheritedGroups,
|
|
1374
|
+
embedding,
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
switch (item.type) {
|
|
1378
|
+
case "fact": {
|
|
1379
|
+
const fact: Fact = {
|
|
1380
|
+
...baseFields,
|
|
1381
|
+
validated: ValidationLevel.None,
|
|
1382
|
+
validated_date: now,
|
|
1383
|
+
};
|
|
1384
|
+
state.human_fact_upsert(fact);
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
case "trait": {
|
|
1388
|
+
const trait: Trait = {
|
|
1389
|
+
...baseFields,
|
|
1390
|
+
strength: item.strength ?? 0.5,
|
|
1391
|
+
};
|
|
1392
|
+
state.human_trait_upsert(trait);
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
case "topic": {
|
|
1396
|
+
if (!item.category) {
|
|
1397
|
+
console.warn(`[handleRewriteRewrite] New topic "${item.name}" missing category — defaulting to "Interest"`);
|
|
1398
|
+
}
|
|
1399
|
+
const topic: Topic = {
|
|
1400
|
+
...baseFields,
|
|
1401
|
+
category: item.category ?? "Interest",
|
|
1402
|
+
exposure_current: 0.5,
|
|
1403
|
+
exposure_desired: 0.5,
|
|
1404
|
+
};
|
|
1405
|
+
state.human_topic_upsert(topic);
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
case "person": {
|
|
1409
|
+
if (!item.relationship) {
|
|
1410
|
+
console.warn(`[handleRewriteRewrite] New person "${item.name}" missing relationship — defaulting to "Unknown"`);
|
|
1411
|
+
}
|
|
1412
|
+
const person: Person = {
|
|
1413
|
+
...baseFields,
|
|
1414
|
+
relationship: item.relationship ?? "Unknown",
|
|
1415
|
+
exposure_current: 0.5,
|
|
1416
|
+
exposure_desired: 0.5,
|
|
1417
|
+
};
|
|
1418
|
+
state.human_person_upsert(person);
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
default:
|
|
1422
|
+
console.warn(`[handleRewriteRewrite] Unknown type "${item.type}" for new item "${item.name}" — skipping`);
|
|
1423
|
+
}
|
|
1424
|
+
newCount++;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
console.log(`[handleRewriteRewrite] Complete for ${itemType} "${itemId}": ${existingCount} existing updated, ${newCount} new created`);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* handleToolSynthesis — second LLM call in the tool flow.
|
|
1432
|
+
* The QueueProcessor already injected tool history into messages and got the
|
|
1433
|
+
* final persona response. Parse and store it exactly like handlePersonaResponse.
|
|
1434
|
+
*/
|
|
1435
|
+
function handleToolSynthesis(response: LLMResponse, state: StateManager): void {
|
|
1436
|
+
console.log(`[handleToolSynthesis] Routing to handlePersonaResponse`);
|
|
1437
|
+
handlePersonaResponse(response, state);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
|
|
1120
1441
|
export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
1121
1442
|
handlePersonaResponse,
|
|
1122
1443
|
handlePersonaGeneration,
|
|
@@ -1137,4 +1458,7 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
|
1137
1458
|
handlePersonaExpire,
|
|
1138
1459
|
handlePersonaExplore,
|
|
1139
1460
|
handleDescriptionCheck,
|
|
1461
|
+
handleToolSynthesis,
|
|
1462
|
+
handleRewriteScan,
|
|
1463
|
+
handleRewriteRewrite,
|
|
1140
1464
|
};
|
package/src/core/llm-client.ts
CHANGED
|
@@ -17,11 +17,17 @@ export interface ResolvedModel {
|
|
|
17
17
|
export interface LLMCallOptions {
|
|
18
18
|
signal?: AbortSignal;
|
|
19
19
|
temperature?: number;
|
|
20
|
+
/** OpenAI-compatible tools array. When present and non-empty, sent with tool_choice: "auto". */
|
|
21
|
+
tools?: Record<string, unknown>[];
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export interface LLMRawResponse {
|
|
23
25
|
content: string | null;
|
|
24
26
|
finishReason: string | null;
|
|
27
|
+
/** Raw tool_calls array from the API response, present when finishReason is "tool_calls". */
|
|
28
|
+
rawToolCalls?: unknown[];
|
|
29
|
+
/** The full assistant message object (needed to inject into history for the tool loop). */
|
|
30
|
+
assistantMessage?: Record<string, unknown>;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
let llmCallCount = 0;
|
|
@@ -170,14 +176,21 @@ export async function callLLMRaw(
|
|
|
170
176
|
headers["anthropic-dangerous-direct-browser-access"] = "true";
|
|
171
177
|
}
|
|
172
178
|
|
|
179
|
+
const requestBody: Record<string, unknown> = {
|
|
180
|
+
model,
|
|
181
|
+
messages: finalMessages,
|
|
182
|
+
temperature,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (options.tools && options.tools.length > 0) {
|
|
186
|
+
requestBody.tools = options.tools;
|
|
187
|
+
requestBody.tool_choice = "auto";
|
|
188
|
+
}
|
|
189
|
+
|
|
173
190
|
const response = await fetch(`${normalizedBaseURL}/chat/completions`, {
|
|
174
191
|
method: "POST",
|
|
175
192
|
headers,
|
|
176
|
-
body: JSON.stringify(
|
|
177
|
-
model,
|
|
178
|
-
messages: finalMessages,
|
|
179
|
-
temperature,
|
|
180
|
-
}),
|
|
193
|
+
body: JSON.stringify(requestBody),
|
|
181
194
|
signal,
|
|
182
195
|
});
|
|
183
196
|
|
|
@@ -189,9 +202,16 @@ export async function callLLMRaw(
|
|
|
189
202
|
const data = await response.json();
|
|
190
203
|
const choice = data.choices?.[0];
|
|
191
204
|
|
|
205
|
+
const assistantMessage = choice?.message as Record<string, unknown> | undefined;
|
|
206
|
+
const rawToolCalls = Array.isArray(choice?.message?.tool_calls)
|
|
207
|
+
? (choice.message.tool_calls as unknown[])
|
|
208
|
+
: undefined;
|
|
209
|
+
|
|
192
210
|
return {
|
|
193
|
-
content: choice?.message?.content ?? null,
|
|
211
|
+
content: (choice?.message?.content as string | null) ?? null,
|
|
194
212
|
finishReason: choice?.finish_reason ?? null,
|
|
213
|
+
rawToolCalls,
|
|
214
|
+
assistantMessage,
|
|
195
215
|
};
|
|
196
216
|
}
|
|
197
217
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type ExtractionOptions,
|
|
11
11
|
} from "./human-extraction.js";
|
|
12
12
|
import { queuePersonaTopicScan, type PersonaTopicContext } from "./persona-topics.js";
|
|
13
|
-
import { buildPersonaExpirePrompt, buildPersonaExplorePrompt, buildDescriptionCheckPrompt } from "../../prompts/ceremony/index.js";
|
|
13
|
+
import { buildPersonaExpirePrompt, buildPersonaExplorePrompt, buildDescriptionCheckPrompt, buildRewriteScanPrompt, type RewriteItemType } from "../../prompts/ceremony/index.js";
|
|
14
14
|
|
|
15
15
|
export function isNewDay(lastCeremony: string | undefined, now: Date): boolean {
|
|
16
16
|
if (!lastCeremony) return true;
|
|
@@ -227,6 +227,10 @@ export function handleCeremonyProgress(state: StateManager): void {
|
|
|
227
227
|
|
|
228
228
|
// Dedup phase: log near-duplicate human entities (dry-run only, no mutations)
|
|
229
229
|
runDedupPhase(state);
|
|
230
|
+
|
|
231
|
+
// Rewrite phase: fire-and-forget scans for bloated human data items
|
|
232
|
+
// No ceremony_progress gating — Expire/Explore only touch persona topics, zero overlap
|
|
233
|
+
queueRewritePhase(state);
|
|
230
234
|
|
|
231
235
|
// Expire phase: queue LLM calls for each active persona
|
|
232
236
|
// handlePersonaExpire already chains to Explore → DescriptionCheck
|
|
@@ -574,3 +578,78 @@ export function runDedupPhase(state: StateManager): void {
|
|
|
574
578
|
|
|
575
579
|
console.log(`[ceremony:dedup] Done. ${totalCandidates} total candidate pair(s) found.`);
|
|
576
580
|
}
|
|
581
|
+
|
|
582
|
+
// =============================================================================
|
|
583
|
+
// REWRITE PHASE (fire-and-forget — queues Low-priority Phase 1 scans)
|
|
584
|
+
// =============================================================================
|
|
585
|
+
|
|
586
|
+
const REWRITE_DESCRIPTION_THRESHOLD = 750;
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Queue Phase 1 "scan" for every human data item whose description exceeds the
|
|
590
|
+
* threshold. Gated on rewrite_model being set in HumanSettings.
|
|
591
|
+
*
|
|
592
|
+
* Fire-and-forget: no ceremony_progress, no blocking. Expire/Explore proceed
|
|
593
|
+
* immediately since they only touch persona topics (zero overlap with human data).
|
|
594
|
+
* Phase 2 items enqueue at Normal priority, naturally processing before more
|
|
595
|
+
* Low-priority Phase 1 scans.
|
|
596
|
+
*/
|
|
597
|
+
export function queueRewritePhase(state: StateManager): void {
|
|
598
|
+
const human = state.getHuman();
|
|
599
|
+
const rewriteModel = human.settings?.rewrite_model;
|
|
600
|
+
|
|
601
|
+
if (!rewriteModel) {
|
|
602
|
+
console.log("[ceremony:rewrite] rewrite_model not set — skipping rewrite phase");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const itemsToScan: Array<{ item: DataItemBase; type: RewriteItemType }> = [];
|
|
607
|
+
|
|
608
|
+
for (const fact of human.facts) {
|
|
609
|
+
if ((fact.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
|
|
610
|
+
itemsToScan.push({ item: fact, type: "fact" });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
for (const trait of human.traits) {
|
|
614
|
+
if ((trait.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
|
|
615
|
+
itemsToScan.push({ item: trait, type: "trait" });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
for (const topic of human.topics) {
|
|
619
|
+
if ((topic.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
|
|
620
|
+
itemsToScan.push({ item: topic, type: "topic" });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
for (const person of human.people) {
|
|
624
|
+
if ((person.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
|
|
625
|
+
itemsToScan.push({ item: person, type: "person" });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (itemsToScan.length === 0) {
|
|
630
|
+
console.log("[ceremony:rewrite] No items above threshold — nothing to rewrite");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
console.log(`[ceremony:rewrite] Found ${itemsToScan.length} item(s) above ${REWRITE_DESCRIPTION_THRESHOLD} chars — queueing Phase 1 scans`);
|
|
635
|
+
|
|
636
|
+
for (const { item, type } of itemsToScan) {
|
|
637
|
+
const prompt = buildRewriteScanPrompt({ item, itemType: type });
|
|
638
|
+
|
|
639
|
+
state.queue_enqueue({
|
|
640
|
+
type: LLMRequestType.JSON,
|
|
641
|
+
priority: LLMPriority.Low,
|
|
642
|
+
system: prompt.system,
|
|
643
|
+
user: prompt.user,
|
|
644
|
+
next_step: LLMNextStep.HandleRewriteScan,
|
|
645
|
+
model: rewriteModel,
|
|
646
|
+
data: {
|
|
647
|
+
itemId: item.id,
|
|
648
|
+
itemType: type,
|
|
649
|
+
rewriteModel, // pass through so Phase 1 handler can queue Phase 2 with the same model
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
console.log(`[ceremony:rewrite] Queued ${itemsToScan.length} Phase 1 scan(s) at Low priority`);
|
|
655
|
+
}
|
|
@@ -281,11 +281,14 @@ export async function queueItemMatch(
|
|
|
281
281
|
break;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// Traits are personality patterns — they must only match against other traits.
|
|
285
|
+
// Non-trait candidates (facts, topics, people) must never absorb trait content,
|
|
286
|
+
// and trait candidates must never cross-match into facts/topics/people.
|
|
284
287
|
const allItemsWithEmbeddings = [
|
|
285
|
-
...human.facts.map(f => ({ ...f, data_type: "fact" as DataItemType })),
|
|
288
|
+
...(dataType !== "trait" ? human.facts.map(f => ({ ...f, data_type: "fact" as DataItemType })) : []),
|
|
286
289
|
...human.traits.map(t => ({ ...t, data_type: "trait" as DataItemType })),
|
|
287
|
-
...human.topics.map(t => ({ ...t, data_type: "topic" as DataItemType })),
|
|
288
|
-
...human.people.map(p => ({ ...p, data_type: "person" as DataItemType })),
|
|
290
|
+
...(dataType !== "trait" ? human.topics.map(t => ({ ...t, data_type: "topic" as DataItemType })) : []),
|
|
291
|
+
...(dataType !== "trait" ? human.people.map(p => ({ ...p, data_type: "person" as DataItemType })) : []),
|
|
289
292
|
].filter(item => item.embedding && item.embedding.length > 0);
|
|
290
293
|
|
|
291
294
|
let topKItems: Array<{
|
|
@@ -321,15 +324,18 @@ export async function queueItemMatch(
|
|
|
321
324
|
}
|
|
322
325
|
|
|
323
326
|
if (topKItems.length === 0) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
327
|
+
|
|
328
|
+
console.log(`[queueItemMatch] No embeddings available, using filtered items (dataType=${dataType})`);
|
|
329
|
+
|
|
330
|
+
if (dataType !== "trait") {
|
|
331
|
+
for (const fact of human.facts) {
|
|
332
|
+
topKItems.push({
|
|
333
|
+
data_type: "fact",
|
|
334
|
+
data_id: fact.id,
|
|
335
|
+
data_name: fact.name,
|
|
336
|
+
data_description: dataType === "fact" ? fact.description : truncateDescription(fact.description),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
333
339
|
}
|
|
334
340
|
|
|
335
341
|
for (const trait of human.traits) {
|
|
@@ -341,22 +347,24 @@ export async function queueItemMatch(
|
|
|
341
347
|
});
|
|
342
348
|
}
|
|
343
349
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
350
|
+
if (dataType !== "trait") {
|
|
351
|
+
for (const topic of human.topics) {
|
|
352
|
+
topKItems.push({
|
|
353
|
+
data_type: "topic",
|
|
354
|
+
data_id: topic.id,
|
|
355
|
+
data_name: topic.name,
|
|
356
|
+
data_description: dataType === "topic" ? topic.description : truncateDescription(topic.description),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const person of human.people) {
|
|
361
|
+
topKItems.push({
|
|
362
|
+
data_type: "person",
|
|
363
|
+
data_id: person.id,
|
|
364
|
+
data_name: person.name,
|
|
365
|
+
data_description: dataType === "person" ? person.description : truncateDescription(person.description),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
360
368
|
}
|
|
361
369
|
}
|
|
362
370
|
|