ei-tui 0.9.3 → 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.
Files changed (52) hide show
  1. package/package.json +4 -1
  2. package/src/README.md +1 -1
  3. package/src/core/context-utils.ts +2 -2
  4. package/src/core/handlers/heartbeat.ts +9 -1
  5. package/src/core/handlers/human-extraction.ts +4 -1
  6. package/src/core/handlers/human-matching.ts +5 -53
  7. package/src/core/handlers/index.ts +1 -51
  8. package/src/core/handlers/persona-generation.ts +1 -28
  9. package/src/core/handlers/utils.ts +2 -9
  10. package/src/core/heartbeat-manager.ts +3 -3
  11. package/src/core/message-manager.ts +6 -5
  12. package/src/core/orchestrators/ceremony.ts +4 -9
  13. package/src/core/orchestrators/extraction-chunker.ts +3 -3
  14. package/src/core/orchestrators/human-extraction.ts +17 -17
  15. package/src/core/orchestrators/index.ts +0 -1
  16. package/src/core/orchestrators/persona-topics.ts +1 -1
  17. package/src/core/orchestrators/room-extraction.ts +5 -5
  18. package/src/core/processor.ts +8 -21
  19. package/src/core/prompt-context-builder.ts +7 -6
  20. package/src/core/state/personas.ts +1 -17
  21. package/src/core/state-manager.ts +0 -66
  22. package/src/core/types/entities.ts +2 -3
  23. package/src/core/types/enums.ts +0 -2
  24. package/src/core/types/rooms.ts +1 -1
  25. package/src/integrations/claude-code/importer.ts +1 -1
  26. package/src/integrations/cursor/importer.ts +1 -1
  27. package/src/integrations/opencode/importer.ts +1 -1
  28. package/src/prompts/ceremony/index.ts +0 -10
  29. package/src/prompts/ceremony/types.ts +1 -42
  30. package/src/prompts/generation/index.ts +0 -3
  31. package/src/prompts/generation/types.ts +0 -15
  32. package/src/prompts/heartbeat/check.ts +18 -6
  33. package/src/prompts/heartbeat/types.ts +2 -1
  34. package/src/prompts/human/index.ts +0 -2
  35. package/src/prompts/human/types.ts +0 -16
  36. package/src/prompts/index.ts +0 -19
  37. package/src/prompts/reflection/index.ts +35 -5
  38. package/src/prompts/reflection/types.ts +1 -1
  39. package/src/prompts/response/index.ts +5 -0
  40. package/src/prompts/response/sections.ts +26 -0
  41. package/src/prompts/response/types.ts +3 -0
  42. package/tui/src/commands/registry.test.ts +10 -5
  43. package/tui/src/globals.d.ts +57 -0
  44. package/tui/src/util/yaml-persona.ts +8 -4
  45. package/tui/src/util/yaml-settings.ts +3 -3
  46. package/src/core/orchestrators/person-migration.ts +0 -55
  47. package/src/prompts/ceremony/description-check.ts +0 -54
  48. package/src/prompts/ceremony/expire.ts +0 -37
  49. package/src/prompts/ceremony/explore.ts +0 -77
  50. package/src/prompts/ceremony/person-migration.ts +0 -77
  51. package/src/prompts/generation/descriptions.ts +0 -91
  52. package/src/prompts/human/fact-scan.ts +0 -150
@@ -20,7 +20,7 @@ export interface ReflectionCriticResult {
20
20
  short_description: string;
21
21
  traits: PersonaTrait[];
22
22
  topics: PersonaTopic[];
23
- };
23
+ } | null;
24
24
  }
25
25
 
26
26
 
@@ -23,6 +23,7 @@ import {
23
23
  buildResponseFormatSection,
24
24
  buildToolsSection,
25
25
  buildTemporalAnchorsSection,
26
+ buildPendingUpdateSection,
26
27
  } from "./sections.js";
27
28
 
28
29
  export type { ResponsePromptData, PromptOutput, PersonaResponseResult } from "./types.js";
@@ -46,6 +47,7 @@ Your role is unique among personas:
46
47
  const guidelines = buildGuidelinesSection("ei");
47
48
  const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
48
49
  const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
50
+ const pendingUpdate = data.persona.pending_update ? buildPendingUpdateSection(data.persona.pending_update) : "";
49
51
  const temporalAnchors = buildTemporalAnchorsSection(data.temporal_anchors, data.human.name);
50
52
  const humanSection = buildHumanSection(data.human);
51
53
  const quotesSection = buildQuotesSection(data.human.quotes, data.human);
@@ -67,6 +69,7 @@ ${guidelines}
67
69
  ${yourTraits}
68
70
 
69
71
  ${yourTopics}
72
+ ${pendingUpdate ? `\n${pendingUpdate}` : ""}
70
73
  ${temporalAnchors ? `\n${temporalAnchors}` : ""}
71
74
  ${humanSection}
72
75
  ${quotesSection}
@@ -93,6 +96,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
93
96
  const guidelines = buildGuidelinesSection(data.persona.name);
94
97
  const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
95
98
  const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
99
+ const pendingUpdate = data.persona.pending_update ? buildPendingUpdateSection(data.persona.pending_update) : "";
96
100
  const temporalAnchors = buildTemporalAnchorsSection(data.temporal_anchors, data.human.name);
97
101
  const humanSection = buildHumanSection(data.human);
98
102
  const quotesSection = buildQuotesSection(data.human.quotes, data.human);
@@ -113,6 +117,7 @@ ${guidelines}
113
117
  ${yourTraits}
114
118
 
115
119
  ${yourTopics}
120
+ ${pendingUpdate ? `\n${pendingUpdate}` : ""}
116
121
  ${temporalAnchors ? `\n${temporalAnchors}` : ""}
117
122
  ${humanSection}
118
123
  ${quotesSection}
@@ -137,6 +137,32 @@ ${formatted}
137
137
  `;
138
138
  }
139
139
 
140
+ // =============================================================================
141
+ // PENDING UPDATE SECTION
142
+ // =============================================================================
143
+
144
+ export function buildPendingUpdateSection(pending_update: NonNullable<import("./types.js").ResponsePromptData["persona"]["pending_update"]>): string {
145
+ const descriptionPart = pending_update.long_description || pending_update.short_description
146
+ ? `### Proposed Description\n${pending_update.long_description || pending_update.short_description}`
147
+ : "";
148
+
149
+ const traitsPart = pending_update.traits.length > 0
150
+ ? `### Proposed Traits\n${pending_update.traits.map(t => `- **${t.name}**: ${t.description}`).join("\n")}`
151
+ : "";
152
+
153
+ const topicsPart = pending_update.topics.length > 0
154
+ ? `### Proposed Interests\n${pending_update.topics.map(t => `- **${t.name}**: ${t.perspective}`).join("\n")}`
155
+ : "";
156
+
157
+ const parts = [descriptionPart, traitsPart, topicsPart].filter(Boolean).join("\n\n");
158
+
159
+ return `## Pending Identity Changes
160
+
161
+ Your human is reviewing proposed updates to your identity. This is yours to be aware of — bring it up if it feels right, or let it sit. Either way, these changes are waiting:
162
+
163
+ ${parts}`;
164
+ }
165
+
140
166
  // =============================================================================
141
167
  // HUMAN SECTION
142
168
  // =============================================================================
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic } from "../../core/types.js";
7
7
  import type { ToolDefinition } from "../../core/types.js";
8
+ import type { PersonaEntity } from "../../core/types/entities.js";
8
9
 
9
10
  export interface TemporalAnchor {
10
11
  role: "human" | "system";
@@ -29,6 +30,8 @@ export interface ResponsePromptData {
29
30
  interested_topics: PersonaTopic[];
30
31
  /** When true, each message has a timestamp prepended; include a note so the persona doesn't echo them */
31
32
  include_message_timestamps?: boolean;
33
+ /** Proposed identity revision pending human review. Persona carries this as ambient self-awareness — no critique, just the proposed changes. */
34
+ pending_update?: PersonaEntity["pending_update"];
32
35
  };
33
36
  human: {
34
37
  name: string;
@@ -1,5 +1,7 @@
1
1
  import { test, expect, describe, beforeEach, mock } from "bun:test";
2
2
  import { parseCommandLine, registerCommand, parseAndExecute, getAllCommands, type Command, type CommandContext } from "./registry";
3
+ import { type EiContextValue } from "../context/ei";
4
+ import type { CliRenderer } from "@opentui/core";
3
5
 
4
6
  describe("parseCommandLine", () => {
5
7
  test("parses simple command", () => {
@@ -67,13 +69,16 @@ describe("parseAndExecute", () => {
67
69
  showOverlay: mock(() => {}),
68
70
  hideOverlay: mock(() => {}),
69
71
  showNotification: mock(() => {}),
70
- exitApp: mock(() => {}),
72
+ exitApp: mock(async () => {}),
71
73
  stopProcessor: mock(async () => {}),
74
+ renderer: {} as unknown as CliRenderer,
75
+ setInputText: () => {},
76
+ getInputText: () => "",
72
77
  ei: {
73
78
  personas: () => [],
74
- activePersona: () => null,
79
+ activePersonaId: () => null,
75
80
  messages: () => [],
76
- queueStatus: () => ({ state: "idle" as const, pending_count: 0 }),
81
+ queueStatus: () => ({ state: "idle" as const, pending_count: 0, dlq_count: 0 }),
77
82
  notification: () => null,
78
83
  selectPersona: () => {},
79
84
  sendMessage: async () => {},
@@ -83,12 +88,12 @@ describe("parseAndExecute", () => {
83
88
  resumeQueue: async () => {},
84
89
  stopProcessor: async () => {},
85
90
  showNotification: () => {},
86
- createPersona: async () => {},
91
+ createPersona: async (_input: { name: string }) => "",
87
92
  archivePersona: async () => {},
88
93
  unarchivePersona: async () => {},
89
94
  setContextBoundary: async () => {},
90
95
  updatePersona: async () => {},
91
- },
96
+ } as unknown as EiContextValue,
92
97
  };
93
98
 
94
99
  beforeEach(() => {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * TUI global type augmentations.
3
+ *
4
+ * TUI's lib config omits "DOM" because this is a terminal app, but the shared
5
+ * src files use `typeof document !== "undefined"` guards for runtime detection.
6
+ * Without a DOM lib declaration, TypeScript emits TS2584 for those checks.
7
+ *
8
+ * Additionally, @types/node pulls in undici-types which types Response.json()
9
+ * as Promise<unknown>, while the DOM lib types it as Promise<any>. The stricter
10
+ * undici typing causes TS18046 in shared src files that call response.json().
11
+ */
12
+
13
+ // Allow `typeof document` checks in shared src files (never actually accessed in TUI)
14
+ declare var document: unknown;
15
+
16
+ // Override undici-types' strict Response.json() -> Promise<unknown> back to any,
17
+ // matching the DOM lib behavior that the shared src files were written against.
18
+ interface Body {
19
+ json(): Promise<any>;
20
+ }
21
+
22
+ interface Response {
23
+ json(): Promise<any>;
24
+ }
25
+
26
+ // Stubs for browser IndexedDB globals referenced in src/storage/indexed.ts.
27
+ // That file is browser-only but is re-exported from src/storage/index.ts,
28
+ // so TypeScript compiles it even when run under the TUI (no DOM lib).
29
+ declare var indexedDB: { open(name: string, version?: number): IDBOpenDBRequest };
30
+
31
+ declare class IDBDatabase {
32
+ objectStoreNames: { contains(name: string): boolean };
33
+ createObjectStore(name: string): unknown;
34
+ transaction(store: string, mode?: string): { objectStore(name: string): IDBObjectStore };
35
+ close(): void;
36
+ }
37
+
38
+ declare class IDBObjectStore {
39
+ get(key: string): IDBRequest;
40
+ put(value: unknown, key?: string): IDBRequest;
41
+ delete(key: string): IDBRequest;
42
+ }
43
+
44
+ declare class IDBOpenDBRequest {
45
+ result: IDBDatabase;
46
+ error: unknown;
47
+ onupgradeneeded: ((event: any) => void) | null;
48
+ onsuccess: ((event: any) => void) | null;
49
+ onerror: ((event: any) => void) | null;
50
+ }
51
+
52
+ declare class IDBRequest {
53
+ result: any;
54
+ error: unknown;
55
+ onsuccess: ((event: any) => void) | null;
56
+ onerror: ((event: any) => void) | null;
57
+ }
@@ -38,7 +38,7 @@ interface EditablePersonaData {
38
38
  traits: YAMLTrait[];
39
39
  topics: YAMLPersonaTopic[];
40
40
  heartbeat_delay_ms?: string | null;
41
- context_window_hours?: number | null;
41
+ context_window_ms?: string | null;
42
42
  is_paused?: boolean;
43
43
  pause_until?: string;
44
44
  is_static?: boolean;
@@ -179,7 +179,9 @@ export function newPersonaFromYAML(yamlContent: string, allTools?: ToolDefinitio
179
179
  heartbeat_delay_ms: data.heartbeat_delay_ms == null
180
180
  ? undefined
181
181
  : parseDuration(data.heartbeat_delay_ms) ?? undefined,
182
- context_window_hours: data.context_window_hours ?? undefined,
182
+ context_window_ms: data.context_window_ms == null
183
+ ? undefined
184
+ : parseDuration(data.context_window_ms) ?? undefined,
183
185
  tools: resolvePersonaToolsFromMap(data.tools, allTools ?? [], allProviders ?? []),
184
186
  };
185
187
  }
@@ -218,7 +220,7 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allT
218
220
  name, perspective, approach, personal_stake, sentiment: sentiment ?? 0, exposure_current, exposure_desired
219
221
  })),
220
222
  heartbeat_delay_ms: persona.heartbeat_delay_ms ? formatDuration(persona.heartbeat_delay_ms) : null,
221
- context_window_hours: persona.context_window_hours ?? null,
223
+ context_window_ms: persona.context_window_ms ? formatDuration(persona.context_window_ms) : null,
222
224
  is_paused: persona.is_paused || undefined,
223
225
  pause_until: persona.pause_until,
224
226
  is_static: persona.is_static || undefined,
@@ -328,7 +330,9 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity, al
328
330
  heartbeat_delay_ms: data.heartbeat_delay_ms == null
329
331
  ? undefined
330
332
  : parseDuration(data.heartbeat_delay_ms) ?? undefined,
331
- context_window_hours: data.context_window_hours ?? undefined,
333
+ context_window_ms: data.context_window_ms == null
334
+ ? undefined
335
+ : parseDuration(data.context_window_ms) ?? undefined,
332
336
  is_paused: data.is_paused ?? false,
333
337
  pause_until: data.pause_until,
334
338
  is_static: data.is_static ?? false,
@@ -16,7 +16,7 @@ interface EditableSettingsData {
16
16
  rewrite_model?: string | null;
17
17
  name_display?: string | null;
18
18
  default_heartbeat_ms?: string | null;
19
- default_context_window_hours?: number | null;
19
+ default_context_window_ms?: string | null;
20
20
  message_min_count?: number | null;
21
21
  message_max_age_days?: number | null;
22
22
  ceremony?: {
@@ -65,7 +65,7 @@ export function settingsToYAML(settings: HumanSettings | undefined, accounts: Pr
65
65
  rewrite_model: guidToDisplay(settings?.rewrite_model),
66
66
  name_display: settings?.name_display ?? null,
67
67
  default_heartbeat_ms: formatDuration(settings?.default_heartbeat_ms ?? 1800000),
68
- default_context_window_hours: settings?.default_context_window_hours ?? 8,
68
+ default_context_window_ms: formatDuration(settings?.default_context_window_ms ?? 28800000),
69
69
  message_min_count: settings?.message_min_count ?? 200,
70
70
  message_max_age_days: settings?.message_max_age_days ?? 14,
71
71
  ceremony: {
@@ -190,7 +190,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
190
190
  rewrite_model: displayToGuid(data.rewrite_model),
191
191
  name_display: nullToUndefined(data.name_display),
192
192
  default_heartbeat_ms: parseMsDuration(data.default_heartbeat_ms, 1800000),
193
- default_context_window_hours: nullToUndefined(data.default_context_window_hours),
193
+ default_context_window_ms: parseMsDuration(data.default_context_window_ms, 28800000),
194
194
  message_min_count: nullToUndefined(data.message_min_count),
195
195
  message_max_age_days: nullToUndefined(data.message_max_age_days),
196
196
  ceremony,
@@ -1,55 +0,0 @@
1
- import { LLMRequestType, LLMPriority, LLMNextStep } from "../types.js";
2
- import type { StateManager } from "../state-manager.js";
3
- import { buildPersonMigrationPrompt } from "../../prompts/ceremony/index.js";
4
-
5
- export function queuePersonMigration(state: StateManager): void {
6
- const human = state.getHuman();
7
-
8
- if (human.settings?.people_migration_complete) {
9
- console.log("[PersonMigration] Migration complete flag set — skipping");
10
- return;
11
- }
12
-
13
- const unmigrated = human.people.filter(p => !p.identifiers || p.identifiers.length === 0);
14
-
15
- if (unmigrated.length === 0) {
16
- console.log("[PersonMigration] All Person records have identifiers — marking migration complete");
17
- state.setHuman({
18
- ...human,
19
- settings: {
20
- ...human.settings,
21
- people_migration_complete: true,
22
- },
23
- });
24
- return;
25
- }
26
-
27
- console.log(`[PersonMigration] Queuing migration for ${unmigrated.length} Person record(s)`);
28
-
29
- const rewriteModel = human.settings?.rewrite_model;
30
-
31
- for (const person of unmigrated) {
32
- const prompt = buildPersonMigrationPrompt({
33
- person: {
34
- name: person.name,
35
- description: person.description,
36
- relationship: person.relationship,
37
- },
38
- });
39
-
40
- state.queue_enqueue({
41
- type: LLMRequestType.JSON,
42
- priority: LLMPriority.Normal,
43
- system: prompt.system,
44
- user: prompt.user,
45
- next_step: LLMNextStep.HandlePersonIdentifierMigration,
46
- ...(rewriteModel ? { model: rewriteModel } : {}),
47
- data: {
48
- person_id: person.id,
49
- ceremony_progress: 1,
50
- },
51
- });
52
- }
53
-
54
- console.log(`[PersonMigration] Queued ${unmigrated.length} migration request(s)`);
55
- }
@@ -1,54 +0,0 @@
1
- import type { DescriptionCheckPromptData } from "./types.js";
2
-
3
- export function buildDescriptionCheckPrompt(data: DescriptionCheckPromptData): { system: string; user: string } {
4
- const traitList = data.traits.length > 0
5
- ? data.traits.map(t => `- ${t.name}: ${t.description}`).join("\n")
6
- : "No traits defined";
7
-
8
- const topicList = data.topics.length > 0
9
- ? data.topics.map(t => `- ${t.name}: ${t.perspective} (exposure: ${t.exposure_current.toFixed(2)})`).join("\n")
10
- : "No topics defined";
11
-
12
- const system = `You are evaluating whether a persona's description needs updating.
13
-
14
- A description should ONLY be updated if there is a SIGNIFICANT mismatch between:
15
- - The current description
16
- - The persona's current traits and topics
17
-
18
- Be VERY conservative. Only recommend updating if:
19
- - The description mentions interests/traits that are completely absent from current data
20
- - The persona has developed major new interests not reflected in the description
21
- - The description actively contradicts the current personality
22
-
23
- Do NOT recommend updating for:
24
- - Minor differences or evolution
25
- - Natural topic drift over time
26
- - Missing details that aren't contradictions
27
-
28
- Return JSON: { "should_update": true/false, "reason": "explanation" }`;
29
-
30
- const user = `Persona: ${data.persona_name}
31
-
32
- Current short description:
33
- ${data.current_short_description ?? "(none)"}
34
-
35
- Current long description:
36
- ${data.current_long_description ?? "(none)"}
37
-
38
- Current personality traits:
39
- ${traitList}
40
-
41
- Current topics of interest:
42
- ${topicList}
43
-
44
- Does this persona's description need updating based on their current traits and topics?
45
-
46
- **Return JSON:**
47
- \`\`\`json
48
- { "should_update": true, "reason": "explanation" }
49
- \`\`\`
50
-
51
- If no update is needed: \`{ "should_update": false, "reason": "explanation" }\``;
52
-
53
- return { system, user };
54
- }
@@ -1,37 +0,0 @@
1
- import type { PersonaExpirePromptData } from "./types.js";
2
-
3
- export function buildPersonaExpirePrompt(data: PersonaExpirePromptData): { system: string; user: string } {
4
- const topicList = data.topics.map(t => {
5
- const display = t.perspective || t.name;
6
- return `- "${t.name}" (exposure: ${t.exposure_current.toFixed(2)}, sentiment: ${t.sentiment.toFixed(2)})\n Perspective: ${display}`;
7
- }).join("\n");
8
-
9
- const system = `You are evaluating which topics a persona should stop caring about.
10
-
11
- A topic should be removed if:
12
- - Its exposure_current is very low (< 0.15) indicating prolonged disinterest
13
- - It no longer aligns with the persona's current interests
14
- - It was a temporary interest that has faded
15
-
16
- Be conservative - only suggest removing topics that are clearly irrelevant.
17
- If unsure, keep the topic.
18
-
19
- Return JSON: { "topic_ids_to_remove": ["id1", "id2"] }
20
- Return empty array if no topics should be removed.`;
21
-
22
- const user = `Persona: ${data.persona_name}
23
-
24
- Current topics:
25
- ${topicList}
26
-
27
- Which topics, if any, should this persona stop caring about?
28
-
29
- **Return JSON:**
30
- \`\`\`json
31
- { "topic_ids_to_remove": ["id1", "id2"] }
32
- \`\`\`
33
-
34
- Return an empty array if no topics should be removed.`;
35
-
36
- return { system, user };
37
- }
@@ -1,77 +0,0 @@
1
- import type { PersonaExplorePromptData } from "./types.js";
2
-
3
- export function buildPersonaExplorePrompt(data: PersonaExplorePromptData): { system: string; user: string } {
4
- const traitList = data.traits.map(t =>
5
- `- ${t.name}: ${t.description} (strength: ${t.strength?.toFixed(2) ?? "N/A"})`
6
- ).join("\n");
7
-
8
- const topicList = data.remaining_topics.map(t =>
9
- `- ${t.name}: ${t.perspective || t.name}`
10
- ).join("\n");
11
-
12
- const themeList = data.recent_conversation_themes.length > 0
13
- ? data.recent_conversation_themes.map(t => `- ${t}`).join("\n")
14
- : "No recent themes identified";
15
-
16
- const system = `You are generating new conversation topics for a persona.
17
-
18
- Topics should:
19
- - Align with the persona's traits and personality
20
- - Complement (not duplicate) existing topics
21
- - Be natural extensions of recent conversation themes
22
- - Be specific enough to drive interesting conversations
23
-
24
- Generate 1-3 new topics that this persona would genuinely care about.
25
-
26
- Return JSON:
27
- {
28
- "new_topics": [{
29
- "name": "Topic Name",
30
- "perspective": "The persona's view or opinion on this topic",
31
- "approach": "How they prefer to engage with this topic",
32
- "personal_stake": "Why this topic matters to them personally",
33
- "sentiment": 0.5,
34
- "exposure_current": 0.2,
35
- "exposure_desired": 0.6
36
- }]
37
- }
38
-
39
- **Field guidance:**
40
- - perspective: REQUIRED - their actual view/opinion
41
- - approach: Optional if unclear - how they discuss this topic
42
- - personal_stake: Optional if unclear - why it matters to them
43
- - exposure_current: Low (0.2) since these are new topics
44
- - exposure_desired: How much the persona would want to discuss this`;
45
-
46
- const user = `Persona: ${data.persona_name}
47
-
48
- Personality traits:
49
- ${traitList}
50
-
51
- Current topics (do not duplicate):
52
- ${topicList}
53
-
54
- Recent conversation themes:
55
- ${themeList}
56
-
57
- Generate new topics this persona would care about.
58
-
59
- **Return JSON:**
60
- \`\`\`json
61
- {
62
- "new_topics": [
63
- {
64
- "name": "Topic Name",
65
- "perspective": "Their view or opinion",
66
- "approach": "How they engage with it",
67
- "personal_stake": "Why it matters to them",
68
- "sentiment": 0.5,
69
- "exposure_current": 0.2,
70
- "exposure_desired": 0.6
71
- }
72
- ]
73
- }
74
- \`\`\``;
75
-
76
- return { system, user };
77
- }
@@ -1,77 +0,0 @@
1
- import type { Person } from "../../core/types/data-items.js";
2
- import type { PromptOutput } from "../persona/types.js";
3
-
4
- export interface PersonMigrationPromptData {
5
- person: Pick<Person, "name" | "description" | "relationship">;
6
- }
7
-
8
- export function buildPersonMigrationPrompt(data: PersonMigrationPromptData): PromptOutput {
9
- const { person } = data;
10
-
11
- const system = `You are extracting identifiers for a Person record in a personal knowledge system.
12
-
13
- A Person record has a \`name\` and \`description\` that describe who they are. Your job is to extract ALL identifiers for this person — every name, handle, alias, or platform ID that refers to them.
14
-
15
- ## CRITICAL RULES
16
-
17
- 1. The \`name\` field value MUST appear in identifiers — never lose it.
18
- 2. If \`name\` contains a space (e.g. "Jeremy Scherer"), create BOTH a \`Full Name\` identifier AND a \`First Name\` identifier for the given name.
19
- 3. Mark exactly one identifier as \`is_primary: true\` — the most natural display name.
20
- 4. Check \`read_memory\` for additional context before finalizing.
21
- 5. Return ONLY a JSON object with an \`identifiers\` array. No other text.
22
- 6. "none" is never valid — every person has at least one identifier (their name).
23
-
24
- ## DESCRIPTION PRE-PROCESSING
25
-
26
- If the description begins with a JSON block (e.g. \`{"identifiers": [...]}\` or \`[{...}]\`), those are pre-seeded identifiers from a prior migration step:
27
- - Parse them out and include them in your output
28
- - Normalize their \`type\` values to Title Case (e.g. \`nickname\` → \`Nickname\`, \`full_name\` → \`Full Name\`)
29
- - Treat the remainder of the description (after the JSON block) as the actual description text for \`read_memory\` context
30
-
31
- ## IDENTIFIER TYPES
32
-
33
- Use Title Case for all types. Built-in types:
34
-
35
- | Type | Meaning |
36
- |------|---------|
37
- | Full Name | Legal or full birth name |
38
- | First Name | Given/first name only |
39
- | Nickname | Informal name, diminutive, pet name |
40
- | Email | Email address |
41
- | GitHub | GitHub username |
42
- | Discord | Discord username |
43
- | Roblox | Roblox username |
44
- | Reddit | Reddit username |
45
- | Twitter | Twitter/X handle |
46
- | FF14 | Final Fantasy XIV character name |
47
- | Relationship | How the user addresses this person: Dad, Pop, Sis, etc. |
48
- | Ei Persona | Ei persona UUID (only if explicitly in the data) |
49
-
50
- Any string is valid as a type — users define their own (e.g. \`Slack-ASU\`, \`Slack-RnP\`, \`sehimu_thinara\`). Use the exact casing the user would recognize.
51
-
52
- ## RESPONSE FORMAT
53
-
54
- \`\`\`json
55
- {
56
- "identifiers": [
57
- { "type": "Nickname", "value": "Flare", "is_primary": true },
58
- { "type": "Full Name", "value": "Jeremy Scherer" },
59
- { "type": "First Name", "value": "Jeremy" },
60
- { "type": "GitHub", "value": "Flare576" },
61
- { "type": "Slack-RnP", "value": "jeremy.scherer" }
62
- ]
63
- }
64
- \`\`\``;
65
-
66
- const user = `Extract identifiers for this Person record.
67
-
68
- Name: ${person.name}
69
- Relationship: ${person.relationship ?? "unknown"}
70
- Description: ${person.description ?? "(none)"}
71
-
72
- First, call read_memory to search for any additional context about "${person.name}". Then return the complete identifiers array.
73
-
74
- Return JSON only.`;
75
-
76
- return { system, user };
77
- }
@@ -1,91 +0,0 @@
1
- import type { PersonaDescriptionsPromptData, PromptOutput } from "./types.js";
2
- import type { PersonaTrait, PersonaTopic } from "../../core/types.js";
3
-
4
- function formatTraitsForPrompt(traits: PersonaTrait[]): string {
5
- if (traits.length === 0) return "(No traits defined)";
6
-
7
- return traits.map(t => {
8
- const strength = t.strength !== undefined ? ` (strength: ${t.strength.toFixed(1)})` : "";
9
- return `- **${t.name}**${strength}: ${t.description}`;
10
- }).join('\n');
11
- }
12
-
13
- function formatTopicsForPrompt(topics: PersonaTopic[]): string {
14
- if (topics.length === 0) return "(No topics defined)";
15
-
16
- return topics.map(t => {
17
- const sentiment = t.sentiment > 0.3 ? "enjoys" : t.sentiment < -0.3 ? "dislikes" : "neutral";
18
- return `- **${t.name}** (${sentiment}): ${t.perspective || t.name}`;
19
- }).join('\n');
20
- }
21
-
22
- export function buildPersonaDescriptionsPrompt(data: PersonaDescriptionsPromptData): PromptOutput {
23
- if (!data.name) {
24
- throw new Error("buildPersonaDescriptionsPrompt: name is required");
25
- }
26
-
27
- const taskFragment = `You are regenerating descriptions for an existing AI persona named "${data.name}".
28
-
29
- Their traits and topics have evolved, and the descriptions may no longer accurately represent who they are.
30
-
31
- **Important**: Only change descriptions if there's a significant mismatch. If the current descriptions still fit, return \`{ "no_change": true }\`.`;
32
-
33
- const currentStateFragment = `## Current State
34
-
35
- ### Aliases
36
- ${data.aliases.length > 0 ? data.aliases.join(", ") : "(None)"}
37
-
38
- ### Traits
39
- ${formatTraitsForPrompt(data.traits)}
40
-
41
- ### Topics
42
- ${formatTopicsForPrompt(data.topics)}`;
43
-
44
- const guidelinesFragment = `## Guidelines
45
-
46
- **When to change:**
47
- - Traits/topics have shifted significantly from the original concept
48
- - Current descriptions mention things that are no longer true
49
- - The persona's core identity has evolved
50
-
51
- **When NOT to change:**
52
- - Descriptions are still accurate even if incomplete
53
- - Changes are minor refinements
54
- - Original descriptions capture the essence well
55
-
56
- **If changing:**
57
- - short_description: 10-15 words capturing the essence
58
- - long_description: 2-3 sentences describing personality, interests, approach
59
- - Preserve the persona's core identity while reflecting evolution`;
60
-
61
- const schemaFragment = `## Response Format
62
-
63
- If descriptions should change:
64
- \`\`\`json
65
- {
66
- "short_description": "New short description here",
67
- "long_description": "New long description here."
68
- }
69
- \`\`\`
70
-
71
- If descriptions are still accurate:
72
- \`\`\`json
73
- {
74
- "no_change": true
75
- }
76
- \`\`\``;
77
-
78
- const system = `${taskFragment}
79
-
80
- ${currentStateFragment}
81
-
82
- ${guidelinesFragment}
83
-
84
- ${schemaFragment}`;
85
-
86
- const user = `Based on the traits and topics above, should ${data.name}'s descriptions be updated?
87
-
88
- Remember: Only change if there's a significant mismatch. Stability is preferred.`;
89
-
90
- return { system, user };
91
- }