ei-tui 0.6.5 → 0.6.7
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/cli/README.md +16 -7
- package/src/cli/commands/people.ts +1 -0
- package/src/cli/mcp.ts +1 -1
- package/src/cli/retrieval.ts +3 -1
- package/src/core/handlers/human-matching.ts +10 -4
- package/src/core/llm-client.ts +12 -1
- package/src/core/orchestrators/human-extraction.ts +19 -7
- package/src/core/persona-manager.ts +3 -0
- package/src/core/processor.ts +3 -0
- package/src/core/queue-processor.ts +8 -5
- package/src/core/state-manager.ts +66 -0
- package/src/core/tools/builtin/read-memory.ts +1 -1
- package/src/core/types/entities.ts +16 -1
- package/src/core/types/integrations.ts +3 -0
- package/src/core/utils/identifier-utils.ts +24 -0
- package/src/core/utils/theme-codec.ts +78 -0
- package/tui/src/commands/me.tsx +14 -7
- package/tui/src/context/ei.tsx +9 -2
- package/tui/src/util/e2e-flags.ts +13 -0
- package/tui/src/util/yaml-human.ts +33 -6
- package/tui/src/util/yaml-provider.ts +34 -9
package/package.json
CHANGED
package/src/cli/README.md
CHANGED
|
@@ -72,9 +72,11 @@ ei "What are the user's current preferences, active projects, and workflow?"
|
|
|
72
72
|
|
|
73
73
|
Ei is a persistent knowledge base built from the user's conversations — facts, preferences,
|
|
74
74
|
people, topics, personas. Use it when the user references past work, mentions how they like things done,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
asks "how did we do X," or needs to look up a person by any name, handle, or account (GitHub username,
|
|
76
|
+
Discord handle, email, nickname, etc.) — people results include an `identifiers` array covering all
|
|
77
|
+
known accounts and aliases for that person. Use `ei --persona "Beta" "walruses"` to scope results to
|
|
78
|
+
what a specific persona has learned. Use `ei personas "name"` to find personas by name. Query again
|
|
79
|
+
mid-session when they correct you or reference something from a previous session.
|
|
78
80
|
```
|
|
79
81
|
|
|
80
82
|
### Claude Code
|
|
@@ -87,9 +89,11 @@ natural-language query about the user's preferences, active projects, and workfl
|
|
|
87
89
|
A `persona` filter is available to scope results to what a specific persona has learned.
|
|
88
90
|
Use `type: "personas"` to search for personas by name.
|
|
89
91
|
|
|
90
|
-
Use Ei when the user references past decisions, mentions people or preferences,
|
|
91
|
-
"how did we do X
|
|
92
|
-
|
|
92
|
+
Use Ei when the user references past decisions, mentions people or preferences, asks
|
|
93
|
+
"how did we do X," or needs to look up a person by any name, handle, or account — people
|
|
94
|
+
results include an `identifiers` array (GitHub username, Discord handle, email, nickname, etc.)
|
|
95
|
+
covering all known accounts and aliases. Query again when they correct you or reference
|
|
96
|
+
something from a previous session.
|
|
93
97
|
```
|
|
94
98
|
|
|
95
99
|
### Cursor
|
|
@@ -111,6 +115,9 @@ conversations (facts, people, topics, quotes, personas).
|
|
|
111
115
|
doesn't have that context.
|
|
112
116
|
- You need the user's preferences, contacts, or project conventions (e.g. who to ask for
|
|
113
117
|
access, how something was fixed).
|
|
118
|
+
- You need to look up a person by any name, handle, or account — people results include an
|
|
119
|
+
`identifiers` array (GitHub username, Discord handle, email, nickname, etc.) covering all
|
|
120
|
+
known accounts and aliases for that person.
|
|
114
121
|
- The question is about the user personally (people, workflow, prior discussions) rather
|
|
115
122
|
than only code.
|
|
116
123
|
|
|
@@ -138,7 +145,9 @@ The installed tool gives OpenCode agents access to all five data types with prop
|
|
|
138
145
|
|
|
139
146
|
All search commands return arrays. Each result includes a `type` field.
|
|
140
147
|
|
|
141
|
-
**Fact /
|
|
148
|
+
**Fact / Topic**: `{ type, id, name, description, sentiment, ...type-specific fields }`
|
|
149
|
+
|
|
150
|
+
**Person**: `{ type, id, name, description, relationship, sentiment, identifiers[] }` — `identifiers` contains all known accounts and aliases (e.g. `{ type: "GitHub", value: "flare576" }`)
|
|
142
151
|
|
|
143
152
|
**Quote**: `{ type, id, text, speaker, timestamp, linked_items[] }`
|
|
144
153
|
|
package/src/cli/mcp.ts
CHANGED
|
@@ -16,7 +16,7 @@ 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 quotes. 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).",
|
|
19
|
+
"Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, and quotes. 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. 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
|
package/src/cli/retrieval.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StorageState, Quote, Fact, Person, Topic } from "../core/types";
|
|
2
2
|
import type { PersonaEntity } from "../core/types/entities.js";
|
|
3
|
-
import type { PersonaTrait, PersonaTopic } from "../core/types/data-items.js";
|
|
3
|
+
import type { PersonaTrait, PersonaTopic, PersonIdentifier } from "../core/types/data-items.js";
|
|
4
4
|
import { decodeAllEmbeddings } from "../storage/embeddings";
|
|
5
5
|
import { crossFind } from "../core/utils/index.ts";
|
|
6
6
|
import { join } from "path";
|
|
@@ -102,6 +102,7 @@ export interface PersonResult {
|
|
|
102
102
|
description: string;
|
|
103
103
|
relationship: string;
|
|
104
104
|
sentiment: number;
|
|
105
|
+
identifiers: PersonIdentifier[];
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export interface TopicResult {
|
|
@@ -182,6 +183,7 @@ function mapPerson(person: Person): PersonResult {
|
|
|
182
183
|
description: person.description,
|
|
183
184
|
relationship: person.relationship,
|
|
184
185
|
sentiment: person.sentiment,
|
|
186
|
+
identifiers: person.identifiers ?? [],
|
|
185
187
|
};
|
|
186
188
|
}
|
|
187
189
|
|
|
@@ -14,7 +14,7 @@ import { calculateExposureCurrent } from "../utils/exposure.js";
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
import { resolveMessageWindow, getMessageText, normalizeRoomMessages } from "./utils.js";
|
|
17
|
-
import { sanitizeEiPersonaIdentifiers } from "../utils/identifier-utils.js";
|
|
17
|
+
import { sanitizeEiPersonaIdentifiers, normalizeIdentifierType } from "../utils/identifier-utils.js";
|
|
18
18
|
|
|
19
19
|
export function handleTopicMatch(response: LLMResponse, state: StateManager): void {
|
|
20
20
|
const result = response.parsed as ItemMatchResult | undefined;
|
|
@@ -284,7 +284,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
284
284
|
if (isNewItem) {
|
|
285
285
|
const llmIdentifiers: PersonIdentifier[] = sanitizeEiPersonaIdentifiers(
|
|
286
286
|
(result.identifiers ?? []).map(i => ({
|
|
287
|
-
type: i.type,
|
|
287
|
+
type: normalizeIdentifierType(i.type, state),
|
|
288
288
|
value: i.value,
|
|
289
289
|
...(i.is_primary ? { is_primary: i.is_primary } : {}),
|
|
290
290
|
})),
|
|
@@ -293,7 +293,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
293
293
|
const allCandidateIds = [...llmIdentifiers, ...candidateIdentifiers];
|
|
294
294
|
if (allCandidateIds.length === 0) {
|
|
295
295
|
const hasSpace = candidateName.includes(' ');
|
|
296
|
-
allCandidateIds.push({ type: hasSpace ? "
|
|
296
|
+
allCandidateIds.push({ type: hasSpace ? "Full Name" : "Nickname", value: candidateName, is_primary: true });
|
|
297
297
|
}
|
|
298
298
|
const deduped: PersonIdentifier[] = [];
|
|
299
299
|
for (const id of allCandidateIds) {
|
|
@@ -304,7 +304,13 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
304
304
|
resolvedIdentifiers = deduped;
|
|
305
305
|
} else {
|
|
306
306
|
const base = [...(existingPerson?.identifiers ?? [])];
|
|
307
|
-
const sanitizedToAdd = sanitizeEiPersonaIdentifiers(
|
|
307
|
+
const sanitizedToAdd = sanitizeEiPersonaIdentifiers(
|
|
308
|
+
(result.identifiers_to_add ?? []).map(i => ({
|
|
309
|
+
...i,
|
|
310
|
+
type: normalizeIdentifierType(i.type, state),
|
|
311
|
+
})),
|
|
312
|
+
state
|
|
313
|
+
);
|
|
308
314
|
for (const id of sanitizedToAdd) {
|
|
309
315
|
if (!base.some(e => e.value === id.value)) {
|
|
310
316
|
base.push({ type: id.type, value: id.value, ...(id.is_primary ? { is_primary: id.is_primary } : {}) });
|
package/src/core/llm-client.ts
CHANGED
|
@@ -51,9 +51,10 @@ function isGuid(str: string): boolean {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function buildResolvedModel(account: ProviderAccount, model: ModelConfig): ResolvedModel {
|
|
54
|
+
const apiModelId = model.model_id ?? model.name;
|
|
54
55
|
return {
|
|
55
56
|
provider: account.name,
|
|
56
|
-
model:
|
|
57
|
+
model: apiModelId === "(default)" ? undefined : apiModelId,
|
|
57
58
|
config: {
|
|
58
59
|
name: account.name,
|
|
59
60
|
baseURL: account.url,
|
|
@@ -164,10 +165,16 @@ function findModelAndAccount(
|
|
|
164
165
|
const model = account?.models?.find((m) => m.name === modelName);
|
|
165
166
|
return { model, account };
|
|
166
167
|
}
|
|
168
|
+
// Try matching by model UUID first
|
|
167
169
|
for (const account of accounts) {
|
|
168
170
|
const model = account.models?.find((m) => m.id === spec);
|
|
169
171
|
if (model) return { model, account };
|
|
170
172
|
}
|
|
173
|
+
// Fall back to matching by account name (bare spec like "EG" or "RnP")
|
|
174
|
+
const accountByName = accounts.find(
|
|
175
|
+
(a) => a.name.toLowerCase() === spec.toLowerCase() && a.enabled
|
|
176
|
+
);
|
|
177
|
+
if (accountByName) return { model: undefined, account: accountByName };
|
|
171
178
|
return { model: undefined, account: undefined };
|
|
172
179
|
}
|
|
173
180
|
|
|
@@ -265,6 +272,10 @@ export async function callLLMRaw(
|
|
|
265
272
|
max_tokens: modelConfig?.max_output_tokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
266
273
|
};
|
|
267
274
|
|
|
275
|
+
if (modelConfig?.thinking_budget !== undefined) {
|
|
276
|
+
requestBody.think = { budget_tokens: modelConfig.thinking_budget };
|
|
277
|
+
}
|
|
278
|
+
|
|
268
279
|
if (options.tools && options.tools.length > 0) {
|
|
269
280
|
requestBody.tools = options.tools;
|
|
270
281
|
requestBody.tool_choice = "auto";
|
|
@@ -281,6 +281,7 @@ export function queueDirectTopicUpdate(
|
|
|
281
281
|
isNewItem: false,
|
|
282
282
|
existingItemId: topic.id,
|
|
283
283
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
284
|
+
extraction_model: extractionModel,
|
|
284
285
|
},
|
|
285
286
|
});
|
|
286
287
|
}
|
|
@@ -425,14 +426,11 @@ export function queueTopicUpdate(
|
|
|
425
426
|
user: prompt.user,
|
|
426
427
|
next_step: LLMNextStep.HandleTopicUpdate,
|
|
427
428
|
data: {
|
|
428
|
-
|
|
429
|
-
personaDisplayName: context.personaDisplayName,
|
|
430
|
-
roomId: context.roomId,
|
|
429
|
+
...context,
|
|
431
430
|
isNewItem,
|
|
432
431
|
existingItemId: existingItem?.id,
|
|
433
432
|
candidateName: isNewItem ? context.candidateName : undefined,
|
|
434
433
|
candidateDescription: isNewItem ? context.candidateDescription : undefined,
|
|
435
|
-
candidateCategory: context.candidateCategory,
|
|
436
434
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
437
435
|
},
|
|
438
436
|
});
|
|
@@ -469,13 +467,25 @@ export function queueEventSummary(
|
|
|
469
467
|
|
|
470
468
|
const allMessages = state.messages_get(personaId);
|
|
471
469
|
const extractionModel = options?.extraction_model;
|
|
470
|
+
const gapMs = gapHours * 60 * 60 * 1000;
|
|
471
|
+
const now = Date.now();
|
|
472
472
|
let totalChunks = 0;
|
|
473
473
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
for (const windowMessages of windows) {
|
|
474
|
+
for (let i = 0; i < windows.length; i++) {
|
|
475
|
+
const windowMessages = windows[i];
|
|
477
476
|
if (windowMessages.length === 0) continue;
|
|
478
477
|
|
|
478
|
+
const isLastWindow = i === windows.length - 1;
|
|
479
|
+
if (isLastWindow) {
|
|
480
|
+
const lastMsgTime = new Date(windowMessages[windowMessages.length - 1].timestamp).getTime();
|
|
481
|
+
if (now - lastMsgTime < gapMs) {
|
|
482
|
+
console.log(`[queueEventSummary] Skipping open window for ${persona.display_name} — last message < ${gapHours}h ago`);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
state.messages_markExtracted(personaId, windowMessages.map(m => m.id), "e");
|
|
488
|
+
|
|
479
489
|
const windowStartTime = new Date(windowMessages[0].timestamp).getTime();
|
|
480
490
|
const messages_context = allMessages.filter(
|
|
481
491
|
m => m.e === true && new Date(m.timestamp).getTime() < windowStartTime
|
|
@@ -599,6 +609,7 @@ export function queuePersonUpdate(
|
|
|
599
609
|
candidateRelationship: context.candidateRelationship,
|
|
600
610
|
candidateIdentifiers: isNewItem ? candidateIdentifiers : undefined,
|
|
601
611
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
612
|
+
extraction_model: context.extraction_model,
|
|
602
613
|
},
|
|
603
614
|
});
|
|
604
615
|
}
|
|
@@ -653,6 +664,7 @@ export async function queueTopicValidate(
|
|
|
653
664
|
data: {
|
|
654
665
|
entity_type: "topic",
|
|
655
666
|
entity_ids: [existingTopic.id, newTopic.id],
|
|
667
|
+
extraction_model: extractionModel,
|
|
656
668
|
},
|
|
657
669
|
});
|
|
658
670
|
}
|
|
@@ -20,6 +20,9 @@ export async function getPersonaList(sm: StateManager): Promise<PersonaSummary[]
|
|
|
20
20
|
unread_count: sm.messages_countUnread(entity.id),
|
|
21
21
|
last_activity: entity.last_activity,
|
|
22
22
|
context_boundary: entity.context_boundary,
|
|
23
|
+
avatar_emoji: entity.avatar_emoji,
|
|
24
|
+
avatar_image: entity.avatar_image,
|
|
25
|
+
preferred_theme: entity.preferred_theme,
|
|
23
26
|
}));
|
|
24
27
|
}
|
|
25
28
|
|
package/src/core/processor.ts
CHANGED
|
@@ -37,9 +37,10 @@ export interface QueueProcessorStartOptions {
|
|
|
37
37
|
onEnqueue?: EnqueueCallback;
|
|
38
38
|
/**
|
|
39
39
|
* Called when a tool executor updates its provider config (e.g. Spotify refresh token rotation).
|
|
40
|
-
* Injected by Processor
|
|
40
|
+
* Injected by Processor pointing to stateManager.queue_enqueue.
|
|
41
41
|
*/
|
|
42
42
|
onProviderConfigUpdate?: (providerId: string, updates: Record<string, string>) => void;
|
|
43
|
+
onUsageUpdate?: (modelId: string, usage: { calls: number; tokens_in: number; tokens_out: number }) => void;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export class QueueProcessor {
|
|
@@ -52,6 +53,7 @@ export class QueueProcessor {
|
|
|
52
53
|
private currentTools: ToolDefinition[] | undefined;
|
|
53
54
|
private currentOnEnqueue: EnqueueCallback | undefined;
|
|
54
55
|
private currentOnProviderConfigUpdate: ((providerId: string, updates: Record<string, string>) => void) | undefined;
|
|
56
|
+
private currentOnUsageUpdate: ((modelId: string, usage: { calls: number; tokens_in: number; tokens_out: number }) => void) | undefined;
|
|
55
57
|
|
|
56
58
|
getState(): QueueProcessorState {
|
|
57
59
|
return this.state;
|
|
@@ -70,6 +72,7 @@ export class QueueProcessor {
|
|
|
70
72
|
this.currentTools = options?.tools;
|
|
71
73
|
this.currentOnEnqueue = options?.onEnqueue;
|
|
72
74
|
this.currentOnProviderConfigUpdate = options?.onProviderConfigUpdate;
|
|
75
|
+
this.currentOnUsageUpdate = options?.onUsageUpdate;
|
|
73
76
|
this.abortController = new AbortController();
|
|
74
77
|
|
|
75
78
|
this.processRequest(request)
|
|
@@ -197,7 +200,7 @@ export class QueueProcessor {
|
|
|
197
200
|
hydratedUser,
|
|
198
201
|
messages,
|
|
199
202
|
request.model,
|
|
200
|
-
{ signal: this.abortController?.signal, tools: openAITools },
|
|
203
|
+
{ signal: this.abortController?.signal, tools: openAITools, onUsageUpdate: this.currentOnUsageUpdate },
|
|
201
204
|
this.currentAccounts
|
|
202
205
|
);
|
|
203
206
|
|
|
@@ -304,7 +307,7 @@ export class QueueProcessor {
|
|
|
304
307
|
hydratedUser,
|
|
305
308
|
messages,
|
|
306
309
|
request.model,
|
|
307
|
-
{ signal: this.abortController?.signal, tools: openAITools },
|
|
310
|
+
{ signal: this.abortController?.signal, tools: openAITools, onUsageUpdate: this.currentOnUsageUpdate },
|
|
308
311
|
this.currentAccounts
|
|
309
312
|
);
|
|
310
313
|
if (thinking) {
|
|
@@ -496,7 +499,7 @@ export class QueueProcessor {
|
|
|
496
499
|
reformatUserPrompt,
|
|
497
500
|
messages, // existing tool history — gives full context without duplicating the ask
|
|
498
501
|
request.model,
|
|
499
|
-
{ signal: this.abortController?.signal },
|
|
502
|
+
{ signal: this.abortController?.signal, onUsageUpdate: this.currentOnUsageUpdate },
|
|
500
503
|
this.currentAccounts
|
|
501
504
|
);
|
|
502
505
|
|
|
@@ -553,7 +556,7 @@ export class QueueProcessor {
|
|
|
553
556
|
reformatUserPrompt,
|
|
554
557
|
[], // no message history needed — schema is already in the system prompt
|
|
555
558
|
request.model,
|
|
556
|
-
{ signal: this.abortController?.signal },
|
|
559
|
+
{ signal: this.abortController?.signal, onUsageUpdate: this.currentOnUsageUpdate },
|
|
557
560
|
this.currentAccounts
|
|
558
561
|
);
|
|
559
562
|
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
RoomCreationInput,
|
|
19
19
|
} from "./types.js";
|
|
20
20
|
import { BUILT_IN_FACT_NAMES } from './constants/built-in-facts.js';
|
|
21
|
+
import type { ThemeDefinition } from './types/entities.js';
|
|
21
22
|
import type { Storage } from "../storage/interface.js";
|
|
22
23
|
import {
|
|
23
24
|
HumanState,
|
|
@@ -68,6 +69,7 @@ export class StateManager {
|
|
|
68
69
|
this.migrateInterestedPersonas();
|
|
69
70
|
this.migrateProviderModel();
|
|
70
71
|
this.migrateRoomMessageContent();
|
|
72
|
+
this.migrateThemes();
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
private migrateRoomMessageContent(): void {
|
|
@@ -566,6 +568,14 @@ export class StateManager {
|
|
|
566
568
|
this.persistenceState.scheduleSave(this.buildStorageState());
|
|
567
569
|
}
|
|
568
570
|
|
|
571
|
+
private migrateThemes(): void {
|
|
572
|
+
const human = this.humanState.get();
|
|
573
|
+
if (!human.settings) return;
|
|
574
|
+
if (human.settings.custom_themes !== undefined) return;
|
|
575
|
+
human.settings.custom_themes = [];
|
|
576
|
+
this.humanState.set(human);
|
|
577
|
+
}
|
|
578
|
+
|
|
569
579
|
getHuman(): HumanEntity {
|
|
570
580
|
return this.humanState.get();
|
|
571
581
|
}
|
|
@@ -575,6 +585,44 @@ export class StateManager {
|
|
|
575
585
|
this.scheduleSave();
|
|
576
586
|
}
|
|
577
587
|
|
|
588
|
+
human_theme_getActive(): string | undefined {
|
|
589
|
+
return this.getHuman().settings?.active_theme;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
human_theme_setActive(id: string | undefined): void {
|
|
593
|
+
const human = this.getHuman();
|
|
594
|
+
human.settings ??= {};
|
|
595
|
+
human.settings.active_theme = id;
|
|
596
|
+
this.setHuman(human);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
human_theme_getAll(): ThemeDefinition[] {
|
|
600
|
+
return this.getHuman().settings?.custom_themes ?? [];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
human_theme_upsert(theme: ThemeDefinition): void {
|
|
604
|
+
const human = this.getHuman();
|
|
605
|
+
human.settings ??= {};
|
|
606
|
+
human.settings.custom_themes ??= [];
|
|
607
|
+
const idx = human.settings.custom_themes.findIndex(t => t.id === theme.id);
|
|
608
|
+
if (idx >= 0) {
|
|
609
|
+
human.settings.custom_themes[idx] = theme;
|
|
610
|
+
} else {
|
|
611
|
+
human.settings.custom_themes.push(theme);
|
|
612
|
+
}
|
|
613
|
+
this.setHuman(human);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
human_theme_remove(id: string): boolean {
|
|
617
|
+
const human = this.getHuman();
|
|
618
|
+
const themes = human.settings?.custom_themes ?? [];
|
|
619
|
+
const idx = themes.findIndex(t => t.id === id);
|
|
620
|
+
if (idx < 0) return false;
|
|
621
|
+
themes.splice(idx, 1);
|
|
622
|
+
this.setHuman(human);
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
|
|
578
626
|
human_fact_upsert(fact: Fact): void {
|
|
579
627
|
this.humanState.fact_upsert(fact);
|
|
580
628
|
this.scheduleSave();
|
|
@@ -1064,6 +1112,24 @@ export class StateManager {
|
|
|
1064
1112
|
return { success: true, cleared };
|
|
1065
1113
|
}
|
|
1066
1114
|
|
|
1115
|
+
model_update_usage(modelId: string, delta: { calls: number; tokens_in: number; tokens_out: number }): void {
|
|
1116
|
+
const human = this.humanState.get();
|
|
1117
|
+
const accounts = human.settings?.accounts;
|
|
1118
|
+
if (!accounts) return;
|
|
1119
|
+
|
|
1120
|
+
for (const account of accounts) {
|
|
1121
|
+
const model = account.models?.find(m => m.id === modelId);
|
|
1122
|
+
if (model) {
|
|
1123
|
+
model.total_calls = (model.total_calls ?? 0) + delta.calls;
|
|
1124
|
+
model.total_tokens_in = (model.total_tokens_in ?? 0) + delta.tokens_in;
|
|
1125
|
+
model.total_tokens_out = (model.total_tokens_out ?? 0) + delta.tokens_out;
|
|
1126
|
+
model.last_used = new Date().toISOString();
|
|
1127
|
+
this.scheduleSave();
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1067
1133
|
async flush(): Promise<void> {
|
|
1068
1134
|
await this.persistenceState.flush();
|
|
1069
1135
|
}
|
|
@@ -57,7 +57,7 @@ export function createReadMemoryExecutor(searchHumanData: SearchHumanData, getPe
|
|
|
57
57
|
const output: Record<string, unknown[]> = {};
|
|
58
58
|
if (results.facts.length > 0) output.facts = results.facts.map(f => ({ name: f.name, description: f.description }));
|
|
59
59
|
if (results.topics.length > 0) output.topics = results.topics.map(t => ({ name: t.name, description: t.description }));
|
|
60
|
-
if (results.people.length > 0) output.people = results.people.map(p => ({ name: p.name, relationship: p.relationship, description: p.description }));
|
|
60
|
+
if (results.people.length > 0) output.people = results.people.map(p => ({ name: p.name, relationship: p.relationship, description: p.description, identifiers: p.identifiers ?? [] }));
|
|
61
61
|
if (results.quotes.length > 0) output.quotes = results.quotes.map(q => ({ text: q.text, speaker: q.speaker }));
|
|
62
62
|
|
|
63
63
|
if (Object.keys(output).length === 0) {
|
|
@@ -44,9 +44,11 @@ export interface BackupConfig {
|
|
|
44
44
|
*/
|
|
45
45
|
export interface ModelConfig {
|
|
46
46
|
id: string; // GUID (crypto.randomUUID())
|
|
47
|
-
name: string; //
|
|
47
|
+
name: string; // Display name shown in UI, e.g. "Gemma4 (thinking)", "(default)"
|
|
48
|
+
model_id?: string; // Actual model identifier sent to API — falls back to name if absent
|
|
48
49
|
token_limit?: number; // Input token limit (user sets effective limit)
|
|
49
50
|
max_output_tokens?: number; // Output token limit (API-enforced)
|
|
51
|
+
thinking_budget?: number; // Thinking token budget: 0 = disabled, N = enable with N tokens, undefined = don't send
|
|
50
52
|
total_calls?: number; // Usage counter
|
|
51
53
|
total_tokens_in?: number; // Usage counter
|
|
52
54
|
total_tokens_out?: number; // Usage counter
|
|
@@ -89,6 +91,14 @@ export interface ProviderAccount {
|
|
|
89
91
|
created_at: string; // ISO timestamp
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
export interface ThemeDefinition {
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
base?: string;
|
|
98
|
+
encoded: string;
|
|
99
|
+
created_at: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
92
102
|
export interface HumanSettings {
|
|
93
103
|
default_model?: string; // Will store ModelConfig.id GUID post-migration
|
|
94
104
|
oneshot_model?: string; // Model for AI-assist (wand) requests; falls back to default_model. Will store ModelConfig.id GUID post-migration.
|
|
@@ -109,6 +119,8 @@ export interface HumanSettings {
|
|
|
109
119
|
backup?: BackupConfig;
|
|
110
120
|
claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
|
|
111
121
|
cursor?: import("../../integrations/cursor/types.js").CursorSettings;
|
|
122
|
+
active_theme?: string;
|
|
123
|
+
custom_themes?: ThemeDefinition[];
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
export interface HumanEntity {
|
|
@@ -150,6 +162,9 @@ export interface PersonaEntity {
|
|
|
150
162
|
tools?: string[]; // IDs of ToolDefinitions this persona can use. Empty/absent = no tool access.
|
|
151
163
|
reflection_last_asked?: string; // ISO timestamp. Set ONLY when Persona explicitly surfaces identity drift (mentioned_reflection: true).
|
|
152
164
|
description_embedding?: number[]; // Embedding of long_description (short_description fallback). Excludes traits. See embedding-service.ts:getPersonaDescriptionText.
|
|
165
|
+
avatar_emoji?: string; // Single emoji character used as avatar in place of initials.
|
|
166
|
+
avatar_image?: string; // Base64-encoded 64×64 image used as avatar (takes priority over avatar_emoji).
|
|
167
|
+
preferred_theme?: string; // Theme ID (built-in name or ThemeDefinition.id). Applied to chat panel when this persona is active.
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
export interface PersonaCreationInput {
|
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import type { PersonIdentifier } from "../types/data-items.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
|
+
import { BUILT_IN_IDENTIFIER_TYPES } from "../constants/built-in-identifier-types.js";
|
|
3
4
|
|
|
4
5
|
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
|
|
|
7
|
+
function toNormalizedKey(s: string): string {
|
|
8
|
+
return s.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Fuzzy-matches LLM-provided type against built-in + in-use types (strip non-alphanumeric, lowercase).
|
|
12
|
+
// "nickname" -> "Nickname", "full_name" -> "Full Name", "EMAIL" -> "Email", "Slack RNP" -> "Slack RNP" (custom, no match)
|
|
13
|
+
export function normalizeIdentifierType(llmType: string, state: StateManager): string {
|
|
14
|
+
const inUseTypes = state.getHuman().people.flatMap(p =>
|
|
15
|
+
(p.identifiers ?? []).map(i => i.type)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const canonicalMap = new Map<string, string>();
|
|
19
|
+
for (const t of [...BUILT_IN_IDENTIFIER_TYPES, ...inUseTypes]) {
|
|
20
|
+
const key = toNormalizedKey(t);
|
|
21
|
+
if (!canonicalMap.has(key)) {
|
|
22
|
+
canonicalMap.set(key, t);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalized = toNormalizedKey(llmType);
|
|
27
|
+
return canonicalMap.get(normalized) ?? llmType;
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
export function sanitizeEiPersonaIdentifiers(
|
|
7
31
|
identifiers: PersonIdentifier[],
|
|
8
32
|
state: StateManager
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ThemeDefinition } from "../types/entities.js";
|
|
2
|
+
|
|
3
|
+
const VERSION = "v1";
|
|
4
|
+
const PREFIX = `ei-theme:${VERSION}:`;
|
|
5
|
+
const TOKEN_COUNT = 37;
|
|
6
|
+
const HEX_LENGTH = 6;
|
|
7
|
+
|
|
8
|
+
export const THEME_TOKEN_ORDER: readonly string[] = [
|
|
9
|
+
"bg-primary", "bg-secondary", "bg-tertiary",
|
|
10
|
+
"border", "border-light",
|
|
11
|
+
"text-primary", "text-secondary", "text-muted",
|
|
12
|
+
"accent", "accent-hover",
|
|
13
|
+
"success", "success-hover",
|
|
14
|
+
"warning", "warning-text",
|
|
15
|
+
"danger",
|
|
16
|
+
"status-thinking", "status-ready", "status-unread", "status-paused",
|
|
17
|
+
"room-cyp", "room-ffa", "room-map",
|
|
18
|
+
"archive-bg-start", "archive-bg-end", "archive-border",
|
|
19
|
+
"ai-assist-start", "ai-assist-end",
|
|
20
|
+
"code-bg", "code-bg-controls", "code-border",
|
|
21
|
+
"code-text", "code-text-muted",
|
|
22
|
+
"code-accent", "code-string", "code-error", "code-success", "code-special",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export const BUILT_IN_THEME_NAMES: readonly string[] = [
|
|
26
|
+
"default", "dark", "coder", "depressing", "cotton-candy",
|
|
27
|
+
"crimuh", "spoopy", "lovey-dovey", "lucky",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export type ThemeTokenMap = Record<string, string>;
|
|
31
|
+
|
|
32
|
+
export function encodeTheme(tokens: ThemeTokenMap): string {
|
|
33
|
+
const hex = THEME_TOKEN_ORDER.map((key) => {
|
|
34
|
+
const value = tokens[`--ei-${key}`] ?? tokens[key] ?? "000000";
|
|
35
|
+
return value.replace(/^#/, "").toLowerCase().padEnd(HEX_LENGTH, "0").slice(0, HEX_LENGTH);
|
|
36
|
+
}).join("");
|
|
37
|
+
return PREFIX + btoa(hex);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function decodeTheme(encoded: string): ThemeTokenMap | null {
|
|
41
|
+
if (!encoded.startsWith(PREFIX)) return null;
|
|
42
|
+
try {
|
|
43
|
+
const hex = atob(encoded.slice(PREFIX.length));
|
|
44
|
+
if (hex.length !== TOKEN_COUNT * HEX_LENGTH) return null;
|
|
45
|
+
const tokens: ThemeTokenMap = {};
|
|
46
|
+
for (let i = 0; i < TOKEN_COUNT; i++) {
|
|
47
|
+
const key = THEME_TOKEN_ORDER[i];
|
|
48
|
+
tokens[`--ei-${key}`] = `#${hex.slice(i * HEX_LENGTH, (i + 1) * HEX_LENGTH)}`;
|
|
49
|
+
}
|
|
50
|
+
return tokens;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function themeToStyleString(tokens: ThemeTokenMap): string {
|
|
57
|
+
return Object.entries(tokens)
|
|
58
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
59
|
+
.join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isBuiltInTheme(id: string): boolean {
|
|
63
|
+
return (BUILT_IN_THEME_NAMES as readonly string[]).includes(id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function makeThemeDefinition(
|
|
67
|
+
name: string,
|
|
68
|
+
tokens: ThemeTokenMap,
|
|
69
|
+
base?: string,
|
|
70
|
+
): ThemeDefinition {
|
|
71
|
+
return {
|
|
72
|
+
id: crypto.randomUUID(),
|
|
73
|
+
name,
|
|
74
|
+
base,
|
|
75
|
+
encoded: encodeTheme(tokens),
|
|
76
|
+
created_at: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
}
|
package/tui/src/commands/me.tsx
CHANGED
|
@@ -103,7 +103,8 @@ export const meCommand: Command = {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
try {
|
|
106
|
-
const
|
|
106
|
+
const currentHuman = await ctx.ei.getHuman();
|
|
107
|
+
const parsed = humanFromYAML(result.content, filteredHuman, currentHuman);
|
|
107
108
|
|
|
108
109
|
for (const id of parsed.deletedFactIds) {
|
|
109
110
|
await ctx.ei.removeDataItem("fact", id);
|
|
@@ -131,14 +132,20 @@ export const meCommand: Command = {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
const deleteCount = parsed.deletedFactIds.length +
|
|
135
|
-
parsed.deletedTopicIds.length +
|
|
135
|
+
const deleteCount = parsed.deletedFactIds.length +
|
|
136
|
+
parsed.deletedTopicIds.length +
|
|
136
137
|
parsed.deletedPersonIds.length;
|
|
137
|
-
const updateCount = parsed.changedFactIds.size +
|
|
138
|
-
parsed.changedTopicIds.size +
|
|
138
|
+
const updateCount = parsed.changedFactIds.size +
|
|
139
|
+
parsed.changedTopicIds.size +
|
|
139
140
|
parsed.changedPersonIds.size;
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
const skippedCount = parsed.skippedFactCount +
|
|
142
|
+
parsed.skippedTopicCount +
|
|
143
|
+
parsed.skippedPersonCount;
|
|
144
|
+
|
|
145
|
+
const msg = skippedCount > 0
|
|
146
|
+
? `Updated ${updateCount}, deleted ${deleteCount}, skipped ${skippedCount} (changed by another process)`
|
|
147
|
+
: `Updated ${updateCount} items, deleted ${deleteCount}`;
|
|
148
|
+
ctx.showNotification(msg, "info");
|
|
142
149
|
return;
|
|
143
150
|
|
|
144
151
|
} catch (parseError) {
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { Processor } from "../../../src/core/processor.js";
|
|
|
14
14
|
import { FileStorage } from "../storage/file.js";
|
|
15
15
|
import { remoteSync } from "../../../src/storage/remote.js";
|
|
16
16
|
import { logger, clearLog, interceptConsole } from "../util/logger.js";
|
|
17
|
+
import { E2E_SKIP_LOCAL_DETECT } from "../util/e2e-flags.js";
|
|
17
18
|
import { ConflictOverlay } from "../components/ConflictOverlay.js";
|
|
18
19
|
import type {
|
|
19
20
|
Ei_Interface,
|
|
@@ -698,7 +699,10 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
698
699
|
try {
|
|
699
700
|
const human = await processor!.getHuman();
|
|
700
701
|
const hasAccounts = human.settings?.accounts && human.settings.accounts.length > 0;
|
|
701
|
-
if (!hasAccounts) {
|
|
702
|
+
if (!hasAccounts && E2E_SKIP_LOCAL_DETECT) {
|
|
703
|
+
logger.info("E2E_SKIP_LOCAL_DETECT active, skipping local LLM check");
|
|
704
|
+
setShowWelcomeOverlay(true);
|
|
705
|
+
} else if (!hasAccounts) {
|
|
702
706
|
logger.info("No LLM accounts configured, checking for local LLM...");
|
|
703
707
|
try {
|
|
704
708
|
const response = await fetch("http://127.0.0.1:1234/v1/models", {
|
|
@@ -707,6 +711,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
707
711
|
});
|
|
708
712
|
if (response.ok) {
|
|
709
713
|
logger.info("Local LLM detected, auto-configuring...");
|
|
714
|
+
const defaultModelId = crypto.randomUUID();
|
|
710
715
|
const localAccount: ProviderAccount = {
|
|
711
716
|
id: crypto.randomUUID(),
|
|
712
717
|
name: "Local LLM",
|
|
@@ -714,13 +719,15 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
714
719
|
url: "http://127.0.0.1:1234/v1",
|
|
715
720
|
enabled: true,
|
|
716
721
|
created_at: new Date().toISOString(),
|
|
722
|
+
default_model: defaultModelId,
|
|
723
|
+
models: [{ id: defaultModelId, name: "(default)" }],
|
|
717
724
|
};
|
|
718
725
|
const currentHuman = await processor!.getHuman();
|
|
719
726
|
await processor!.updateHuman({
|
|
720
727
|
settings: {
|
|
721
728
|
...currentHuman.settings,
|
|
722
729
|
accounts: [localAccount],
|
|
723
|
-
default_model:
|
|
730
|
+
default_model: defaultModelId,
|
|
724
731
|
},
|
|
725
732
|
});
|
|
726
733
|
showNotification("Local LLM detected and configured!", "info");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EI_E2E_MODE — bitfield for test seams that can't be solved via data seeding.
|
|
3
|
+
*
|
|
4
|
+
* Use prime-power bits so combinations are unambiguous:
|
|
5
|
+
* 1 — skip local LLM auto-detect (fetch to :1234/:11434)
|
|
6
|
+
* 2 — (reserved for next scenario)
|
|
7
|
+
* 3 — flags 1 + 2 combined
|
|
8
|
+
*
|
|
9
|
+
* Production code should never set this. Tests pass it via env in test.use({ env: { EI_E2E_MODE: "1" } }).
|
|
10
|
+
*/
|
|
11
|
+
const E2E_MODE = parseInt(process.env.EI_E2E_MODE ?? "0", 10);
|
|
12
|
+
|
|
13
|
+
export const E2E_SKIP_LOCAL_DETECT = (E2E_MODE & 1) !== 0;
|
|
@@ -225,6 +225,9 @@ export interface HumanYAMLResult {
|
|
|
225
225
|
changedFactIds: Set<string>;
|
|
226
226
|
changedTopicIds: Set<string>;
|
|
227
227
|
changedPersonIds: Set<string>;
|
|
228
|
+
skippedFactCount: number;
|
|
229
|
+
skippedTopicCount: number;
|
|
230
|
+
skippedPersonCount: number;
|
|
228
231
|
}
|
|
229
232
|
|
|
230
233
|
function identifiersEqual(a: PersonIdentifier[] | undefined, b: PersonIdentifier[] | undefined): boolean {
|
|
@@ -279,7 +282,7 @@ function personChanged(parsed: Person, original: Person): boolean {
|
|
|
279
282
|
return !identifiersEqual(parsed.identifiers, original.identifiers);
|
|
280
283
|
}
|
|
281
284
|
|
|
282
|
-
export function humanFromYAML(yamlContent: string, original?: HumanEntity): HumanYAMLResult {
|
|
285
|
+
export function humanFromYAML(yamlContent: string, original?: HumanEntity, current?: HumanEntity): HumanYAMLResult {
|
|
283
286
|
const stripped = yamlContent
|
|
284
287
|
.split('\n')
|
|
285
288
|
.filter(line => !/^\s*#\s*\[read-only\]/.test(line))
|
|
@@ -292,6 +295,15 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
292
295
|
const changedFactIds = new Set<string>();
|
|
293
296
|
const changedTopicIds = new Set<string>();
|
|
294
297
|
const changedPersonIds = new Set<string>();
|
|
298
|
+
let skippedFactCount = 0;
|
|
299
|
+
let skippedTopicCount = 0;
|
|
300
|
+
let skippedPersonCount = 0;
|
|
301
|
+
|
|
302
|
+
const staleInState = (id: string | undefined, originalItem: { last_updated: string } | undefined, currentItems: { id: string; last_updated: string }[] | undefined): boolean => {
|
|
303
|
+
if (!id || !originalItem || !current || !currentItems) return false;
|
|
304
|
+
const currentItem = currentItems.find(i => i.id === id);
|
|
305
|
+
return !!currentItem && currentItem.last_updated !== originalItem.last_updated;
|
|
306
|
+
};
|
|
295
307
|
|
|
296
308
|
const facts: Fact[] = [];
|
|
297
309
|
for (const f of data.facts ?? []) {
|
|
@@ -306,10 +318,14 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
306
318
|
: { ...parsed, last_updated: new Date().toISOString(), persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
307
319
|
facts.push(fact);
|
|
308
320
|
if (!originalFact || factChanged(fact, originalFact)) {
|
|
309
|
-
if (
|
|
310
|
-
|
|
321
|
+
if (staleInState(parsed.id, originalFact, current?.facts)) {
|
|
322
|
+
skippedFactCount++;
|
|
323
|
+
} else {
|
|
324
|
+
if (fact.description && !originalFact?.validated_date) {
|
|
325
|
+
fact.validated_date = new Date().toISOString();
|
|
326
|
+
}
|
|
327
|
+
changedFactIds.add(fact.id);
|
|
311
328
|
}
|
|
312
|
-
changedFactIds.add(fact.id);
|
|
313
329
|
}
|
|
314
330
|
}
|
|
315
331
|
}
|
|
@@ -327,7 +343,11 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
327
343
|
: { ...parsed, last_updated: new Date().toISOString(), persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
328
344
|
topics.push(topic);
|
|
329
345
|
if (!originalTopic || topicChanged(topic, originalTopic)) {
|
|
330
|
-
|
|
346
|
+
if (staleInState(parsed.id, originalTopic, current?.topics)) {
|
|
347
|
+
skippedTopicCount++;
|
|
348
|
+
} else {
|
|
349
|
+
changedTopicIds.add(topic.id);
|
|
350
|
+
}
|
|
331
351
|
}
|
|
332
352
|
}
|
|
333
353
|
}
|
|
@@ -350,7 +370,11 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
350
370
|
: { ...parsed, last_updated: new Date().toISOString(), identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
351
371
|
people.push(person);
|
|
352
372
|
if (!originalPerson || personChanged(person, originalPerson)) {
|
|
353
|
-
|
|
373
|
+
if (staleInState(parsed.id, originalPerson, current?.people)) {
|
|
374
|
+
skippedPersonCount++;
|
|
375
|
+
} else {
|
|
376
|
+
changedPersonIds.add(person.id);
|
|
377
|
+
}
|
|
354
378
|
}
|
|
355
379
|
}
|
|
356
380
|
}
|
|
@@ -365,5 +389,8 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
365
389
|
changedFactIds,
|
|
366
390
|
changedTopicIds,
|
|
367
391
|
changedPersonIds,
|
|
392
|
+
skippedFactCount,
|
|
393
|
+
skippedTopicCount,
|
|
394
|
+
skippedPersonCount,
|
|
368
395
|
};
|
|
369
396
|
}
|
|
@@ -5,10 +5,15 @@ import type {
|
|
|
5
5
|
} from "../../../src/core/types.js";
|
|
6
6
|
import { modelGuidToDisplay } from "./yaml-shared.js";
|
|
7
7
|
|
|
8
|
+
const tokenFormatter = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
|
|
9
|
+
const formatTokens = (n: number) => tokenFormatter.format(n);
|
|
10
|
+
|
|
8
11
|
interface EditableModelData {
|
|
9
12
|
name: string;
|
|
13
|
+
model_id?: string;
|
|
10
14
|
token_limit?: number;
|
|
11
15
|
max_output_tokens?: number;
|
|
16
|
+
thinking_budget?: number;
|
|
12
17
|
_delete?: boolean;
|
|
13
18
|
}
|
|
14
19
|
|
|
@@ -45,11 +50,14 @@ function parseModels(editableModels: EditableModelData[]): import('../../../src/
|
|
|
45
50
|
const result: import('../../../src/core/types.js').ModelConfig[] = [];
|
|
46
51
|
for (const m of editableModels) {
|
|
47
52
|
if (m._delete) continue;
|
|
53
|
+
const modelId = m.model_id ?? undefined;
|
|
48
54
|
result.push({
|
|
49
55
|
id: crypto.randomUUID(),
|
|
50
56
|
name: m.name,
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
model_id: (modelId === null || modelId === m.name) ? undefined : modelId,
|
|
58
|
+
token_limit: m.token_limit ?? undefined,
|
|
59
|
+
max_output_tokens: m.max_output_tokens ?? undefined,
|
|
60
|
+
thinking_budget: m.thinking_budget ?? undefined,
|
|
53
61
|
});
|
|
54
62
|
}
|
|
55
63
|
return result;
|
|
@@ -70,6 +78,10 @@ export function newProviderToYAML(name?: string): string {
|
|
|
70
78
|
const modelsYAML = [
|
|
71
79
|
"models:",
|
|
72
80
|
" - name: (default)",
|
|
81
|
+
" model_id: (default)",
|
|
82
|
+
" token_limit: null",
|
|
83
|
+
" max_output_tokens: null",
|
|
84
|
+
" thinking_budget: null",
|
|
73
85
|
" # _delete: true",
|
|
74
86
|
"# _delete: true # Delete this entire provider",
|
|
75
87
|
].join("\n");
|
|
@@ -141,16 +153,26 @@ export function providerToYAML(account: ProviderAccount): string {
|
|
|
141
153
|
if (modelList.length > 0) {
|
|
142
154
|
for (const m of modelList) {
|
|
143
155
|
modelLines.push(` - name: ${m.name}`);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
modelLines.push(` model_id: ${m.model_id ?? m.name}`);
|
|
157
|
+
modelLines.push(` token_limit: ${m.token_limit ?? null}`);
|
|
158
|
+
modelLines.push(` max_output_tokens: ${m.max_output_tokens ?? null}`);
|
|
159
|
+
modelLines.push(` thinking_budget: ${m.thinking_budget ?? null}`);
|
|
160
|
+
if (m.total_calls !== undefined || m.total_tokens_in !== undefined) {
|
|
161
|
+
const tokensIn = m.total_tokens_in ?? 0;
|
|
162
|
+
const tokensOut = m.total_tokens_out ?? 0;
|
|
163
|
+
modelLines.push(` # stats: ${formatTokens(m.total_calls ?? 0)} calls · ${formatTokens(tokensIn)} in / ${formatTokens(tokensOut)} out`);
|
|
164
|
+
if (m.last_used) {
|
|
165
|
+
modelLines.push(` # used: ${m.last_used}`);
|
|
166
|
+
}
|
|
149
167
|
}
|
|
150
168
|
modelLines.push(` _delete: false`);
|
|
151
169
|
}
|
|
152
170
|
} else {
|
|
153
171
|
modelLines.push(" - name: (default)");
|
|
172
|
+
modelLines.push(` model_id: (default)`);
|
|
173
|
+
modelLines.push(` token_limit: null`);
|
|
174
|
+
modelLines.push(` max_output_tokens: null`);
|
|
175
|
+
modelLines.push(` thinking_budget: null`);
|
|
154
176
|
modelLines.push(" _delete: false");
|
|
155
177
|
}
|
|
156
178
|
modelLines.push("_delete: false # Set to true to delete this entire provider");
|
|
@@ -185,11 +207,14 @@ export function providerFromYAML(yamlContent: string, original: ProviderAccount)
|
|
|
185
207
|
for (const m of data.models ?? []) {
|
|
186
208
|
if (m._delete) continue;
|
|
187
209
|
const existing = existingModels.find(em => em.name === m.name);
|
|
210
|
+
const modelId = m.model_id ?? undefined;
|
|
188
211
|
parsedModels.push({
|
|
189
212
|
id: existing?.id ?? crypto.randomUUID(),
|
|
190
213
|
name: m.name,
|
|
191
|
-
|
|
192
|
-
|
|
214
|
+
model_id: (modelId === null || modelId === m.name) ? undefined : modelId,
|
|
215
|
+
token_limit: m.token_limit ?? undefined,
|
|
216
|
+
max_output_tokens: m.max_output_tokens ?? undefined,
|
|
217
|
+
thinking_budget: m.thinking_budget ?? undefined,
|
|
193
218
|
total_calls: existing?.total_calls,
|
|
194
219
|
total_tokens_in: existing?.total_tokens_in,
|
|
195
220
|
total_tokens_out: existing?.total_tokens_out,
|